Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DO NOT MERGE: SPDX 3 Bindings #249

Closed
wants to merge 1 commit into from
Closed

Conversation

JPEWdev
Copy link

@JPEWdev JPEWdev commented Nov 14, 2024

This is a very initial pass at the generated go bindings from shacl2code. These bindings are incomplete, but give a basic idea of what they might look like for evaluation purposes

@nishakm
Copy link

nishakm commented Dec 13, 2024

I'd like to try and pair this down to the basics required to create an SPDX "document". So far, I've been operation on the assumption that this requires:

  • Agent
  • CreationInfo

For example, in example1 of the spdx-examples repo, the bare minimum required json is:

{
  "@context" : "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
  "@graph" : [ {
    "@id" : "_:creationInfo_0",
    "type" : "CreationInfo",
    "specVersion" : "3.0.1",
    "createdBy" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd0" ],
    "created" : "2021-08-26T01:46:00Z"
  }, {
    "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd0",
    "type" : "Person",
    "externalIdentifier" : [ {
      "type" : "ExternalIdentifier",
      "identifier" : "[email protected]",
      "externalIdentifierType" : "email"
    } ],
    "name" : "Steve Winslow",
    "creationInfo" : "_:creationInfo_0"
  }, {
    "spdxId" : "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/document0",
    "type" : "SpdxDocument",
    "rootElement" : [ "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd0" ],
    "name" : "hello",
    "creationInfo" : "_:creationInfo_0"
  }]
}

I'm not sure if creating an SPDX document requires a package, but from my experimentation, it does require a CreationInfo and an Agent to be created beforehand. In order to create the CreationInfo, the Agent's SPDXID needs to be created first.
Does this match everyone's understanding?

@JPEWdev
Copy link
Author

JPEWdev commented Dec 13, 2024

Yes, that looks correct to me.

@goneall
Copy link
Member

goneall commented Dec 13, 2024

Me too - that's about the minimum

@nishakm
Copy link

nishakm commented Dec 17, 2024

First pass feedback:

  • type ([T any]) parameter is only allowed for go 1.18 and higher and the current go.mod says go 1.13. I would drop type parameters.
  • I'm still trying to figure out how to actually generate the json I posted above. How do I create a "Person" with a name and email?
  • I noticed some weird combination of type constraints and interfaces. The purpose of using the interface is because Go doesn't understand polymorphism. In order to make a "is a" relationship, you have to make an interface and then have objects implement that interface.
  • No Getters and Setters for properties in objects.

@nishakm
Copy link

nishakm commented Dec 17, 2024

Some initial testing:

  1 package main
  2 
  3 import "spdx3/pkg/spdx"
  4 
  5 func main() {
  6         p := spdx.MakePerson()
  7         p.Name = "Steve Winslow"
  8 }

->

 go build main.go 
# command-line-arguments
./main.go:7:2: cannot assign to p.Name (neither addressable nor a map index expression)

I'm actually surprised the compiler can find that the Person struct has a property called Name because I couldn't find it in my code search.

./main.go:7:4: p.name undefined (type spdx.Person has no field or method name, but does have Name)

@JPEWdev
Copy link
Author

JPEWdev commented Dec 17, 2024

First pass feedback:

* type ([T any]) parameter is only allowed for go 1.18 and higher and the current go.mod says go 1.13. I would drop type parameters.

I'll see if I can make something work. Is there a specific minimum version of go we should support? 1.13 feels very old as a minimum version. If there is a way to allow the "generic" types in 1.13 I'm fine with that but the bindings will be.... pretty cantankerous if there isn't a way to do this.

* I'm still trying to figure out how to actually generate the json I posted above. How do I create a "Person" with a name and email?

Something like:

p := model.MakePerson()
p.Name().Set("Joshua")
email := model.MakeExternalIndentifier()
email.ExternalIdentifierType().Set(model.ExternalIdentifierTypeEmail)
email.Identifier().Set("[email protected]")

# I might add an append to add a single item to the end, because that is common
p.ExternalIdentifier().Set([]Ref[ExternalIdentifier]{model.MakeObjectRef(email)})
* I noticed some weird combination of type constraints and interfaces. The purpose of using the interface is because Go doesn't understand polymorphism. In order to make a "is a" relationship, you have to make an interface and then have objects implement that interface.

Can you be more specific? In general, you should only be dealing with interfaces.

