Skip to content

Commit

Permalink
feat(credential): create dataset description credential
Browse files Browse the repository at this point in the history
feat(credential): create dataset description credential
  • Loading branch information
bdeneux committed Sep 4, 2024
1 parent 07895c8 commit c7d32af
Show file tree
Hide file tree
Showing 6 changed files with 358 additions and 13 deletions.
125 changes: 125 additions & 0 deletions credential/template/dataset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package template

import (
"bytes"
_ "embed"
"errors"
gotemplate "text/template"
"time"

"github.com/axone-protocol/axone-sdk/credential"
"github.com/axone-protocol/axone-sdk/dataverse"
"github.com/google/uuid"
)

//go:embed vc-desc-tpl.jsonld
var datasetTemplate string

var _ credential.Descriptor = NewDataset()

type DatasetDescriptor struct {
id string
datasetDID string
title string
description string
format string
tags []string
topic string
issuanceDate *time.Time
}

func NewDataset() *DatasetDescriptor {
t := time.Now().UTC()
return &DatasetDescriptor{
id: uuid.New().String(),
issuanceDate: &t,
}
}

func (d *DatasetDescriptor) WithID(id string) *DatasetDescriptor {
d.id = id
return d
}

func (d *DatasetDescriptor) WithDatasetDID(did string) *DatasetDescriptor {
d.datasetDID = did
return d
}

func (d *DatasetDescriptor) WithTitle(title string) *DatasetDescriptor {
d.title = title
return d
}

func (d *DatasetDescriptor) WithDescription(description string) *DatasetDescriptor {
d.description = description
return d
}

func (d *DatasetDescriptor) WithFormat(format string) *DatasetDescriptor {
d.format = format
return d
}

func (d *DatasetDescriptor) WithTags(tags []string) *DatasetDescriptor {
d.tags = tags
return d
}

func (d *DatasetDescriptor) WithTopic(topic string) *DatasetDescriptor {
d.topic = topic
return d
}

func (d *DatasetDescriptor) WithIssuanceDate(t time.Time) *DatasetDescriptor {
d.issuanceDate = &t
return d
}

func (d *DatasetDescriptor) validate() error {
if d.datasetDID == "" {
return errors.New("dataset DID is required")
}
if d.title == "" {
return errors.New("title is required")
}
return nil
}

func (d *DatasetDescriptor) IssuedAt() *time.Time {
return d.issuanceDate

Check warning on line 90 in credential/template/dataset.go

View check run for this annotation

Codecov / codecov/patch

credential/template/dataset.go#L89-L90

Added lines #L89 - L90 were not covered by tests
}

func (d *DatasetDescriptor) ProofPurpose() string {
return "assertionMethod"

Check warning on line 94 in credential/template/dataset.go

View check run for this annotation

Codecov / codecov/patch

credential/template/dataset.go#L93-L94

Added lines #L93 - L94 were not covered by tests
}

func (d *DatasetDescriptor) Generate() (*bytes.Buffer, error) {
err := d.validate()
if err != nil {
return nil, err
}

tpl, err := gotemplate.New("datasetDescriptionVC").Parse(datasetTemplate)
if err != nil {
return nil, err

Check warning on line 105 in credential/template/dataset.go

View check run for this annotation

Codecov / codecov/patch

credential/template/dataset.go#L105

Added line #L105 was not covered by tests
}

buf := bytes.Buffer{}
err = tpl.Execute(&buf, map[string]any{
"NamespacePrefix": dataverse.W3IDPrefix,
"CredID": d.id,
"DatasetDID": d.datasetDID,
"Title": d.title,
"Description": d.description,
"Format": d.format,
"Tags": d.tags,
"Topic": d.topic,
"IssuedAt": d.issuanceDate.Format(time.RFC3339),
})
if err != nil {
return nil, err

Check warning on line 121 in credential/template/dataset.go

View check run for this annotation

Codecov / codecov/patch

credential/template/dataset.go#L121

Added line #L121 was not covered by tests
}

return &buf, nil
}
157 changes: 157 additions & 0 deletions credential/template/dataset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package template

import (
"errors"
"testing"
"time"

"github.com/axone-protocol/axone-sdk/credential"
"github.com/axone-protocol/axone-sdk/testutil"
"github.com/hyperledger/aries-framework-go/pkg/doc/verifiable"
. "github.com/smartystreets/goconvey/convey"
)

