Files
did-it/service.go

156 lines
3.4 KiB
Go
Raw Permalink Normal View History

package did
import (
"encoding/json"
"fmt"
)
// Specification: https://www.w3.org/TR/cid-1.0/#services
// List of service types and their fields: https://www.w3.org/TR/did-extensions-properties/#service-types
// Services is a collection of Service.
type Services []Service
// ServiceById retrieves a Service from the Services slice by its id.
// Returns the Service and true if found, otherwise returns an empty Service and false.
func (ss Services) ServiceById(id string) (Service, bool) {
for _, s := range ss {
if s.Id == id {
return s, true
}
}
return Service{}, false
}
// ServiceByType returns zero or one Service matching the given type.
// If there is more than one service for that type, the first match is returned.
func (ss Services) ServiceByType(_type string) (Service, bool) {
for _, s := range ss {
if s.HasType(_type) {
return s, true
}
}
return Service{}, false
}
// Service is a means of communicating or interacting with the DID subject or associated entities
// via one or more service endpoints.
// It can have one or more types.
type Service struct {
Id string
Types []string
Endpoints []any // either strEndpoint or mapEndpoint
}
func (s Service) HasType(_type string) bool {
for _, t := range s.Types {
if t == _type {
return true
}
}
return false
}
func (s Service) MarshalJSON() ([]byte, error) {
var aux struct {
Id string `json:"id"`
Type any `json:"type"`
Endpoint any `json:"serviceEndpoint"`
}
aux.Id = s.Id
switch len(s.Types) {
case 0:
return nil, fmt.Errorf("service type is required")
case 1:
aux.Type = s.Types[0]
default:
aux.Type = s.Types
}
switch len(s.Endpoints) {
case 0:
return nil, fmt.Errorf("service endpoint is required")
case 1:
aux.Endpoint = s.Endpoints[0]
default:
aux.Endpoint = s.Endpoints
}
return json.Marshal(aux)
}
func (s *Service) UnmarshalJSON(bytes []byte) error {
var aux struct {
Id string `json:"id"`
Type json.RawMessage `json:"type"`
Endpoint json.RawMessage `json:"serviceEndpoint"`
}
err := json.Unmarshal(bytes, &aux)
if err != nil {
return err
}
if len(aux.Id) == 0 {
return fmt.Errorf("service id is required")
}
s.Id = aux.Id
s.Types, err = unmarshalSingleOrArray[string](aux.Type)
if err != nil {
return err
}
if len(s.Types) == 0 {
return fmt.Errorf("service type is required")
}
for _, _type := range s.Types {
if len(_type) == 0 {
return fmt.Errorf("invalid service type: must not be empty string")
}
}
s.Endpoints, err = unmarshalSingleOrArray[any](aux.Endpoint)
if err != nil {
return err
}
if len(s.Endpoints) == 0 {
return fmt.Errorf("service endpoint is required")
}
for i, endpoint := range s.Endpoints {
switch endpoint := endpoint.(type) {
case string:
s.Endpoints[i] = StrEndpoint(endpoint)
case map[string]any:
s.Endpoints[i] = MapEndpoint(endpoint)
default:
return fmt.Errorf("endpoint must be %T or %T", StrEndpoint(""), MapEndpoint{})
}
}
return nil
}
type StrEndpoint string
type MapEndpoint map[string]any
func unmarshalSingleOrArray[T any](data json.RawMessage) ([]T, error) {
if data == nil {
return nil, nil
}
var single T
if err := json.Unmarshal(data, &single); err == nil {
return []T{single}, nil
}
var array []T
if err := json.Unmarshal(data, &array); err == nil {
return array, nil
}
return nil, fmt.Errorf("must be %T or array of %T", single, single)
}