* No Getters and Setters for properties in objects.

That's really not possible if we want to do any sort of assignment validation. The assignment validation is really helpful in the other language bindings since it gives immediate feedback about where you're using the model wrong, so I think the bindings are much more useful with them, even if it's not "the go way". We really need to be dealing with all the SPDX objects via interfaces to keep the polymorphism as best possible (at least as far as I can tell), and you can't (AFAICT) have properties on interfaces, just getters and setters.

@nishakm
Copy link

nishakm commented Dec 17, 2024

First pass feedback:

* type ([T any]) parameter is only allowed for go 1.18 and higher and the current go.mod says go 1.13. I would drop type parameters.

I'll see if I can make something work. Is there a specific minimum version of go we should support? 1.13 feels very old as a minimum version. If there is a way to allow the "generic" types in 1.13 I'm fine with that but the bindings will be.... pretty cantankerous if there isn't a way to do this.

The way to do this is just not use type PropertyInterface[T any] interface as [T any] translates to "interface" anyway. type PropertyInterface interface is enough.

Can you be more specific? In general, you should only be dealing with interfaces.

Oh I see my problem: in some places interfaces are called "Element", "Agent" etc, and in others, it's called "PropertyInterface" rather than "Property". But now I know where to look for things. Maybe consistent naming for the SHACL parts and the SPDX parts?

* No Getters and Setters for properties in objects.

That's really not possible if we want to do any sort of assignment validation. The assignment validation is really helpful in the other language bindings since it gives immediate feedback about where you're using the model wrong, so I think the bindings are much more useful with them, even if it's not "the go way". We really need to be dealing with all the SPDX objects via interfaces to keep the polymorphism as best possible (at least as far as I can tell), and you can't (AFAICT) have properties on interfaces, just getters and setters.

I got it - manipulating objects happen at the SHACL level so that validation works. Let me try again.

@JPEWdev
Copy link
Author

JPEWdev commented Dec 17, 2024

First pass feedback:

* type ([T any]) parameter is only allowed for go 1.18 and higher and the current go.mod says go 1.13. I would drop type parameters.

I'll see if I can make something work. Is there a specific minimum version of go we should support? 1.13 feels very old as a minimum version. If there is a way to allow the "generic" types in 1.13 I'm fine with that but the bindings will be.... pretty cantankerous if there isn't a way to do this.

The way to do this is just not use type PropertyInterface[T any] interface as [T any] translates to "interface" anyway. type PropertyInterface interface is enough.

Hmm, I don't think that's the same. I'm using Generics here: https://go.dev/doc/tutorial/generics which it looks like were added in go 1.18.... it would be pretty annoying to write this code without them TBH (it would have to be way more verbose)

Can you be more specific? In general, you should only be dealing with interfaces.

Oh I see my problem: in some places interfaces are called "Element", "Agent" etc, and in others, it's called "PropertyInterface" rather than "Property". But now I know where to look for things. Maybe consistent naming for the SHACL parts and the SPDX parts?

* No Getters and Setters for properties in objects.

That's really not possible if we want to do any sort of assignment validation. The assignment validation is really helpful in the other language bindings since it gives immediate feedback about where you're using the model wrong, so I think the bindings are much more useful with them, even if it's not "the go way". We really need to be dealing with all the SPDX objects via interfaces to keep the polymorphism as best possible (at least as far as I can tell), and you can't (AFAICT) have properties on interfaces, just getters and setters.

I got it - manipulating objects happen at the SHACL level so that validation works. Let me try again.

@nishakm
Copy link

nishakm commented Dec 17, 2024

Something like:

p := model.MakePerson()
p.Name().Set("Joshua")
email := model.MakeExternalIndentifier()
email.ExternalIdentifierType().Set(model.ExternalIdentifierTypeEmail)
email.Identifier().Set("[email protected]")

# I might add an append to add a single item to the end, because that is common
p.ExternalIdentifier().Set([]Ref[ExternalIdentifier]{model.MakeObjectRef(email)})

Can you finish this with CreationInfo? I am having trouble with the ListPropertyInterface.

@JPEWdev
Copy link
Author

JPEWdev commented Dec 17, 2024

ci := model.MakeCreationInfo()
ci.SpecVersion().Set("3.0.1")
ci.CreatedBy().Set(model.MakeObjectRef(p))
ci.Created().Set(time.Now())