func TestDatasetDescriptor_Generate(t *testing.T) {
tests := []struct {
name string
vc credential.Descriptor
wantErr error
check func(*verifiable.Credential)
}{
{
name: "Valid dataset VC",
vc: NewDataset().
WithID("id").
WithDatasetDID("datasetID").
WithTitle("title").
WithDescription("description").
WithFormat("format").
WithTags([]string{"tag1", "tag2"}).
WithTopic("topic").
WithIssuanceDate(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)),
check: func(vc *verifiable.Credential) {
So(vc.ID, ShouldEqual, "https://w3id.org/axone/ontology/v4/schema/credential/dataset/description/id")
So(vcSubject(vc).ID, ShouldEqual, "datasetID")
So(vc.Issuer.ID, ShouldEqual, "datasetID")
So(vcSubject(vc).CustomFields["hasTitle"], ShouldEqual, "title")
So(vcSubject(vc).CustomFields["hasDescription"], ShouldEqual, "description")
So(vcSubject(vc).CustomFields["hasFormat"], ShouldEqual, "format")
So(vcSubject(vc).CustomFields["hasTag"], ShouldResemble, []interface{}{"tag1", "tag2"})
So(vcSubject(vc).CustomFields["hasTopic"], ShouldEqual, "topic")
So(vc.Issued.Time, ShouldEqual, time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC))
},
},
{
name: "Valid dataset VC with default issuance date",
vc: NewDataset().
WithID("id").
WithDatasetDID("datasetID").
WithTitle("title").
WithDescription("description").
WithFormat("format").
WithTags([]string{"tag1", "tag2"}).
WithTopic("topic"),
check: func(vc *verifiable.Credential) {
So(vc.ID, ShouldEqual, "https://w3id.org/axone/ontology/v4/schema/credential/dataset/description/id")
So(vcSubject(vc).ID, ShouldEqual, "datasetID")
So(vc.Issuer.ID, ShouldEqual, "datasetID")
So(vcSubject(vc).CustomFields["hasTitle"], ShouldEqual, "title")
So(vcSubject(vc).CustomFields["hasDescription"], ShouldEqual, "description")
So(vcSubject(vc).CustomFields["hasFormat"], ShouldEqual, "format")
So(vcSubject(vc).CustomFields["hasTag"], ShouldResemble, []interface{}{"tag1", "tag2"})
So(vcSubject(vc).CustomFields["hasTopic"], ShouldEqual, "topic")
So(vc.Issued.Time, ShouldHappenWithin, time.Second, time.Now().UTC())
},
},
{
name: "Valid dataset VC with generated id",
vc: NewDataset().
WithDatasetDID("datasetID").
WithTitle("title").
WithDescription("description").
WithFormat("format").
WithTags([]string{"tag1", "tag2"}).
WithTopic("topic").
WithIssuanceDate(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)),
check: func(vc *verifiable.Credential) {
So(vc.ID, ShouldStartWith, "https://w3id.org/axone/ontology/v4/schema/credential/dataset/description/")
So(vcSubject(vc).ID, ShouldEqual, "datasetID")
So(vc.Issuer.ID, ShouldEqual, "datasetID")
So(vcSubject(vc).CustomFields["hasTitle"], ShouldEqual, "title")
So(vcSubject(vc).CustomFields["hasDescription"], ShouldEqual, "description")
So(vcSubject(vc).CustomFields["hasFormat"], ShouldEqual, "format")
So(vcSubject(vc).CustomFields["hasTag"], ShouldResemble, []interface{}{"tag1", "tag2"})
So(vcSubject(vc).CustomFields["hasTopic"], ShouldEqual, "topic")
So(vc.Issued.Time, ShouldEqual, time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC))
},
},
{
name: "Missing title",
vc: NewDataset().
WithID("id").
WithDatasetDID("datasetID").
WithDescription("description").
WithFormat("format").
WithTags([]string{"tag1", "tag2"}).
WithTopic("topic").
WithIssuanceDate(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)),
wantErr: credential.NewVCError(credential.ErrGenerate, errors.New("title is required")),
},
{
name: "Missing dataset DID",
vc: NewDataset().
WithID("id").
WithTitle("title").
WithDescription("description").
WithFormat("format").
WithTags([]string{"tag1", "tag2"}).
WithTopic("topic").
WithIssuanceDate(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)),
wantErr: credential.NewVCError(credential.ErrGenerate, errors.New("dataset DID is required")),
},
{
name: "Valid dataset VC without optional value",
vc: NewDataset().
WithDatasetDID("datasetID").
WithTitle("title"),
check: func(vc *verifiable.Credential) {
So(vcSubject(vc).ID, ShouldEqual, "datasetID")
So(vc.Issuer.ID, ShouldEqual, "datasetID")
So(vcSubject(vc).CustomFields["hasTitle"], ShouldEqual, "title")
So(vcSubject(vc).CustomFields["hasDescription"], ShouldEqual, "")
So(vcSubject(vc).CustomFields["hasFormat"], ShouldEqual, "")
So(vcSubject(vc).CustomFields["hasTag"], ShouldResemble, []interface{}{})
So(vcSubject(vc).CustomFields["hasTopic"], ShouldEqual, "")
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
Convey("Given a credential generator", t, func() {
docLoader, err := testutil.MockDocumentLoader()
So(err, ShouldBeNil)

parser := credential.NewDefaultParser(docLoader)
generator := credential.New(test.vc).
WithParser(parser)

Convey("When a governance VC is generated", func() {
vc, err := generator.Generate()

Convey("Then the governance VC should be generated", func() {
if test.wantErr != nil {
So(err, ShouldNotBeNil)
So(err.Error(), ShouldEqual, test.wantErr.Error())
So(vc, ShouldBeNil)
} else {
So(vc, ShouldNotBeNil)
test.check(vc)
So(err, ShouldBeNil)
}
})
})
})
})
}
}
23 changes: 10 additions & 13 deletions credential/template/governance.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"bytes"
_ "embed"
"errors"
"text/template"
gotemplate "text/template"
"time"

