diff --git a/.vscode/settings.json b/.vscode/settings.json index 57abda7..33513f2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,7 @@ { "licenser.author": "go-dataspace", - "licenser.license": "AL2" + "licenser.license": "AL2", + "go.addTags": { + "transform": "camelcase" + } } diff --git a/dsp/catalog_types.go b/dsp/catalog_types.go new file mode 100644 index 0000000..599f798 --- /dev/null +++ b/dsp/catalog_types.go @@ -0,0 +1,98 @@ +// Copyright 2024 go-dataspace +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dsp + +import ( + "github.com/go-dataspace/run-dsp/jsonld" + "github.com/go-dataspace/run-dsp/odrl" +) + +// CatalogRequestMessage is a message to request a catalog. Note that the filter format is defined +// as "implementation specific" and nothing else in the spec. +// TODO: define how we want to support filters. +type CatalogRequestMessage struct { + Context jsonld.Context `json:"@context"` + Type string `json:"@type" validate:"required,eq=dspace:CatalogRequestMessage"` + Filter []any `json:"dspace:filter"` +} + +// DatasetRequestMessage is a message to request a dataset. +type DatasetRequestMessage struct { + Context jsonld.Context `json:"@context"` + Type string `json:"@type" validate:"required,eq=dspace:DatasetRequestMessage"` + Dataset string `json:"dspace:dataset" validate:"required"` +} + +// CatalogAcknowledgement is an acknowledgement for a catalog, containing a dataset. +type CatalogAcknowledgement struct { + Dataset + Context jsonld.Context `json:"@context"` + Type string `json:"@type" validate:"required,eq=dcat:Catalog"` + Datasets []Dataset `json:"dcat:dataset" validate:"gte=1,dive"` + Service []DataService `json:"dcat:service" validate:"gte=1,dive"` + ParticipantID string `json:"dspace:participantID,omitempty"` + Homepage string `json:"foaf:homepage,omitempty"` +} + +// CatalogError is a standardised error for catalog requests. +type CatalogError struct { + Context jsonld.Context `json:"@context"` + Type string `json:"@type" validate:"required,eq=dspace:CatalogError"` + Code string `json:"dspace:code"` + Reason []map[string]any `json:"dspace:reason"` +} + +// Dataset is a DCAT dataset. +type Dataset struct { + Resource + HasPolicy []odrl.Offer `json:"odrl:hasPolicy" validate:"required,gte=1,dive"` + Distribution []Distribution `json:"dcat:distribution" validate:"gte=1,dive"` +} + +// Resource is a DCAT resource. +type Resource struct { + Keyword []string `json:"dcat:keyword,omitempty"` + Theme []Reference `json:"dcat:them,omitempty" validate:"gte=1,dive"` + ConformsTo string `json:"dct:conformsTo,omitempty"` + Creator string `json:"dct:creator,omitempty"` + Description []Multilanguage `json:"dct:description,omitempty" validate:"dive"` + Identifier string `json:"dct:identifier,omitempty"` + Issued string `json:"dct:issued,omitempty"` + Modified string `json:"dct:modified,omitempty"` + Title string `json:"dct:title,omitempty"` +} + +// Distribution is a DCAT distribution. +type Distribution struct { + Title string `json:"dct:title,omitempty"` + Description []Multilanguage `json:"dct:description,omitempty" validate:"dive"` + Issued string `json:"dct:issued,omitempty"` + Modified string `json:"dct:modified,omitempty"` + HasPolicy []odrl.Offer `json:"odrl:hasPolicy" validate:"gte=1,dive"` + AccessService []DataService `json:"dcat:accessService" validate:"required,gte=1,dive"` +} + +// DataService is a DCAT dataservice. +type DataService struct { + Resource + EndpointDescription string `json:"dcat:endpointDescription,omitempty"` + EndpointURL string `json:"dcat:endpointURL,omitempty"` + ServesDataset []Dataset `json:"dcat:servesDataset,omitempty"` +} + +// Reference is a DCAT reference. +type Reference struct { + ID string `json:"@id" validate:"required"` +} diff --git a/dsp/common_types.go b/dsp/common_types.go new file mode 100644 index 0000000..4c4457d --- /dev/null +++ b/dsp/common_types.go @@ -0,0 +1,41 @@ +// Copyright 2024 go-dataspace +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dsp + +// This file contains types to support the IDSA dataspace protocol. +// Currently we support 2024-1. +// Reference: https://docs.internationaldataspaces.org/ids-knowledgebase/v/dataspace-protocol + +import ( + "github.com/go-dataspace/run-dsp/jsonld" +) + +// VersionResponse contains multiple protocol version specifications. +type VersionResponse struct { + Context jsonld.Context `json:"@context"` + ProtocolVersions []ProtocolVersion `json:"protocolVersions" validate:"required,gte=1,dive"` +} + +// ProtocolVersion contains a version and the path to the endpoints. +type ProtocolVersion struct { + Version string `json:"version" validate:"required"` + Path string `json:"path" validate:"required,dirpath"` +} + +// Multilanguage is a DCAT multilanguage set. +type Multilanguage struct { + Value string `json:"@value" validate:"required"` + Language string `json:"@language" validate:"required"` +} diff --git a/dsp/contract_types.go b/dsp/contract_types.go new file mode 100644 index 0000000..2342ac2 --- /dev/null +++ b/dsp/contract_types.go @@ -0,0 +1,97 @@ +// Copyright 2024 go-dataspace +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dsp + +import ( + "github.com/go-dataspace/run-dsp/jsonld" + "github.com/go-dataspace/run-dsp/odrl" +) + +// ContractRequestMessage is a dsp contract request. +type ContractRequestMessage struct { + Context jsonld.Context `json:"@context"` + Type string `json:"@type" validate:"required,eq=dspace:ContractRequestMessage"` + ProviderPID string `json:"dspace:providerPid"` + ConsumerPID string `json:"dspace:consumerPid" validate:"required"` + Offer odrl.MessageOffer `json:"dspace:offer" validate:"required"` + CallbackAddress string `json:"dspace:callbackAddress" validate:"required"` +} + +// ContractOfferMessage is a DSP contract offer. +type ContractOfferMessage struct { + Context jsonld.Context `json:"@context"` + Type string `json:"@type" validate:"required,eq=dspace:ContractOfferMessage"` + ProviderPID string `json:"dspace:providerPid" validate:"required"` + ConsumerPID string `json:"dspace:consumerPid"` + Offer odrl.MessageOffer `json:"dspace:offer" validate:"required"` + CallbackAddress string `json:"dspace:callbackAddress" validate:"required"` +} + +// ContractAgreementMessage is a DSP contract agreement. +type ContractAgreementMessage struct { + Context jsonld.Context `json:"@context"` + Type string `json:"@type" validate:"required,eq=dspace:ContractAgreementMessage"` + ProviderPID string `json:"dspace:providerPid" validate:"required"` + ConsumerPID string `json:"dspace:consumerPid"` + Agreement odrl.Agreement `json:"dspace:agreement" validate:"required"` + CallbackAddress string `json:"dspace:callbackAddress" validate:"required"` +} + +// ContractAgreementVerificationMessage verifies the contract agreement. +type ContractAgreementVerificationMessage struct { + Context jsonld.Context `json:"@context"` + Type string `json:"@type" validate:"required,eq=dspace:ContractAgreementMessage"` + ProviderPID string `json:"dspace:providerPid" validate:"required"` + ConsumerPID string `json:"dspace:consumerPid" validate:"required"` +} + +// ContractNegotiationEventMessage notifies of a contract event. +type ContractNegotiationEventMessage struct { + Context jsonld.Context `json:"@context"` + Type string `json:"@type" validate:"required,eq=dspace:ContractAgreementMessage"` + ProviderPID string `json:"dspace:providerPid" validate:"required"` + ConsumerPID string `json:"dspace:consumerPid" validate:"required"` + EventType string `json:"dspace:eventType" validate:"required,oneof=dspace:ACCEPTED dspace:FINALIZED"` +} + +// ContractNegotiationTerminationMessage terminates the negotiation. +type ContractNegotiationTerminationMessage struct { + Context jsonld.Context `json:"@context"` + Type string `json:"@type" validate:"required,eq=dspace:ContractNegotiationTerminationMessage"` + ProviderPID string `json:"dspace:providerPid" validate:"required"` + ConsumerPID string `json:"dspace:consumerPid" validate:"required"` + Code string `json:"dspace:code"` + Reason []map[string]any `json:"dspace:reason"` +} + +// ContractNegotiation is a response to show the state of the contract negotiation. +type ContractNegotiation struct { + Context jsonld.Context `json:"@context"` + Type string `json:"@type" validate:"required,eq=dspace:ContractNegotiation"` + ProviderPID string `json:"dspace:providerPid" validate:"required"` + ConsumerPID string `json:"dspace:consumerPid" validate:"required"` + State string `json:"dspace:state" validate:"required,contract_state"` +} + +// ContractNegotiationError is a response to show the state of the contract negotiation. +type ContractNegotiationERror struct { + Context jsonld.Context `json:"@context,omitempty"` + Type string `json:"@type,omitempty" validate:"required,eq=dspace:ContractNegotiation"` + ProviderPID string `json:"dspace:providerPid,omitempty" validate:"required"` + ConsumerPID string `json:"dspace:consumerPid,omitempty" validate:"required"` + Code string `json:"dspace:code,omitempty"` + Reason []map[string]any `json:"dspace:reason,omitempty"` + Description []Multilanguage `json:"dct:description,omitempty"` +} diff --git a/dsp/transfer_types.go b/dsp/transfer_types.go new file mode 100644 index 0000000..e8e69e0 --- /dev/null +++ b/dsp/transfer_types.go @@ -0,0 +1,99 @@ +// Copyright 2024 go-dataspace +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dsp + +import "github.com/go-dataspace/run-dsp/jsonld" + +// TransferRequestMessage requests a data transfer. +type TransferRequestMessage struct { + Context jsonld.Context `json:"@context,omitempty"` + Type string `json:"@type,omitempty" validate:"required,eq=dspace:TransferRequestMessage"` + AgreementID string `json:"dspace:agreementID" validate:"required"` + Format string `json:"dct:format" validate:"required"` + DataAddress DataAddress `json:"dspace:dataAddress"` + CallbackAddress string `json:"dspace:callbackAddress" validate:"required"` + ConsumerPID string `json:"dspace:consumerPid" validate:"required"` +} + +// TransferStartMessage signals a transfer start. +type TransferStartMessage struct { + Context jsonld.Context `json:"@context,omitempty"` + Type string `json:"@type,omitempty" validate:"required,eq=dspace:TransferStartMessage"` + ProviderPID string `json:"dspace:providerPid" validate:"required"` + ConsumerPID string `json:"dspace:consumerPid" validate:"required"` + DataAddress DataAddress `json:"dspace:dataAddress"` +} + +// TransferSuspensionMessage signals the suspension of a datatransfer. +type TransferSuspensionMessage struct { + Context jsonld.Context `json:"@context,omitempty"` + Type string `json:"@type,omitempty" validate:"required,eq=dspace:TransferSuspensionMessage"` + ProviderPID string `json:"dspace:providerPid,omitempty" validate:"required"` + ConsumerPID string `json:"dspace:consumerPid,omitempty" validate:"required"` + Code string `json:"code,omitempty"` + Reason []map[string]any `json:"reason,omitempty"` +} + +// TransferCompletionMessage signals the completion of a datatransfer. +type TransferCompletionMessage struct { + Context jsonld.Context `json:"@context,omitempty"` + Type string `json:"@type,omitempty" validate:"required,eq=dspace:TransferCompletionMessage"` + ProviderPID string `json:"dspace:providerPid,omitempty" validate:"required"` + ConsumerPID string `json:"dspace:consumerPid,omitempty" validate:"required"` +} + +// TransferTerminationMessage signals the suspension of a datatransfer. +type TransferTerminationMessage struct { + Context jsonld.Context `json:"@context,omitempty"` + Type string `json:"@type,omitempty" validate:"required,eq=dspace:TransferTerminationMessage"` + ProviderPID string `json:"dspace:providerPid,omitempty" validate:"required"` + ConsumerPID string `json:"dspace:consumerPid,omitempty" validate:"required"` + Code string `json:"code,omitempty"` + Reason []map[string]any `json:"reason,omitempty"` +} + +// TransferProcess are state change reponses. +type TransferProcess struct { + Context jsonld.Context `json:"@context,omitempty"` + Type string `json:"@type,omitempty" validate:"required,eq=dspace:TransferProcess"` + ProviderPID string `json:"dspace:providerPid,omitempty" validate:"required"` + ConsumerPID string `json:"dspace:consumerPid,omitempty" validate:"required"` + State string `json:"dspace:state" validate:"required,transfer_state"` +} + +// TransferError signals the suspension of a datatransfer. +type TransferError struct { + Context jsonld.Context `json:"@context,omitempty"` + Type string `json:"@type,omitempty" validate:"required,eq=dspace:TransferError"` + ProviderPID string `json:"dspace:providerPid,omitempty" validate:"required"` + ConsumerPID string `json:"dspace:consumerPid,omitempty" validate:"required"` + Code string `json:"code,omitempty"` + Reason []map[string]any `json:"reason,omitempty"` +} + +// DataAddress represents a dataspace data address. +type DataAddress struct { + Type string `json:"@type,omitempty" validate:"required,eq=dspace:DataAddress"` + EndpointType string `json:"endpointType" validate:"required"` + Endpoint string `json:"endpoint" validate:"required"` + EndpointProperties []EndpointProperty `json:"endpointProperties"` +} + +// EndpointProperty represents endpoint properties. +type EndpointProperty struct { + Type string `json:"@type,omitempty" validate:"required,eq=dspace:EndpointProperty"` + Name string `json:"dspace:name" validate:"required"` + Value string `json:"dspace:value" validate:"required"` +} diff --git a/dsp/validators.go b/dsp/validators.go new file mode 100644 index 0000000..ffb567f --- /dev/null +++ b/dsp/validators.go @@ -0,0 +1,58 @@ +// Copyright 2024 go-dataspace +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dsp + +import ( + "slices" + + "github.com/go-dataspace/run-dsp/odrl" + "github.com/go-playground/validator/v10" +) + +func transferProcessState(fl validator.FieldLevel) bool { + states := []string{ + "dspace:REQUESTED", + "dspace:STARTED", + "dspace:TERMINATED", + "dspace:COMPLETED", + "dspace:SUSPENDED", + } + return slices.Contains(states, fl.Field().String()) +} + +func contractNegotiationState(fl validator.FieldLevel) bool { + states := []string{ + "dspace:REQUESTED", + "dspace:OFFERED", + "dspace:ACCEPTED", + "dspace:AGREED", + "dspace:VERIFIED", + "dspace:FINALIZED", + "dspace:TERMINATED", + } + return slices.Contains(states, fl.Field().String()) +} + +// This registers all the validators of this package, and also calls the odrl register function +// as this package uses the odrl structs as well. +func RegisterValidators(v *validator.Validate) error { + if err := v.RegisterValidation("transfer_state", transferProcessState); err != nil { + return err + } + if err := v.RegisterValidation("contract_state", contractNegotiationState); err != nil { + return err + } + return odrl.RegisterValidators(v) +} diff --git a/go.mod b/go.mod index 4b6845e..38219ee 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,20 @@ module github.com/go-dataspace/run-dsp -go 1.22.3 +go 1.22 require ( github.com/alecthomas/kong v0.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.21.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/samber/slog-http v1.3.1 // indirect go.opentelemetry.io/otel v1.19.0 // indirect go.opentelemetry.io/otel/trace v1.19.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index a830c40..ab4e9a7 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,28 @@ github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA= github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.21.0 h1:4fZA11ovvtkdgaeev9RGWPgc1uj3H8W+rNYyH/ySBb0= +github.com/go-playground/validator/v10 v10.21.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/samber/slog-http v1.3.1 h1:Fho8CGX4elTKAXFKCNGloRAz2yWt1WD+vXpO9iylQ9g= github.com/samber/slog-http v1.3.1/go.mod h1:n6h4x2ZBeTgLqMKf95EuNlU6mcJF1b/RVLxo1od5+V0= go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/jsonld/context.go b/jsonld/context.go new file mode 100644 index 0000000..8f02e35 --- /dev/null +++ b/jsonld/context.go @@ -0,0 +1,143 @@ +// Copyright 2024 go-dataspace +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package jsonld contains utility types and functions to handle JSON-LD files. +package jsonld + +import ( + "encoding/json" + "fmt" +) + +// ContextEntry is a JSON-LD context entry. +type ContextEntry struct { + ID string `json:"@id,omitempty"` + Type string `json:"@type,omitempty"` +} + +// UnmarshalJSON unmarshals a context entry. It first tries to unmarshal an entry as an object, +// and if that fails, it will try to unmarshal it as a string. If that succeeds, it will +// assign that string to the `ID` field. +func (ce *ContextEntry) UnmarshalJSON(data []byte) error { + var entry struct { + ID string `json:"@id,omitempty"` + Type string `json:"@type,omitempty"` + } + if err := json.Unmarshal(data, &entry); err == nil { + ce.ID = entry.ID + ce.Type = entry.Type + return nil + } + + var id string + if err := json.Unmarshal(data, &id); err == nil { + ce.ID = id + return nil + } + return fmt.Errorf("Couldn't unmarshal ContextEntry: %s", data) +} + +// MarshalJSON will marshal the ContextEntry as an object if Type is not empty, and +// as a string containing just the ID if Type is empty. +func (ce *ContextEntry) MarshalJSON() ([]byte, error) { + if ce.Type != "" { + return json.Marshal(struct { + ID string `json:"@id,omitempty"` + Type string `json:"@type,omitempty"` + }{ + ID: ce.ID, + Type: ce.Type, + }) + } + + return json.Marshal(ce.ID) +} + +// Context is a JSON-LD @context entry. In JSON-LD this can be a string, a list of strings, a map +// containing either strings or objects. +type Context struct { + rootContexts []ContextEntry + namedContexts map[string]ContextEntry +} + +// UnmarshalJSON will first try to unmarshal the context as a map of ContextEntry structs, +// if that fails, it will try to unmarshal it as a list of strings, and if that fails, as +// a single string. +func (c *Context) UnmarshalJSON(data []byte) error { + var nc map[string]ContextEntry + if err := json.Unmarshal(data, &nc); err == nil { + c.namedContexts = nc + return nil + } + rootContexts := make([]ContextEntry, 0) + var lc []string + if err := json.Unmarshal(data, &lc); err == nil { + for _, id := range lc { + rootContexts = append(rootContexts, ContextEntry{ID: id}) + } + c.rootContexts = rootContexts + return nil + } + var sc string + if err := json.Unmarshal(data, &sc); err == nil { + rootContexts = append(rootContexts, ContextEntry{ID: sc}) + c.rootContexts = rootContexts + return nil + } + return fmt.Errorf("Couldn't unmarshal Context: %s", data) +} + +// MarshalJSON will first check if namedContext are available, and marshal those if they are. +// If they are not, it will check if there's only a single rootContext and marshal that as a +// string, else it will marshal the entire rootContexts list. +func (c *Context) MarshalJSON() ([]byte, error) { + if len(c.namedContexts) == 0 { + return json.Marshal(c.namedContexts) + } + if len(c.rootContexts) == 1 { + return json.Marshal(c.rootContexts[0]) + } + return json.Marshal(c.rootContexts) +} + +// GetContextsFor looks up the namespace/element by name and return the relevant contexts for it. +// If there are any named contexts, it returns those, else it returns the root contexts. +func (c *Context) GetContextsFor(ns string) []ContextEntry { + if context, found := c.namedContexts[ns]; found { + return []ContextEntry{context} + } + return c.rootContexts +} + +// GetRootContexts returns the root contexts, this can be empty if there are named contexts. +func (c *Context) GetRootContexts() []ContextEntry { + return c.rootContexts +} + +// NewRootContext creates a new context from the given list. Do note that any `Type` properties +// will be lost. +func NewRootContext(c []ContextEntry) Context { + return Context{ + rootContexts: c, + namedContexts: map[string]ContextEntry{}, + } +} + +// NewNamedContext creates a new context from the given map. +func NewNamedContext(c map[string]ContextEntry) Context { + return Context{ + rootContexts: make([]ContextEntry, 0), + namedContexts: c, + } +} diff --git a/odrl/types.go b/odrl/types.go new file mode 100644 index 0000000..21701c4 --- /dev/null +++ b/odrl/types.go @@ -0,0 +1,86 @@ +// Copyright 2024 go-dataspace +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package odrl contains ODRL code +package odrl + +import "time" + +//nolint:lll +// This is for now a partial port of this JSON schema: +// https://international-data-spaces-association.github.io/ids-specification/2024-1/negotiation/message/schema/contract-schema.json + +// Offer is an ODRL offer. +type Offer struct { + MessageOffer +} + +// MessageOffer is an ODRL MessageOffer. +type MessageOffer struct { + PolicyClass + Type string `json:"@type" validate:"required,eq=odrl:Offer"` +} + +// PolicyClass is an ODRL PolicyClass. +type PolicyClass struct { + AbstractPolicyRule + ID string `json:"@id" validate:"required"` + Profile []Reference `json:"odrl:profile,omitempty" validate:"dive"` + Permission []Permission `json:"odrl:permission,omitempty" validate:"gte=1,dive"` + Obligation []Duty `json:"odrl:obligation,omitempty" validate:"gte=1,dive"` +} + +// AbstractPolicyRule defines an ODRL abstract policy rule. +type AbstractPolicyRule struct { + Assigner string `json:"assigner,omitempty"` + Assignee string `json:"assignee,omitempty"` +} + +// Reference is a reference. +type Reference struct { + ID string `json:"@id" validate:"required"` +} + +// Permission is a permisson entry. +type Permission struct { + AbstractPolicyRule + Action string `json:"action" validate:"required,odrl_action"` + Constraint []Constraint `json:"constraint,omitempty" validate:"gte=1,dive"` + Duty Duty `json:"duty,omitempty" validate:"dive"` +} + +// Duty is an ODRL duty. +type Duty struct { + AbstractPolicyRule + ID string `json:"@id"` + Action string `json:"action" validate:"required,odrl_action"` + Constraint []Constraint `json:"constraint,omitempty" validate:"gte=1,dive"` +} + +// Constraint is an ODRL constraint. +type Constraint struct { + RightOperand map[string]any `json:"odrl:rightOperand"` + RightOperandReference Reference `json:"odrl:rightOperandReference"` + LeftOperand string `json:"odrl:leftOperand" validate:"odrl_leftoperand"` + Operator string `json:"odrl:operator" validate:"odrl_operator"` // TODO: implment custom verifier. +} + +// Agreement is an ODRL agreement. +type Agreement struct { + PolicyClass + Type string `json:"@type" validate:"required,eq=odrl:Agreement"` + ID string `json:"@id" validate:"required"` + Target string `json:"@target" validate:"required"` + Timestamp time.Time `json:"dspace:timestamp"` +} diff --git a/odrl/validators.go b/odrl/validators.go new file mode 100644 index 0000000..f532f7d --- /dev/null +++ b/odrl/validators.go @@ -0,0 +1,149 @@ +// Copyright 2024 go-dataspace +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package odrl + +import ( + "slices" + + "github.com/go-playground/validator/v10" +) + +func action(fl validator.FieldLevel) bool { + states := []string{ + "odrl:delete", + "odrl:execute", + "cc:SourceCode", + "odrl:anonymize", + "odrl:extract", + "odrl:read", + "odrl:index", + "odrl:compensate", + "odrl:sell", + "odrl:derive", + "odrl:ensureExclusivity", + "odrl:annotate", + "cc:Reproduction", + "odrl:translate", + "odrl:include", + "cc:DerivativeWorks", + "cc:Distribution", + "odrl:textToSpeech", + "odrl:inform", + "odrl:grantUse", + "odrl:archive", + "odrl:modify", + "odrl:aggregate", + "odrl:attribute", + "odrl:nextPolicy", + "odrl:digitize", + "cc:Attribution", + "odrl:install", + "odrl:concurrentUse", + "odrl:distribute", + "odrl:synchronize", + "odrl:move", + "odrl:obtainConsent", + "odrl:print", + "cc:Notice", + "odrl:give", + "odrl:uninstall", + "cc:Sharing", + "odrl:reviewPolicy", + "odrl:watermark", + "odrl:play", + "odrl:reproduce", + "odrl:transform", + "odrl:display", + "odrl:stream", + "cc:ShareAlike", + "odrl:acceptTracking", + "cc:CommericalUse", + "odrl:present", + "odrl:use", + } + return slices.Contains(states, fl.Field().String()) +} + +func leftOperand(fl validator.FieldLevel) bool { + states := []string{ + "odrl:absolutePosition", + "odrl:absoluteSize", + "odrl:absoluteSpatialPosition", + "odrl:absoluteTemporalPosition", + "odrl:count", + "odrl:dateTime", + "odrl:delayPeriod", + "odrl:deliveryChannel", + "odrl:device", + "odrl:elapsedTime", + "odrl:event", + "odrl:fileFormat", + "odrl:industry", + "odrl:language", + "odrl:media", + "odrl:meteredTime", + "odrl:payAmount", + "odrl:percentage", + "odrl:product", + "odrl:purpose", + "odrl:recipient", + "odrl:relativePosition", + "odrl:relativeSize", + "odrl:relativeSpatialPosition", + "odrl:relativeTemporalPosition", + "odrl:resolution", + "odrl:spatial", + "odrl:spatialCoordinates", + "odrl:system", + "odrl:systemDevice", + "odrl:timeInterval", + "odrl:unitOfCount", + "odrl:version", + "odrl:virtualLocation", + } + return slices.Contains(states, fl.Field().String()) +} + +func operator(fl validator.FieldLevel) bool { + states := []string{ + "odrl:eq", + "odrl:gt", + "odrl:gteq", + "odrl:hasPart", + "odrl:isA", + "odrl:isAllOf", + "odrl:isAnyOf", + "odrl:isNoneOf", + "odrl:isPartOf", + "odrl:lt", + "odrl:term-lteq", + "odrl:neq", + } + return slices.Contains(states, fl.Field().String()) +} + +// This registers all the validators of this package. +func RegisterValidators(v *validator.Validate) error { + if err := v.RegisterValidation("odrl_action", action); err != nil { + return err + } + if err := v.RegisterValidation("odrl_leftoperand", leftOperand); err != nil { + return err + } + if err := v.RegisterValidation("odrl_operator", operator); err != nil { + return err + } + return nil +}