p.CreationInfo().Set(model.MakeObjectRef(ci))

@JPEWdev
Copy link
Author

JPEWdev commented Dec 17, 2024

Oh, right, the typing is a little weird with the references in golang.... hang on and I'll get an example that works.

@JPEWdev
Copy link
Author

JPEWdev commented Dec 17, 2024

@nishakm Hey, you found a bug :) ! The latest version works with this example code:

package main

import (
    "time"
    "os"
    "fmt"
    "encoding/json"
    "model"
)

func main() {
    // Note: Object de-duplication when serializing is not implemented in the
    // go bindings yet, so this example code links by IRI reference instead of
    // by object reference. This would not normally be the recommended way to
    // do this, as object references are much more usable

    p := v3_0_1.MakePerson()
    p.ID().Set("http://spdx.org/spdxdocs/person-example")
    p.Name().Set("Joshua")

    ci := v3_0_1.MakeCreationInfo()

    // This would not normally be required, but references are done by IRI
    // because deduplication is not implemented
    ci.ID().Set("_:creation-info")

    ci.SpecVersion().Set("3.0.1")

    // Normally would do:
    // ci.CreatedBy().Set([]v3_0_1.Ref[v3_0_1.Agent]{v3_0_1.MakeObjectRef[v3_0_1.Agent](p)})
    ci.CreatedBy().Set([]v3_0_1.Ref[v3_0_1.Agent]{v3_0_1.MakeIRIRef[v3_0_1.Agent](p.ID().Get())})
    ci.Created().Set(time.Now())

    // Normally would do:
    //p.CreationInfo().Set(v3_0_1.MakeObjectRef(ci))
    p.CreationInfo().Set(v3_0_1.MakeIRIRef[v3_0_1.CreationInfo](ci.ID().Get()))

    email := v3_0_1.MakeExternalIdentifier()
    email.ExternalIdentifierType().Set(v3_0_1.ExternalIdentifierTypeEmail)
    email.Identifier().Set("[email protected]")

    p.ExternalIdentifier().Set([]v3_0_1.Ref[v3_0_1.ExternalIdentifier]{v3_0_1.MakeObjectRef(email)})

    doc := v3_0_1.MakeSpdxDocument()
    doc.ID().Set("http://spdx.org/spdxdocs/doc-example")

    // Normally would do:
    //doc.CreationInfo().Set(v3_0_1.MakeObjectRef(ci))
    doc.CreationInfo().Set(v3_0_1.MakeIRIRef[v3_0_1.CreationInfo](ci.ID().Get()))

    objset := v3_0_1.NewSHACLObjectSet()
    objset.AddObject(p)
    objset.AddObject(doc)

    // The creation info would not normally be added manually, but it has to be
    // since it's referenced by IRI (not by object) due to no deduplication
    // when serializing
    objset.AddObject(ci)


    file, err := os.Create("out.json")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer file.Close()

    encoder := json.NewEncoder(file)
    if err := objset.Encode(encoder); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }

}

Note that serialization is still very incomplete in the go bindings, so this code does references by IRI instead of by object, but the places that affects are commented in the code

This is a *very* initial pass at the generated go bindings from
shacl2code. These bindings are incomplete, but give a basic idea of what
they might look like for evaluation purposes
@JPEWdev
Copy link
Author

JPEWdev commented Dec 17, 2024

OK, I was motivated to do some cleanup here. The object references are now fixed, as well as few other things. Here is the new test code which works and passes SPDX 3 validation:

package main

import (
    "time"
    "os"
    "fmt"
    "encoding/json"
    "model"
)

func main() {
    objset := v3_0_1.NewSHACLObjectSet()

    p := v3_0_1.MakePerson()
    p.ID().Set("http://spdx.org/spdxdocs/person-example")
    p.Name().Set("Joshua")
    objset.AddObject(p)

    ci := v3_0_1.MakeCreationInfo()
    ci.SpecVersion().Set("3.0.1")
    ci.CreatedBy().Append(v3_0_1.MakeObjectRef[v3_0_1.Agent](p))
    ci.Created().Set(time.Now().UTC())

    p.CreationInfo().Set(v3_0_1.MakeObjectRef(ci))

    email := v3_0_1.MakeExternalIdentifier()
    email.ExternalIdentifierType().Set(v3_0_1.ExternalIdentifierTypeEmail)
    email.Identifier().Set("[email protected]")

    p.ExternalIdentifier().Append(v3_0_1.MakeObjectRef(email))

    doc := v3_0_1.MakeSpdxDocument()
    doc.ID().Set("http://spdx.org/spdxdocs/doc-example")
    doc.CreationInfo().Set(v3_0_1.MakeObjectRef(ci))

    objset.AddObject(doc)

    file, err := os.Create("out.json")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer file.Close()

    encoder := json.NewEncoder(file)
    if err := objset.Encode(encoder); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }

}