"github.com/axone-protocol/axone-sdk/credential"
Expand All @@ -27,9 +27,13 @@ type GovernanceDescriptor struct {

// NewGovernance creates a new governance verifiable credential descriptor.
// DatasetDID and GovAddr are required. If ID is not provided, it will be generated.
// If issuance date is not provided, it will be set to the current time at the generation.
// If issuance date is not provided, it will be set to the current time at descriptor instantiation.
func NewGovernance() *GovernanceDescriptor {
return &GovernanceDescriptor{}
t := time.Now().UTC()
return &GovernanceDescriptor{
id: uuid.New().String(),
issuanceDate: &t,
}
}

func (g *GovernanceDescriptor) WithID(id string) *GovernanceDescriptor {
Expand All @@ -52,20 +56,13 @@ func (g *GovernanceDescriptor) WithIssuanceDate(t time.Time) *GovernanceDescript
return g
}

func (g *GovernanceDescriptor) prepare() error {
if g.id == "" {
g.id = uuid.New().String()
}
func (g *GovernanceDescriptor) validate() error {
if g.datasetDID == "" {
return errors.New("dataset DID is required")
}
if g.govAddr == "" {
return errors.New("governance address is required")
}
if g.issuanceDate == nil {
t := time.Now().UTC()
g.issuanceDate = &t
}
return nil
}

Expand All @@ -78,12 +75,12 @@ func (g *GovernanceDescriptor) ProofPurpose() string {
}

func (g *GovernanceDescriptor) Generate() (*bytes.Buffer, error) {
err := g.prepare()
err := g.validate()
if err != nil {
return nil, err
}

tpl, err := template.New("governanceVC").Parse(governanceTemplate)
tpl, err := gotemplate.New("governanceVC").Parse(governanceTemplate)
if err != nil {
return nil, err

Check warning on line 85 in credential/template/governance.go

View check run for this annotation

Codecov / codecov/patch

credential/template/governance.go#L85

Added line #L85 was not covered by tests
}
Expand Down
21 changes: 21 additions & 0 deletions credential/template/vc-desc-tpl.jsonld
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"{{ .NamespacePrefix }}/schema/credential/dataset/description/"
],
"type": [
"VerifiableCredential",
"DatasetDescriptionCredential"
],
"id": "{{ .NamespacePrefix }}/schema/credential/dataset/description/{{ .CredID }}",
"credentialSubject": {
"id": "{{ .DatasetDID }}",
"hasTitle": "{{ .Title }}",
"hasDescription": "{{ .Description }}",
"hasFormat": "{{ .Format }}",
"hasTag": [{{ with .Tags }}{{ range $i, $tag := . }}{{ if $i }},{{ end }}"{{ $tag }}"{{ end }}{{ end }}],
"hasTopic":"{{ .Topic }}"
},
"issuanceDate": "{{ .IssuedAt }}",
"issuer": "{{ .DatasetDID }}"
}
Loading

0 comments on commit c7d32af

Please sign in to comment.