From 8c53ed7984792126d3cbb4f802080abaf1f014be Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Fri, 2 Aug 2024 16:11:18 -0400 Subject: [PATCH 1/2] feat: initial golang support Co-authored-by: Nisha Kumar Signed-off-by: Keith Zantow --- src/shacl2code/lang/__init__.py | 1 + src/shacl2code/lang/common.py | 3 +- .../lang/go_runtime/graph_builder.go | 314 +++++++++++ src/shacl2code/lang/go_runtime/ld_context.go | 525 ++++++++++++++++++ .../lang/go_runtime/runtime_test.go | 325 +++++++++++ .../lang/go_runtime/superclass_view.go | 62 +++ src/shacl2code/lang/golang.py | 321 +++++++++++ src/shacl2code/lang/templates/golang.j2 | 151 +++++ 8 files changed, 1701 insertions(+), 1 deletion(-) create mode 100644 src/shacl2code/lang/go_runtime/graph_builder.go create mode 100644 src/shacl2code/lang/go_runtime/ld_context.go create mode 100644 src/shacl2code/lang/go_runtime/runtime_test.go create mode 100644 src/shacl2code/lang/go_runtime/superclass_view.go create mode 100644 src/shacl2code/lang/golang.py create mode 100644 src/shacl2code/lang/templates/golang.j2 diff --git a/src/shacl2code/lang/__init__.py b/src/shacl2code/lang/__init__.py index 20d916e..7589513 100644 --- a/src/shacl2code/lang/__init__.py +++ b/src/shacl2code/lang/__init__.py @@ -9,3 +9,4 @@ from .jinja import JinjaRender # noqa: F401 from .python import PythonRender # noqa: F401 from .jsonschema import JsonSchemaRender # noqa: F401 +from .golang import GolangRender # noqa: F401 diff --git a/src/shacl2code/lang/common.py b/src/shacl2code/lang/common.py index eb9a1cc..c022dc0 100644 --- a/src/shacl2code/lang/common.py +++ b/src/shacl2code/lang/common.py @@ -72,10 +72,10 @@ def abort_helper(msg): env.globals["abort"] = abort_helper env.globals["SHACL2CODE"] = SHACL2CODE env.globals["SH"] = SH + template = env.get_template(template.name) render = template.render( - disclaimer=f"This file was automatically generated by {os.path.basename(sys.argv[0])}. DO NOT MANUALLY MODIFY IT", **render_args, ) @@ -135,6 +135,7 @@ def get_all_named_individuals(cls): "abstract_classes": abstract_classes, "enums": enums, "context": model.context, + "disclaimer": f"This file was automatically generated by {os.path.basename(sys.argv[0])}. DO NOT MANUALLY MODIFY IT", **self.get_additional_render_args(), } diff --git a/src/shacl2code/lang/go_runtime/graph_builder.go b/src/shacl2code/lang/go_runtime/graph_builder.go new file mode 100644 index 0000000..273d546 --- /dev/null +++ b/src/shacl2code/lang/go_runtime/graph_builder.go @@ -0,0 +1,314 @@ +package runtime + +import ( + "fmt" + "reflect" + "strings" +) + +type graphBuilder struct { + ldc ldContext + input []any + graph []any + idPrefix string + nextID map[reflect.Type]int + ids map[reflect.Value]string +} + +func (b *graphBuilder) toGraph() []any { + return b.graph +} + +func (b *graphBuilder) add(o any) (context *serializationContext, err error) { + v := reflect.ValueOf(o) + if v.Type().Kind() != reflect.Pointer { + if v.CanAddr() { + v = v.Addr() + } else { + newV := reflect.New(v.Type()) + newV.Elem().Set(v) + v = newV + } + } + val, err := b.toValue(v) + // objects with IDs get added to the graph during object traversal + if _, isTopLevel := val.(map[string]any); isTopLevel && err == nil { + b.graph = append(b.graph, val) + } + ctx := b.findContext(v.Type()) + return ctx, err +} + +func (b *graphBuilder) findContext(t reflect.Type) *serializationContext { + t = baseType(t) // object may be a pointer, but we want the base types + for _, context := range b.ldc { + for _, typ := range context.iriToType { + if t == typ.typ { + return context + } + } + } + return nil +} + +func (b *graphBuilder) toStructMap(v reflect.Value) (value any, err error) { + t := v.Type() + if t.Kind() != reflect.Struct { + return nil, fmt.Errorf("expected struct type, got: %v", stringify(v)) + } + + meta, ok := fieldByType[ldType](t) + if !ok { + return nil, fmt.Errorf("struct does not have LDType metadata: %v", stringify(v)) + } + + iri := meta.Tag.Get(typeIriCompactTag) + if iri == "" { + iri = meta.Tag.Get(typeIriTag) + } + + context := b.findContext(t) + tc := context.typeToContext[t] + + typeProp := ldTypeProp + if context.typeAlias != "" { + typeProp = context.typeAlias + } + out := map[string]any{ + typeProp: iri, + } + + hasValues := false + id := "" + + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if skipField(f) { + continue + } + + prop := f.Tag.Get(propIriCompactTag) + if prop == "" { + prop = f.Tag.Get(propIriTag) + } + + fieldV := v.Field(i) + + if !isRequired(f) && isEmpty(fieldV) { + continue + } + + val, err := b.toValue(fieldV) + if err != nil { + return nil, err + } + + if isIdField(f) { + id, _ = val.(string) + if id == "" { + // if this struct does not have an ID set, and does not have multiple references, + // it is output inline, it does not need an ID, but does need an ID + // when it is moved to the top-level graph and referenced elsewhere + if !b.hasMultipleReferences(v.Addr()) { + continue + } + val, _ = b.ensureID(v.Addr()) + } else if tc != nil { + // compact named IRIs + if _, ok := tc.iriToName[id]; ok { + id = tc.iriToName[id] + } + } + } else { + hasValues = true + } + + out[prop] = val + } + + if id != "" && !hasValues { + // if we _only_ have an ID set and no other values, consider this a named individual + return id, nil + } + + return out, nil +} + +func isIdField(f reflect.StructField) bool { + return f.Tag.Get(propIriTag) == ldIDProp +} + +func isEmpty(v reflect.Value) bool { + return !v.IsValid() || v.IsZero() +} + +func isRequired(f reflect.StructField) bool { + if isIdField(f) { + return true + } + required := f.Tag.Get(propIsRequiredTag) + return required != "" && !strings.EqualFold(required, "false") +} + +func (b *graphBuilder) toValue(v reflect.Value) (any, error) { + if !v.IsValid() { + return nil, nil + } + + switch v.Type().Kind() { + case reflect.Interface: + return b.toValue(v.Elem()) + case reflect.Pointer: + if v.IsNil() { + return nil, nil + } + if !b.hasMultipleReferences(v) { + return b.toValue(v.Elem()) + } + return b.ensureID(v) + case reflect.Struct: + return b.toStructMap(v) + case reflect.Slice: + var out []any + for i := 0; i < v.Len(); i++ { + val, err := b.toValue(v.Index(i)) + if err != nil { + return nil, err + } + out = append(out, val) + } + return out, nil + case reflect.String: + return v.String(), nil + default: + if v.CanInterface() { + return v.Interface(), nil + } + return nil, fmt.Errorf("unable to convert value to maps: %v", stringify(v)) + } +} + +func (b *graphBuilder) ensureID(ptr reflect.Value) (string, error) { + if ptr.Type().Kind() != reflect.Pointer { + return "", fmt.Errorf("expected pointer, got: %v", stringify(ptr)) + } + if id, ok := b.ids[ptr]; ok { + return id, nil + } + + v := ptr.Elem() + t := v.Type() + + id, err := b.getID(v) + if err != nil { + return "", err + } + if id == "" { + if b.nextID == nil { + b.nextID = map[reflect.Type]int{} + } + nextID := b.nextID[t] + 1 + b.nextID[t] = nextID + id = fmt.Sprintf("_:%s-%v", t.Name(), nextID) + } + b.ids[ptr] = id + val, err := b.toValue(v) + if err != nil { + return "", err + } + b.graph = append(b.graph, val) + return id, nil +} + +func (b *graphBuilder) getID(v reflect.Value) (string, error) { + t := v.Type() + if t.Kind() != reflect.Struct { + return "", fmt.Errorf("expected struct, got: %v", stringify(v)) + } + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if isIdField(f) { + fv := v.Field(i) + if f.Type.Kind() != reflect.String { + return "", fmt.Errorf("invalid type for ID field %v in: %v", f, stringify(v)) + } + return fv.String(), nil + } + } + return "", nil +} + +// hasMultipleReferences returns true if the ptr value has multiple references in the input slice +func (b *graphBuilder) hasMultipleReferences(ptr reflect.Value) bool { + if !ptr.IsValid() { + return false + } + count := 0 + visited := map[reflect.Value]struct{}{} + for _, v := range b.input { + count += refCountR(ptr, visited, reflect.ValueOf(v)) + if count > 1 { + return true + } + } + return false +} + +// refCount returns the reference count of the value in the container object +func refCount(find any, container any) int { + visited := map[reflect.Value]struct{}{} + ptrV := reflect.ValueOf(find) + if !ptrV.IsValid() { + return 0 + } + return refCountR(ptrV, visited, reflect.ValueOf(container)) +} + +// refCountR recursively searches for the value, find, in the value v +func refCountR(find reflect.Value, visited map[reflect.Value]struct{}, v reflect.Value) int { + if find.Equal(v) { + return 1 + } + if !v.IsValid() { + return 0 + } + if _, ok := visited[v]; ok { + return 0 + } + visited[v] = struct{}{} + switch v.Kind() { + case reflect.Interface: + return refCountR(find, visited, v.Elem()) + case reflect.Pointer: + if v.IsNil() { + return 0 + } + return refCountR(find, visited, v.Elem()) + case reflect.Struct: + count := 0 + for i := 0; i < v.NumField(); i++ { + count += refCountR(find, visited, v.Field(i)) + } + return count + case reflect.Slice: + count := 0 + for i := 0; i < v.Len(); i++ { + count += refCountR(find, visited, v.Index(i)) + } + return count + default: + return 0 + } +} + +func stringify(o any) string { + if v, ok := o.(reflect.Value); ok { + if !v.IsValid() { + return "invalid value" + } + if !v.IsZero() && v.CanInterface() { + o = v.Interface() + } + } + return fmt.Sprintf("%#v", o) +} diff --git a/src/shacl2code/lang/go_runtime/ld_context.go b/src/shacl2code/lang/go_runtime/ld_context.go new file mode 100644 index 0000000..c3fa721 --- /dev/null +++ b/src/shacl2code/lang/go_runtime/ld_context.go @@ -0,0 +1,525 @@ +package runtime + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "path" + "reflect" + "strings" +) + +// ldType is a 0-size data holder property type for type-level linked data +type ldType struct{} + +// ldContext is the holder for all known LD contexts and required definitions +type ldContext map[string]*serializationContext + +// RegisterTypes registers types to be used when serializing/deserialising documents +func (c ldContext) RegisterTypes(contextUrl string, types ...any) ldContext { + ctx := c[contextUrl] + if ctx == nil { + ctx = &serializationContext{ + contextUrl: contextUrl, + typeAlias: "type", // FIXME this needs to come from the LD context + iriToType: map[string]*typeContext{}, + typeToContext: map[reflect.Type]*typeContext{}, + } + c[contextUrl] = ctx + } + for _, typ := range types { + ctx.registerType(typ) + } + return c +} + +// IRIMap registers compact IRIs for the given type +func (c ldContext) IRIMap(contextUrl string, typ any, nameMap map[string]string) ldContext { + c.RegisterTypes(contextUrl) // ensure there is a context created + ctx := c[contextUrl] + + t := reflect.TypeOf(typ) + t = baseType(t) // types should be passed as pointers; we want the base types + tc := ctx.typeToContext[t] + if tc == nil { + ctx.registerType(typ) + tc = ctx.typeToContext[t] + } + for iri, compact := range nameMap { + tc.iriToName[iri] = compact + tc.nameToIri[compact] = iri + } + return c +} + +func (c ldContext) ToJSON(writer io.Writer, value any) error { + vals, err := c.toMaps(value) + if err != nil { + return err + } + enc := json.NewEncoder(writer) + enc.SetEscapeHTML(false) + return enc.Encode(vals) +} + +func (c ldContext) toMaps(o ...any) (values map[string]any, errors error) { + // the ld graph is referenced here + // traverse the go objects to output to the graph + builder := graphBuilder{ + ldc: c, + input: o, + ids: map[reflect.Value]string{}, + } + + var err error + var context *serializationContext + for _, o := range builder.input { + context, err = builder.add(o) + if err != nil { + return nil, err + } + } + + return map[string]any{ + ldContextProp: context.contextUrl, + ldGraphProp: builder.toGraph(), + }, nil +} + +func (c ldContext) FromJSON(reader io.Reader) ([]any, error) { + vals := map[string]any{} + dec := json.NewDecoder(reader) + err := dec.Decode(&vals) + if err != nil { + return nil, err + } + return c.FromMaps(vals) +} + +func (c ldContext) FromMaps(values map[string]any) ([]any, error) { + instances := map[string]reflect.Value{} + + var errs error + var graph []any + + context, _ := values[ldContextProp].(string) + currentContext := c[context] + if currentContext == nil { + return nil, fmt.Errorf("unknown document %s type: %v", ldContextProp, context) + } + + nodes, _ := values[ldGraphProp].([]any) + if nodes == nil { + return nil, fmt.Errorf("%s array not present in root object", ldGraphProp) + } + + // one pass to create all the instances + for _, node := range nodes { + _, err := c.getOrCreateInstance(currentContext, instances, anyType, node) + errs = appendErr(errs, err) + } + + // second pass to fill in all refs + for _, node := range nodes { + got, err := c.getOrCreateInstance(currentContext, instances, anyType, node) + errs = appendErr(errs, err) + if err == nil && got.IsValid() { + graph = append(graph, got.Interface()) + } + } + + return graph, errs +} + +func (c ldContext) getOrCreateInstance(currentContext *serializationContext, instances map[string]reflect.Value, expectedType reflect.Type, incoming any) (reflect.Value, error) { + if isPrimitive(expectedType) { + if convertedVal := convertTo(incoming, expectedType); convertedVal != emptyValue { + return convertedVal, nil + } + return emptyValue, fmt.Errorf("unable to convert incoming value to type %v: %+v", typeName(expectedType), incoming) + } + switch incoming := incoming.(type) { + case string: + instance := c.findById(currentContext, instances, incoming) + if instance != emptyValue { + return instance, nil + } + // not found: have a complex type with string indicates an IRI or other primitive + switch expectedType.Kind() { + case reflect.Pointer: + expectedType = expectedType.Elem() + if isPrimitive(expectedType) { + val, err := c.getOrCreateInstance(currentContext, instances, expectedType, incoming) + if err != nil { + return emptyValue, err + } + instance = reflect.New(expectedType) + instance.Elem().Set(val) + return instance, nil + } + if expectedType.Kind() == reflect.Struct { + return emptyValue, fmt.Errorf("unexpected pointer reference external IRI reference: %v", incoming) + } + fallthrough + case reflect.Struct: + instance = reflect.New(expectedType) + instance = instance.Elem() + err := c.setStructProps(currentContext, instances, instance, map[string]any{ + ldIDProp: incoming, + }) + return instance, err + case reflect.Interface: + return emptyValue, fmt.Errorf("unable to determine appropriate type for external IRI reference: %v", incoming) + default: + } + case map[string]any: + return c.getOrCreateFromMap(currentContext, instances, incoming) + } + return emptyValue, fmt.Errorf("unexpected data type: %#v", incoming) +} + +func convertTo(incoming any, typ reflect.Type) reflect.Value { + v := reflect.ValueOf(incoming) + if v.CanConvert(typ) { + return v.Convert(typ) + } + return emptyValue +} + +func (c ldContext) findById(_ *serializationContext, instances map[string]reflect.Value, incoming string) reflect.Value { + inst, ok := instances[incoming] + if ok { + return inst + } + return emptyValue +} + +func (c ldContext) getOrCreateFromMap(currentContext *serializationContext, instances map[string]reflect.Value, incoming map[string]any) (reflect.Value, error) { + typ, ok := incoming[ldTypeProp].(string) + if !ok && currentContext.typeAlias != "" { + typ, ok = incoming[currentContext.typeAlias].(string) + } + if !ok { + return emptyValue, fmt.Errorf("not a string") + } + + t, ok := currentContext.iriToType[typ] + if !ok { + return emptyValue, fmt.Errorf("don't have type: %v", typ) + } + + id, _ := incoming[ldIDProp].(string) + if id == "" && t.idProp != "" { + id, _ = incoming[t.idProp].(string) + } + inst, ok := instances[id] + if !ok { + inst = reflect.New(baseType(t.typ)) // New(T) returns *T + if id != "" { + // only set instance references when an ID is provided + instances[id] = inst + } + } + + // valid type, make a new one and fill it from the incoming maps + return inst, c.fill(currentContext, instances, inst, incoming) +} + +func (c ldContext) fill(currentContext *serializationContext, instances map[string]reflect.Value, instance reflect.Value, incoming any) error { + switch incoming := incoming.(type) { + case string: + inst := c.findById(currentContext, instances, incoming) + if inst != emptyValue { + return c.setValue(currentContext, instances, instance, inst) + } + // should be an incoming ID if string + return c.setValue(currentContext, instances, instance, map[string]any{ + ldIDProp: incoming, + }) + case map[string]any: + return c.setStructProps(currentContext, instances, instance, incoming) + } + return fmt.Errorf("unsupported incoming data type: %#v attempting to set instance: %#v", incoming, instance.Interface()) +} + +func (c ldContext) setValue(currentContext *serializationContext, instances map[string]reflect.Value, target reflect.Value, incoming any) error { + var errs error + typ := target.Type() + switch typ.Kind() { + case reflect.Slice: + switch incoming := incoming.(type) { + case []any: + return c.setSliceValue(currentContext, instances, target, incoming) + } + // try mapping a single value to an incoming slice + return c.setValue(currentContext, instances, target, []any{incoming}) + case reflect.Struct: + switch incoming := incoming.(type) { + case map[string]any: + return c.setStructProps(currentContext, instances, target, incoming) + case string: + // named individuals just need an object with the iri set + return c.setStructProps(currentContext, instances, target, map[string]any{ + ldIDProp: incoming, + }) + } + case reflect.Interface, reflect.Pointer: + switch incoming := incoming.(type) { + case string, map[string]any: + inst, err := c.getOrCreateInstance(currentContext, instances, typ, incoming) + errs = appendErr(errs, err) + if inst != emptyValue { + target.Set(inst) + return nil + } + } + default: + if newVal := convertTo(incoming, typ); newVal != emptyValue { + target.Set(newVal) + } else { + errs = appendErr(errs, fmt.Errorf("unable to convert %#v to %s, dropping", incoming, typeName(typ))) + } + } + return nil +} + +func (c ldContext) setStructProps(currentContext *serializationContext, instances map[string]reflect.Value, instance reflect.Value, incoming map[string]any) error { + var errs error + typ := instance.Type() + for typ.Kind() == reflect.Pointer { + instance = instance.Elem() + typ = instance.Type() + } + if typ.Kind() != reflect.Struct { + return fmt.Errorf("unable to set struct properties on non-struct type: %#v", instance.Interface()) + } + tc := currentContext.typeToContext[typ] + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + if skipField(field) { + continue + } + fieldVal := instance.Field(i) + + propIRI := field.Tag.Get(propIriTag) + if propIRI == "" { + continue + } + incomingVal, ok := incoming[propIRI] + if !ok { + compactIRI := field.Tag.Get(propIriCompactTag) + if compactIRI != "" { + incomingVal, ok = incoming[compactIRI] + } + } + if !ok { + continue + } + // don't set blank node IDs, these will be regenerated on output + if propIRI == ldIDProp { + if tc != nil { + if str, ok := incomingVal.(string); ok { + if fullIRI, ok := tc.nameToIri[str]; ok { + incomingVal = fullIRI + } + } + } + if isBlankNodeID(incomingVal) { + continue + } + } + errs = appendErr(errs, c.setValue(currentContext, instances, fieldVal, incomingVal)) + } + return errs +} + +func (c ldContext) setSliceValue(currentContext *serializationContext, instances map[string]reflect.Value, target reflect.Value, incoming []any) error { + var errs error + sliceType := target.Type() + if sliceType.Kind() != reflect.Slice { + return fmt.Errorf("expected slice, got: %#v", target) + } + sz := len(incoming) + if sz > 0 { + elemType := sliceType.Elem() + newSlice := reflect.MakeSlice(sliceType, 0, sz) + for i := 0; i < sz; i++ { + incomingValue := incoming[i] + if incomingValue == nil { + continue // don't allow null values + } + newItemValue, err := c.getOrCreateInstance(currentContext, instances, elemType, incomingValue) + errs = appendErr(errs, err) + if newItemValue != emptyValue { + // validate we can actually set the type + if newItemValue.Type().AssignableTo(elemType) { + newSlice = reflect.Append(newSlice, newItemValue) + } + } + } + target.Set(newSlice) + } + return errs +} + +func skipField(field reflect.StructField) bool { + return field.Type.Size() == 0 +} + +func typeName(t reflect.Type) string { + switch { + case isPointer(t): + return "*" + typeName(t.Elem()) + case isSlice(t): + return "[]" + typeName(t.Elem()) + case isMap(t): + return "map[" + typeName(t.Key()) + "]" + typeName(t.Elem()) + case isPrimitive(t): + return t.Name() + } + return path.Base(t.PkgPath()) + "." + t.Name() +} + +func isSlice(t reflect.Type) bool { + return t.Kind() == reflect.Slice +} + +func isMap(t reflect.Type) bool { + return t.Kind() == reflect.Map +} + +func isPointer(t reflect.Type) bool { + return t.Kind() == reflect.Pointer +} + +func isPrimitive(t reflect.Type) bool { + switch t.Kind() { + case reflect.String, + reflect.Int, + reflect.Int8, + reflect.Int16, + reflect.Int32, + reflect.Int64, + reflect.Uint, + reflect.Uint8, + reflect.Uint16, + reflect.Uint32, + reflect.Uint64, + reflect.Float32, + reflect.Float64, + reflect.Bool: + return true + default: + return false + } +} + +const ( + ldIDProp = "@id" + ldTypeProp = "@type" + ldContextProp = "@context" + ldGraphProp = "@graph" + typeIriTag = "iri" + typeIriCompactTag = "iri-compact" + propIriTag = "iri" + propIriCompactTag = "iri-compact" + typeIdPropTag = "id-prop" + propIsRequiredTag = "required" +) + +var ( + emptyValue reflect.Value + anyType = reflect.TypeOf((*any)(nil)).Elem() +) + +type typeContext struct { + typ reflect.Type + iri string + compact string + idProp string + iriToName map[string]string + nameToIri map[string]string +} + +type serializationContext struct { + contextUrl string + typeAlias string + iriToType map[string]*typeContext + typeToContext map[reflect.Type]*typeContext +} + +func fieldByType[T any](t reflect.Type) (reflect.StructField, bool) { + var v T + typ := reflect.TypeOf(v) + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if f.Type == typ { + return f, true + } + } + return reflect.StructField{}, false +} + +func (m *serializationContext) registerType(instancePointer any) { + t := reflect.TypeOf(instancePointer) + t = baseType(t) // types should be passed as pointers; we want the base types + + tc := m.typeToContext[t] + if tc != nil { + return // already registered + } + tc = &typeContext{ + typ: t, + iriToName: map[string]string{}, + nameToIri: map[string]string{}, + } + meta, ok := fieldByType[ldType](t) + if ok { + tc.iri = meta.Tag.Get(typeIriTag) + tc.compact = meta.Tag.Get(typeIriCompactTag) + tc.idProp = meta.Tag.Get(typeIdPropTag) + } + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if !isIdField(f) { + continue + } + compactIdProp := f.Tag.Get(typeIriCompactTag) + if compactIdProp != "" { + tc.idProp = compactIdProp + } + } + m.iriToType[tc.iri] = tc + m.iriToType[tc.compact] = tc + m.typeToContext[t] = tc +} + +// appendErr appends errors, flattening joined errors +func appendErr(err error, errs ...error) error { + if joined, ok := err.(interface{ Unwrap() []error }); ok { + return errors.Join(append(joined.Unwrap(), errs...)...) + } + if err == nil { + return errors.Join(errs...) + } + return errors.Join(append([]error{err}, errs...)...) +} + +// baseType returns the base type if this is a pointer or interface +func baseType(t reflect.Type) reflect.Type { + switch t.Kind() { + case reflect.Pointer, reflect.Interface: + return baseType(t.Elem()) + default: + return t + } +} + +// isBlankNodeID indicates this is a blank node ID, e.g. _:CreationInfo-1 +func isBlankNodeID(val any) bool { + if val, ok := val.(string); ok { + return strings.HasPrefix(val, "_:") + } + return false +} diff --git a/src/shacl2code/lang/go_runtime/runtime_test.go b/src/shacl2code/lang/go_runtime/runtime_test.go new file mode 100644 index 0000000..2c83224 --- /dev/null +++ b/src/shacl2code/lang/go_runtime/runtime_test.go @@ -0,0 +1,325 @@ +package runtime + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + "testing" + + "github.com/pmezard/go-difflib/difflib" +) + +/* + SPDX compatible definitions for this test can be generated using something like: + + .venv/bin/python -m shacl2code generate -i https://spdx.org/rdf/3.0.0/spdx-model.ttl -i https://spdx.org/rdf/3.0.0/spdx-json-serialize-annotations.ttl -x https://spdx.org/rdf/3.0.0/spdx-context.jsonld golang --package runtime --output src/shacl2code/lang/go_runtime/generated_code.go --remap-props element=elements,externalIdentifier=externalIdentifiers --include-runtime false +*/ +func Test_spdxExportImportExport(t *testing.T) { + doc := SpdxDocument{ + SpdxId: "old-id", + DataLicense: nil, + Imports: nil, + NamespaceMap: nil, + } + + doc.SetSpdxId("new-id") + + agent := &SoftwareAgent{ + Name: "some-agent", + Summary: "summary", + } + c := &CreationInfo{ + Comment: "some-comment", + Created: "", + CreatedBy: []IAgent{ + agent, + }, + CreatedUsing: []ITool{ + &Tool{ + ExternalIdentifiers: []IExternalIdentifier{ + &ExternalIdentifier{ + ExternalIdentifierType: ExternalIdentifierType_Cpe23, + Identifier: "cpe23:a:myvendor:my-product:*:*:*:*:*:*:*", + }, + }, + Name: "not-tools-golang", + }, + }, + SpecVersion: "", + } + agent.SetCreationInfo(c) + + // add a package + + pkg1 := &Package{ + Name: "some-package-1", + PackageVersion: "1.2.3", + CreationInfo: c, + } + pkg2 := &Package{ + Name: "some-package-2", + PackageVersion: "2.4.5", + CreationInfo: c, + } + doc.Elements = append(doc.Elements, pkg2) + + file1 := &File{ + Name: "/bin/bash", + CreationInfo: c, + } + doc.Elements = append(doc.Elements, file1) + + // add relationships + + doc.Elements = append(doc.Elements, + &Relationship{ + CreationInfo: c, + From: file1, + RelationshipType: RelationshipType_Contains, + To: []IElement{ + pkg1, + pkg2, + }, + }, + ) + + doc.Elements = append(doc.Elements, + &Relationship{ + CreationInfo: c, + From: pkg1, + RelationshipType: RelationshipType_DependsOn, + To: []IElement{ + pkg2, + }, + }, + ) + + doc.Elements = append(doc.Elements, + &AIPackage{ + CreationInfo: c, + TypeOfModel: []string{"a model"}, + }, + ) + + got := encodeDecodeRecode(t, &doc) + + // some basic verification: + + var pkgs []IPackage + for _, e := range got.GetElements() { + if rel, ok := e.(IRelationship); ok && rel.GetRelationshipType() == RelationshipType_Contains { + if from, ok := rel.GetFrom().(IFile); ok && from.GetName() == "/bin/bash" { + for _, el := range rel.GetTo() { + if pkg, ok := el.(IPackage); ok { + pkgs = append(pkgs, pkg) + } + } + + } + } + } + if len(pkgs) != 2 { + t.Error("wrong packages returned") + } +} + +func Test_stringSlice(t *testing.T) { + p := &AIPackage{ + TypeOfModel: []string{"a model"}, + } + encodeDecodeRecode(t, p) +} + +func Test_profileConformance(t *testing.T) { + doc := &SpdxDocument{ + ProfileConformance: []ProfileIdentifierType{ + ProfileIdentifierType_Software, + }, + } + encodeDecodeRecode(t, doc) +} + +func encodeDecodeRecode[T comparable](t *testing.T, obj T) T { + // serialization: + maps, err := ldGlobal.toMaps(obj) + if err != nil { + t.Fatal(err) + } + + buf := bytes.Buffer{} + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") + err = enc.Encode(maps) + if err != nil { + t.Fatal(err) + } + + json1 := buf.String() + fmt.Printf("--------- initial JSON: ----------\n%s\n\n", json1) + + // deserialization: + graph, err := ldGlobal.FromJSON(strings.NewReader(json1)) + var got T + for _, entry := range graph { + if e, ok := entry.(T); ok { + got = e + break + } + } + + var empty T + if got == empty { + t.Fatalf("did not find object in graph, json: %s", json1) + } + + // re-serialize: + maps, err = ldGlobal.toMaps(got) + if err != nil { + t.Fatal(err) + } + buf = bytes.Buffer{} + enc = json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") + err = enc.Encode(maps) + if err != nil { + t.Fatal(err) + } + json2 := buf.String() + fmt.Printf("--------- reserialized JSON: ----------\n%s\n", json2) + + // compare original to parsed and re-encoded + + diff := difflib.UnifiedDiff{ + A: difflib.SplitLines(json1), + B: difflib.SplitLines(json2), + FromFile: "Original", + ToFile: "Current", + Context: 3, + } + text, _ := difflib.GetUnifiedDiffString(diff) + if text != "" { + t.Fatal(text) + } + + return got +} + +func Test_refCount(t *testing.T) { + type O1 struct { + Name string + } + + type O2 struct { + Name string + O1s []*O1 + } + + o1 := &O1{"o1"} + o2 := &O1{"o2"} + o3 := &O1{"o3"} + o21 := &O2{"o21", []*O1{o1, o1, o2, o3}} + o22 := []*O2{ + {"o22-1", []*O1{o1, o1, o1, o1, o2, o3}}, + {"o22-2", []*O1{o1, o1, o1, o1, o2, o3}}, + {"o22-3", []*O1{o1, o1, o1, o1, o2, o3}}, + } + + type O3 struct { + Name string + Ref []*O3 + } + o31 := &O3{"o31", nil} + o32 := &O3{"o32", []*O3{o31}} + o33 := &O3{"o33", []*O3{o32}} + o31.Ref = []*O3{o33} + o34 := &O3{"o34", []*O3{o31, o32}} + o35 := &O3{"o35", []*O3{o31, o32, o31, o32}} + + type O4 struct { + Name string + Ref any + } + o41 := &O4{"o41", nil} + o42 := &O4{"o42", o41} + + tests := []struct { + name string + checkObj any + checkIn any + expected int + }{ + { + name: "none", + checkObj: o33, + checkIn: o21, + expected: 0, + }, + { + name: "interface", + checkObj: o41, + checkIn: o42, + expected: 1, + }, + { + name: "single", + checkObj: o3, + checkIn: o21, + expected: 1, + }, + { + name: "multiple", + checkObj: o1, + checkIn: o21, + expected: 2, + }, + + { + name: "multiple 2", + checkObj: o1, + checkIn: o22, + expected: 12, + }, + { + name: "circular 1", + checkObj: o31, + checkIn: o31, + expected: 1, + }, + { + name: "circular 2", + checkObj: o32, + checkIn: o31, + expected: 1, + }, + { + name: "circular 3", + checkObj: o33, + checkIn: o31, + expected: 1, + }, + { + name: "circular multiple", + checkObj: o32, + checkIn: o34, + expected: 2, + }, + { + name: "circular multiple 2", + checkObj: o32, + checkIn: o35, + expected: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cnt := refCount(tt.checkObj, tt.checkIn) + if cnt != tt.expected { + t.Errorf("wrong reference count: %v != %v", tt.expected, cnt) + } + }) + } +} diff --git a/src/shacl2code/lang/go_runtime/superclass_view.go b/src/shacl2code/lang/go_runtime/superclass_view.go new file mode 100644 index 0000000..1d96e01 --- /dev/null +++ b/src/shacl2code/lang/go_runtime/superclass_view.go @@ -0,0 +1,62 @@ +package runtime + +import ( + "fmt" + "reflect" +) + +// SuperclassView is a helper function to emulate some semblance of inheritance, +// while still having simple structs without embedding, it is highly experimental +func SuperclassView[View any](base any) *View { + var view *View + baseValue := reflect.ValueOf(base) + baseType := baseValue.Type() + validateBaseType(baseType) // base must be a pointer, see usage examples + viewType := reflect.TypeOf(view) + validateFieldAlignment(baseType, viewType) // base memory layout must be compatible with view + view = reflect.NewAt(viewType.Elem(), baseValue.UnsafePointer()).Interface().(*View) + return view +} + +func validateBaseType(base reflect.Type) { + if base.Kind() != reflect.Pointer { + panic(fmt.Errorf("invalid base type; must be a pointer")) + } + if base.Elem().Kind() != reflect.Struct { + panic(fmt.Errorf("invalid base type; must be a pointer to a struct")) + } +} + +func validateFieldAlignment(base, view reflect.Type) { + // should be passed either 2 pointers to struct types or 2 struct types + if base.Kind() == reflect.Pointer && view.Kind() == reflect.Pointer { + base = base.Elem() + view = view.Elem() + } + if base.Kind() != reflect.Struct || view.Kind() != reflect.Struct { + panic(fmt.Errorf("base and view types must point to structs; got base: %s and view: %s", typeName(base), typeName(view))) + } + // view needs to be a subset of the number of fields in base + if view.NumField() > base.NumField() { + panic(fmt.Errorf("view %s (%d fields) is not a subset of %s (%d fields)", typeName(view), view.NumField(), typeName(base), base.NumField())) + } + for i := 0; i < view.NumField(); i++ { + baseField := base.Field(i) + viewField := view.Field(i) + // ignore zero-sized fields + if baseField.Type.Size() == 0 && viewField.Type.Size() == 0 { + continue + } + // field layout must be identical, name _should_ be the same + if baseField.Name != viewField.Name { + panic(fmt.Errorf("field %d in base is named %s but view expects %s", i, baseField.Name, viewField.Name)) + } + if baseField.Type != viewField.Type { + panic(fmt.Errorf("field %d in base is has type %s but view expects %s", i, typeName(baseField.Type), typeName(viewField.Type))) + } + if baseField.Offset != viewField.Offset { + panic(fmt.Errorf("field %d in base is named %d but view expects %d", i, baseField.Offset, viewField.Offset)) + } + // seems to align + } +} diff --git a/src/shacl2code/lang/golang.py b/src/shacl2code/lang/golang.py new file mode 100644 index 0000000..417e1c3 --- /dev/null +++ b/src/shacl2code/lang/golang.py @@ -0,0 +1,321 @@ +# SPDX-License-Identifier: MIT + +from .common import BasicJinjaRender +from .lang import language, TEMPLATE_DIR +from ..model import Property + +import os +import sys +import subprocess +import re +import inspect + +DATATYPES = { + "http://www.w3.org/2001/XMLSchema#string": "string", + "http://www.w3.org/2001/XMLSchema#anyURI": "string", + "http://www.w3.org/2001/XMLSchema#integer": "int", + "http://www.w3.org/2001/XMLSchema#positiveInteger": "uint", # "PInt", + "http://www.w3.org/2001/XMLSchema#nonNegativeInteger": "uint", + "http://www.w3.org/2001/XMLSchema#boolean": "bool", + "http://www.w3.org/2001/XMLSchema#decimal": "float64", + "http://www.w3.org/2001/XMLSchema#dateTime": "string", # "DateTime", + "http://www.w3.org/2001/XMLSchema#dateTimeStamp": "string", # "DateTimeStamp", +} + +RESERVED_WORDS = { + "package" +} + + +@language("golang") +class GolangRender(BasicJinjaRender): + HELP = "Go Language Bindings" + # conform to go:generate format: https://pkg.go.dev/cmd/go#hdr-Generate_Go_files_by_processing_source + disclaimer = f"Code generated by {os.path.basename(sys.argv[0])}. DO NOT EDIT." + + # set during render, for convenience + render_args = None + + # arg defaults: + package_name = "model" + license_id = False + use_embedding = False + getter_prefix = "Get" + setter_prefix = "Set" + export_structs = "true" + interface_prefix = "I" + interface_suffix = "" + struct_prefix = "" + struct_suffix = "" + embedded_prefix = "super_" + pluralize = False + pluralize_length = 3 + include_runtime = "true" + include_view_pointers = False + as_concrete_prefix = "As" + uppercase_constants = True + constant_separator = "_" + remap_props = "" + remap_props_map = {} + + @classmethod + def get_arguments(cls, parser): + super().get_arguments(parser) + parser.add_argument("--package-name", help="Go package name to generate", default=cls.package_name) + parser.add_argument("--license-id", help="SPDX License identifier to include in the generated code", default=cls.license_id) + parser.add_argument("--use-embedding", type=bool, help="use embedded structs", default=cls.use_embedding) + parser.add_argument("--export-structs", help="export structs", default=cls.export_structs) + parser.add_argument("--struct-suffix", help="struct stuffix", default=cls.struct_suffix) + parser.add_argument("--interface-prefix", help="interface prefix", default=cls.interface_prefix) + parser.add_argument("--include-runtime", help="include runtime functions inline", default=cls.include_runtime) + parser.add_argument("--include-view-pointers", type=bool, help="include runtime functions inline", default=cls.include_view_pointers) + parser.add_argument("--disclaimer", help="file header", default=cls.disclaimer) + parser.add_argument("--remap-props", help="property name mapping", default=cls.remap_props) + parser.add_argument("--pluralize", default=cls.pluralize) + + def __init__(self, args): + super().__init__(args, TEMPLATE_DIR / "golang.j2") + for k, v in inspect.getmembers(args): + if k in GolangRender.__dict__ and not k in BasicJinjaRender.__dict__: + setattr(self, k, v) + + def render(self, template, output, *, extra_env={}, render_args={}): + if self.remap_props: + self.remap_props_map = dict(item.split("=") for item in self.remap_props.split(",")) + + class FW: + d = "" + + def write(self, d): + self.d += d + + w = FW() + self.render_args = render_args + super().render(template, w, extra_env=extra_env, render_args=render_args) + formatted = gofmt(w.d) + output.write(formatted) + + def get_extra_env(self): + return { + "trim_iri": trim_iri, + "indent": indent, + "upper_first": upper_first, + "lower_first": lower_first, + "is_array": is_array, + "comment": comment, + } + + def get_additional_render_args(self): + render_args = {} + # add all directly defined functions and variables + for k, v in inspect.getmembers(self): + if k.startswith("_") or k in BasicJinjaRender.__dict__: + continue + render_args[k] = v + return render_args + + def parents(self,cls): + return [self.all_classes().get(parent_id) for parent_id in cls.parent_ids] + + def properties(self,cls): + props = cls.properties + if cls.id_property and not cls.parent_ids: + return [ + Property( + path="@id", + datatype="http://www.w3.org/2001/XMLSchema#string", + min_count=1, # is this accurate? + max_count=1, + varname=cls.id_property, + comment="identifier property", + ), + ] + props + return props + + def all_classes(self): + return self.render_args["classes"] + + def pluralize_name(self,str): + if not self.pluralize: + return str + if len(str) < self.pluralize_length: + return str + if not str.endswith('s'): + return str + "s" + return str + + def struct_prop_name(self,prop): + # prop: + # class_id, comment, datatype, enum_values, max_count, min_count, path, pattern, varname + name = prop.varname + if is_array(prop): + name = self.pluralize_name(name) + + if name in self.remap_props_map: + name = self.remap_props_map[name] + + name = type_name(name) + + if self.export_structs.lower() != "false": + return upper_first(name) + + return lower_first(name) + + def prop_type(self,prop): + # prop: + # class_id, comment, datatype, enum_values, max_count, min_count, path, pattern, varname + if prop.datatype in DATATYPES: + typ = DATATYPES[prop.datatype] + else: + cls = self.all_classes().get(prop.class_id) + if self.requires_interface(cls): + typ = self.interface_name(cls) + else: + typ = self.struct_name(cls) + + return typ + + def parent_has_prop(self, cls, prop): + for parent in self.parents(cls): + for p in self.properties(parent): + if p.varname == prop.varname: + return True + if self.parent_has_prop(parent, prop): + return True + + return False + + def requires_interface(self,cls): + if cls.properties: + return True + if cls.derived_ids: + return True + # if cls.named_individuals: + # return False + # if cls.node_kind == rdflib.term.URIRef('http://www.w3.org/ns/shacl#BlankNodeOrIRI'): + # return True + if cls.parent_ids: + return True + return False + + def include_prop(self, cls, prop): + return not self.parent_has_prop(cls, prop) + + def interface_name(self,cls): + return upper_first(self.interface_prefix + type_name(cls.clsname) + self.interface_suffix) + + def struct_name(self,cls): + name = self.struct_prefix + type_name(cls.clsname) + self.struct_suffix + if self.export_structs: + name = upper_first(name) + else: + name = lower_first(name) + + if name in RESERVED_WORDS: + return name + "_" + + return name + + def pretty_name(self,cls): + return upper_first(type_name(cls.clsname)) + + def constant_var_name(self,named): + if self.uppercase_constants: + return upper_first(named.varname) + return named.varname + + def getter_name(self,prop): + return self.getter_prefix + upper_first(self.struct_prop_name(prop)) + + def setter_name(self,prop): + return self.setter_prefix + upper_first(self.struct_prop_name(prop)) + + def concrete_name(self,cls): + if self.export_structs.lower() != "false": + return self.struct_name(cls) + if self.use_embedding: + return self.embedded_prefix + upper_first(self.struct_name(cls)) + if cls.is_abstract: + return self.struct_name(cls) + return upper_first(self.struct_name(cls)) + + def include_runtime_code(self): + if self.include_runtime.lower() == "false": + return "" + + package_replacer = "package[^\n]+" + import_replacer = "import[^)]+\\)" + code = "" + dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "go_runtime") + with open(os.path.join(dir, "ld_context.go")) as f: + code += re.sub(package_replacer, "", f.read()) + with open(os.path.join(dir, "graph_builder.go")) as f: + code += re.sub(package_replacer, "", re.sub(import_replacer, "", f.read())) + if self.include_view_pointers: + with open(os.path.join(dir, "superclass_view.go")) as f: + code += re.sub(package_replacer, "", re.sub(import_replacer, "", f.read())) + return code + + +# common utility functions + + +def upper_first(str): + return str[0].upper() + str[1:] + + +def lower_first(str): + return str[0].lower() + str[1:] + + +def indent(indent_with, str): + parts = re.split("\n", str) + return indent_with + ("\n"+indent_with).join(parts) + + +def dedent(str, amount): + prefix = ' ' * amount + parts = re.split("\n", str) + for i in range(len(parts)): + if parts[i].startswith(prefix): + parts[i] = parts[i][len(prefix):] + return '\n'.join(parts) + + +def type_name(name): + if isinstance(name, list): + name = "".join(name) + parts = re.split(r'[^a-zA-Z0-9]', name) + part = parts[len(parts)-1] + return upper_first(part) + + +def gofmt(code): + try: + proc = subprocess.Popen(["gofmt"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, text=True) + result = proc.communicate(input=code) + if result[0] != "": + return result[0] + return code + except: + return code + + +def is_array(prop): + return prop.max_count is None or prop.max_count > 1 + + +def comment(indent_with, identifier, text): + if text.lower().startswith(identifier.lower()): + text = text[len(identifier):] + text = identifier + " " + lower_first(text) + return indent(indent_with, text) + + +def trim_iri(base,iri): + if not base.endswith("/"): + base += "/" + if False and iri.startswith(base): + return iri[len(base):] + return iri + diff --git a/src/shacl2code/lang/templates/golang.j2 b/src/shacl2code/lang/templates/golang.j2 new file mode 100644 index 0000000..db2a0a5 --- /dev/null +++ b/src/shacl2code/lang/templates/golang.j2 @@ -0,0 +1,151 @@ +// {{ disclaimer }} +{%- if license_id %} +// +// SPDX-License-Identifier: {{ license_id }} +{%- endif %} + +package {{ package_name }} + +{#- include runtime code inline #} +{{- include_runtime_code() }} + +{#- struct_props outputs all properties for concrete struct implementations #} +{%- macro struct_props(cls) %} + {#- there is no embedding, so recursively output all parent properties #} + {%- for parent in parents(cls) %} + {%- if not use_embedding %} + {{- struct_props(parent) }} + {%- else %} + {{ concrete_name(parent) }} + {%- endif %} + {% endfor %} + {#- output direct struct properties #} + {% for prop in properties(cls) %} + {%- if include_prop(cls,prop) %} + {{ comment("// ", struct_prop_name(prop), prop.comment)|indent }} + {{ struct_prop_name(prop) }} {% if is_array(prop) %}[]{% endif %}{{ prop_type(prop) }} `iri:"{{ prop.path }}" iri-compact:"{{ context.compact_vocab(prop.path) }}"` + {%- endif %} + {%- endfor %} +{%- endmacro %} + +{#- struct_funcs outputs all functions for concrete struct implementations #} +{%- macro struct_funcs(base,cls) %} + {#- embedded structs may be expanded props #} + {%- if not use_embedding %} + {%- for parent in parents(cls) %} + {{ struct_funcs(base,parent) }} + {%- endfor %} + {%- endif %} + +{% if requires_interface(cls) and include_view_pointers %} +func (o *{{ struct_name(base) }}) {{ as_concrete_prefix }}{{ concrete_name(cls) }}() *{{ concrete_name(cls) }} { + {%- if base == cls %} + return o + {%- else %} + return SuperclassView[{{ concrete_name(cls) }}](o) + {%- endif %} +} +{%- endif %} + {%- for prop in properties(cls) %} + {%- if include_prop(cls,prop) %} +func (o *{{ struct_name(base) }}) {{ getter_name(prop) }}() {% if is_array(prop) %}[]{% endif %}{{ prop_type(prop) }} { + return o.{{ struct_prop_name(prop) }} +} +func (o *{{ struct_name(base) }}) {{ setter_name(prop) }}(v {% if is_array(prop) %}...{% endif %}{{ prop_type(prop) }}) { + o.{{ struct_prop_name(prop) }} = v +} + {%- endif %} + {%- endfor %} +{%- endmacro %} + +{#- interface_props outputs all interface property definitions #} +{%- macro interface_props(cls) -%} + {#- embedding parent interfaces for proper inheritance #} + {%- for parent in parents(cls) %} + {{- interface_name(parent) }} + {% endfor %} + {%- for prop in properties(cls) %} + {%- if include_prop(cls,prop) %} + {{ comment("// ", getter_name(prop), prop.comment)|indent }} + {{ getter_name(prop) }}() {% if is_array(prop) %}[]{% endif %}{{ prop_type(prop) }} + + {{ setter_name(prop) }}({% if is_array(prop) %}...{% endif %}{{ prop_type(prop) }}) + {% endif %} + {%- endfor %} +{%- endmacro %} + +{#- ------------------------ CONSTRUCTOR ------------------------ #} +{%- macro constructor(cls) %} +func New{{ pretty_name(cls) }}() {{ interface_name(cls) }} { + return &{{ struct_name(cls) }}{} +} +{%- endmacro %} + +{#- ------------------------ INTERFACE ------------------------ #} +{%- macro interface(cls) %} +type {{ interface_name(cls) }} interface { + {{ interface_props(cls) }} +} +{%- endmacro %} + +{#- ------------------------ STRUCT ------------------------ #} +{%- macro struct(cls) %} +type {{ struct_name(cls) }} struct { + _ ldType `iri:"{{ cls._id }}" iri-compact:"{{ context.compact_vocab(cls._id) }}"{% if cls.id_property %} id-prop:"{{ cls.id_property }}"{% endif %}` + {% if not cls.id_property %} + Iri string `iri:"@id"` + {%- endif %} + {{- struct_props(cls) }} +} +{%- endmacro %} + +{#- ------------ CLASSES AND INTERFACES -------------- #} +{%- for cls in classes %} + {#- output the interface definition if required #} + {%- if requires_interface(cls) %} + {{ interface(cls) }} + {%- endif %} + + {#- output the struct definition #} + {{ struct(cls) }} + + {%- if include_view_pointers and concrete_name(cls) != struct_name(cls) %} + type {{ concrete_name(cls) }} = {{ struct_name(cls) }} + {%- endif %} + + {#- output any named constants #} + {%- if cls.named_individuals %} + var ( + {%- for ind in cls.named_individuals %} + {{ pretty_name(cls) }}{{ constant_separator }}{{ constant_var_name(ind) }} {%- if requires_interface(cls) %} {{ interface_name(cls) }} {%- endif %} = {{ struct_name(cls) }}{ {% if not cls.id_property %}Iri{% else %}{{ cls.id_property }}{% endif %}:"{{ trim_iri(cls._id,ind._id) }}" } + {%- endfor %} + ) + {%- endif %} + + {%- if not cls.is_abstract and requires_interface(cls) %} + {{ constructor(cls) }} + {%- endif %} + + {{ struct_funcs(cls,cls) }} +{%- endfor %} + +{#- ------------ type mapping for serialization -------------- #} +var ldGlobal = ldContext{} +{%- for url in context.urls %}. + RegisterTypes("{{ url }}", + {%- for cls in classes %} + &{{ struct_name(cls) }}{}, + {%- endfor %} + ) + {%- for cls in classes %} + {%- for prop in properties(cls) %} + {%- if prop.enum_values -%}. + IRIMap("{{ url }}", &{{ prop_type(prop) }}{}, map[string]string{ + {%- for iri in prop.enum_values %} + "{{ iri }}": "{{ context.compact_vocab(iri, prop.path) }}", + {%- endfor %} + }) + {%- endif %} + {%- endfor %} + {%- endfor %} +{%- endfor %} From eb4060b9f0c31c7f981e1e411dbc0ec7160706d6 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Sat, 3 Aug 2024 21:35:25 -0400 Subject: [PATCH 2/2] fix: support external references without specified types Signed-off-by: Keith Zantow --- src/shacl2code/lang/go_runtime/ld_context.go | 39 ++++++++++++++++++- .../lang/go_runtime/runtime_test.go | 17 ++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/shacl2code/lang/go_runtime/ld_context.go b/src/shacl2code/lang/go_runtime/ld_context.go index c3fa721..6a75fc0 100644 --- a/src/shacl2code/lang/go_runtime/ld_context.go +++ b/src/shacl2code/lang/go_runtime/ld_context.go @@ -170,7 +170,20 @@ func (c ldContext) getOrCreateInstance(currentContext *serializationContext, ins }) return instance, err case reflect.Interface: - return emptyValue, fmt.Errorf("unable to determine appropriate type for external IRI reference: %v", incoming) + // an IRI with an interface is a reference to an unknown type, so use the closest type + newType, found := c.findExternalReferenceType(currentContext, expectedType) + if found { + instance = reflect.New(newType) + // try to return the appropriately assignable instance + if !instance.Type().AssignableTo(expectedType) { + instance = instance.Elem() + } + err := c.setStructProps(currentContext, instances, instance, map[string]any{ + ldIDProp: incoming, + }) + return instance, err + } + return emptyValue, fmt.Errorf("unable to determine external reference type while populating %v for IRI reference: %v", typeName(expectedType), incoming) default: } case map[string]any: @@ -363,6 +376,30 @@ func (c ldContext) setSliceValue(currentContext *serializationContext, instances return errs } +func (c ldContext) findExternalReferenceType(currentContext *serializationContext, expectedType reflect.Type) (reflect.Type, bool) { + tc := currentContext.typeToContext[expectedType] + if tc != nil { + return tc.typ, true + } + bestMatch := anyType + for t := range currentContext.typeToContext { + if t.Kind() != reflect.Struct { + continue + } + // the type with the fewest fields assignable to the target is a good candidate to be an abstract type + if reflect.PointerTo(t).AssignableTo(expectedType) && (bestMatch == anyType || bestMatch.NumField() > t.NumField()) { + bestMatch = t + } + } + if bestMatch != anyType { + currentContext.typeToContext[expectedType] = &typeContext{ + typ: bestMatch, + } + return bestMatch, true + } + return anyType, false +} + func skipField(field reflect.StructField) bool { return field.Type.Size() == 0 } diff --git a/src/shacl2code/lang/go_runtime/runtime_test.go b/src/shacl2code/lang/go_runtime/runtime_test.go index 2c83224..8619c93 100644 --- a/src/shacl2code/lang/go_runtime/runtime_test.go +++ b/src/shacl2code/lang/go_runtime/runtime_test.go @@ -3,7 +3,6 @@ package runtime import ( "bytes" "encoding/json" - "fmt" "strings" "testing" @@ -140,6 +139,18 @@ func Test_profileConformance(t *testing.T) { encodeDecodeRecode(t, doc) } +func Test_externalID(t *testing.T) { + doc := &SpdxDocument{ + Elements: []IElement{ + &Element{ + SpdxId: "http://someplace.org/ac7b643f0b2d", + }, + }, + } + encodeDecodeRecode(t, doc) +} + +// encodeDecodeRecode encodes to JSON, decodes from the JSON, and re-encodes in JSON to validate nothing is lost func encodeDecodeRecode[T comparable](t *testing.T, obj T) T { // serialization: maps, err := ldGlobal.toMaps(obj) @@ -157,7 +168,7 @@ func encodeDecodeRecode[T comparable](t *testing.T, obj T) T { } json1 := buf.String() - fmt.Printf("--------- initial JSON: ----------\n%s\n\n", json1) + t.Logf("--------- initial JSON: ----------\n%s\n\n", json1) // deserialization: graph, err := ldGlobal.FromJSON(strings.NewReader(json1)) @@ -188,7 +199,7 @@ func encodeDecodeRecode[T comparable](t *testing.T, obj T) T { t.Fatal(err) } json2 := buf.String() - fmt.Printf("--------- reserialized JSON: ----------\n%s\n", json2) + t.Logf("--------- reserialized JSON: ----------\n%s\n", json2) // compare original to parsed and re-encoded