@nishakm
Copy link

nishakm commented Dec 18, 2024

@JPEWdev So you can use interface{} instead of golang types:

// SHACL Property
type Property interface {
	Get()
	Set(val interface{}) error
}

type PropertyBase struct {
	value interface{}
	name  string
}

func (p *PropertyBase) Get() interface{} {
	return p.value
}

func (p *PropertyBase) Set(val interface{}) error {
	p.value = val
	return nil
}

func main() {
	prop1 := PropertyBase{}
	prop1.Set("hello")
	prop2 := PropertyBase{}
	prop2.Set(1)
}

But this is really annoying so I agree with you that we probably need to move to the latest Go version (1.23 at this time). We may need to host a new repo in spdx for this specifically (at least for now).

@nishakm
Copy link

nishakm commented Dec 18, 2024

This looks right to me...

{
  "@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
  "@graph": [
    {
      "creationInfo": {
        "@id": "_:0",
        "created": "2024-12-18T16:43:24Z",
        "createdBy": [
          "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd0"
        ],
        "specVersion": "3.0.1",
        "type": "CreationInfo"
      },
      "externalIdentifier": [
        {
          "externalIdentifierType": "email",
          "identifier": "[email protected]",
          "type": "ExternalIdentifier"
        }
      ],
      "name": "Steve Winslow",
      "spdxId": "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd0",
      "type": "Person"
    },
    {
      "creationInfo": "_:0",
      "spdxId": "https://swinslow.net/spdx-examples/example1/hello-v3-specv3/document0",
      "type": "SpdxDocument"
    }
  ]
}

@nishakm
Copy link

nishakm commented Dec 19, 2024

@JPEWdev it looks like SHACLObjectSet doesn't have any methods to get individual objects? Is this something we want to implement?

@nishakm
Copy link

nishakm commented Jan 15, 2025

My code right now:

  1 package main
  2 
  3 import (
  4         "encoding/json"
  5         "fmt"
  6         "os"
  7         spdx "spdx3/pkg/spdx"
  8         "time"
  9 )
 10 
 11 func main() {
 12         // Write doc
 13         //WriteToStdout()
 14 
 15         // read doc
 16         ReadFromStdin()
 17 
 18 }
 19 
 20 func WriteToStdout() {
 21         // not a document but a SHACLObjectSet
 22         objset := MakeDoc()
 23         // write to stdout
 24         encoder := json.NewEncoder(os.Stdout)
 25         if err := objset.Encode(encoder); err != nil {
 26                 fmt.Println(err)
 27                 os.Exit(1)
 28         }
 29 }
 30 
 31 func ReadFromStdin() {
 32         // read from stdin
 33         decoder := json.NewDecoder(os.Stdin)
 34         objset := spdx.NewSHACLObjectSet()
 35         if err := objset.Decode(decoder); err != nil {
 36                 fmt.Println(err)
 37                 os.Exit(1)
 38         }
 39         fmt.Println(objset)
 40 }
 41 
 42 func MakeDoc() spdx.SHACLObjectSet {
 43         // hmmm
 44         objset := spdx.NewSHACLObjectSet()
 45 
 46         p := spdx.MakePerson()
 47         // ought to be generated
 48         p.ID().Set("https://swinslow.net/spdx-examples/example1/hello-v3-specv3/SPDXRef-gnrtd0")
 49         p.Name().Set("Steve Winslow")
 50         // This is how you create a reference
 51         p_ref := spdx.MakeObjectRef[spdx.Agent](p)
 52         objset.AddObject(p)

@JPEWdev
Copy link
Author

JPEWdev commented Jan 15, 2025

The code bindings are going to be put in spdx/spdx-go-model#1 so further discussion should be moved there

@JPEWdev JPEWdev closed this Jan 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants