README ¶
aas-core3.0-golang
Manipulate, verify and de/serialize asset administration shells in Go.
This is a software development kit (SDK) to:
- manipulate,
- verify, and
- de/serialize to and from JSON
… Asset Administration Shells based on the version 3.0 of the meta-model.
For a brief introduction, see Getting Started.
For a detailed documentation of the API, see API Documentation.
We documented most of the rationale behind the implementation and interface choices in the section Design Decisions.
If you want to contribute, see our Contributing Guide.
The history of the module is listed in the Change Log.
Getting Started
Here's a quick intro to get you started with the SDK. See how you can:
- Install the SDK,
- Programmatically create, get and set properties of an AAS model,
- Switch on runtime types of instances,
- Iterate over, copy and transform a model,
- Verify a model,
- De/serialize a model from and to JSON, and
- De/serialize a model from and to XML, and
- Enhance instances with your custom data.
Install the SDK
The SDK is available as a module github.com/aas-core-works/aas-core3.0-golang
.
Install it using go get
:
go get github.com/aas-core-works/aas-core3.0-golang
Create, Get and Set
The package types
defines all the data types of the meta-model.
This includes structs, interfaces and enumerations.
Creation
We model each meta-model class, abstract and concrete alike, as Go interface.
You should prefer interfaces to structs so that you can use enhancing
package (see below in Section Enhancing).
The most general interface types.IClass
represents an instance of the AAS model.
All other interfaces adopt it.
We use constructors to create an AAS model.
They are marked as New*
.
For example, types.NewEnvironment
.
Usually you start bottom-up, all the way up to the types.Environment
.
Getting and Setting Properties
All properties of the classes are modeled as getter and setter methods.
The properties which are not set should be assigned a nil
.
The lists are modeled as slices.
For example, types.Environment.Submodels
:
func (e *Environment) Submodels() []ISubmodel
Byte arrays are modeled as slices of byte
.
For example, types.Blob.Value
:
func (b *Blob) Value() []byte
Getters with a Default Value
For optional properties which come with a default value, we provide special getters, {property name}OrDefault
.
If the property is nil
, this getter will give you the default value.
Otherwise, if the property is set, the actual value of the property will be returned.
For example, see types.IHasKind.KindOrDefault
.
Example: Create an Environment with a Submodel
Here is a very rudimentary example where we show how to create an environment which contains a submodel.
The submodel will contain two elements, a property and a blob.
package main
import (
"fmt"
aastypes "github.com/aas-core-works/aas-core3.0-golang/types"
)
// Create a new instance of the `value` and return the pointer to it.
func NewString(value string) *string {
return &value
}
func main() {
// Create the first element
someElement := aastypes.NewProperty(
aastypes.DataTypeDefXSDString,
)
someElement.SetIDShort(
NewString("someProperty"),
)
someElement.SetValue(
NewString("some-value"),
)
// Create the second element
anotherElement := aastypes.NewBlob(
"application/octet-stream",
)
anotherElement.SetIDShort(
NewString("someBlob"),
)
anotherElement.SetValue(
[]byte{0xDE, 0xAD, 0xBE, 0xEF},
)
// Nest the elements in a submodel
submodel := aastypes.NewSubmodel(
"some-unique-global-identifier",
)
submodel.SetSubmodelElements(
[]aastypes.ISubmodelElement{
someElement,
anotherElement,
},
)
// Now create the environment to wrap it all up
environment := aastypes.NewEnvironment()
environment.SetSubmodels(
[]aastypes.ISubmodel{
submodel,
},
)
// You can set the properties.
environment.Submodels()[0].SubmodelElements()[0].(aastypes.IProperty).SetValue(
NewString("changed-value"),
)
// You can access the properties from the children.
fmt.Printf(
"%v\n",
*environment.Submodels()[0].SubmodelElements()[0].(aastypes.IProperty).Value(),
)
// Output:
// changed-value
}
(See: Example CreateAnEnvironmentWithASubmodel)
Switch on Runtime Types
As we noted in Section Creation, the classes of the meta-model are specified as Go interfaces.
Go uses structural typing so any struct satisfying an interface automatically implements that interface.
This breaks runtime type switches, as you can not exactly infer the exact runtime type of instance as soon as it satisfies multiple interfaces.
To that end, every instance is provided with ModelType()
method that provides the exact model type at runtime as types.ModelType
.
This has an additional benefit for computational efficiency. Applying type switches is more complex than applying a switch on an integer enumeration. A switch on an enumerator can be optimized by compiler as a jump table, while type switches are not as straight-forward, especially due to multiple inheritance.
We also provide functions Is*
in types
package to allow for runtime type checks which honour the inheritance.
For example, see types.IsSubmodelElement
.
Here is a short example with types.IsSubmodelElement
, types.IsProperty
and types.IsBlob
:
package main
import (
"fmt"
aastypes "github.com/aas-core-works/aas-core3.0-golang/types"
)
func main() {
// Create the first element
someElement := aastypes.NewProperty(
aastypes.DataTypeDefXSDInt,
)
fmt.Printf("%v\n", aastypes.IsSubmodelElement(someElement))
fmt.Printf("%v\n", aastypes.IsProperty(someElement))
fmt.Printf("%v\n", aastypes.IsBlob(someElement))
// Output:
// true
// true
// false
}
(See: Example IsXxx)
Iterate
Looping through the instances of a model is tedious to write manually, especially when you want to recursively iterate over a model.
The SDK provides two methods for all the structs implementing types.IClass
, DescendOnce
and Descend
, which you can use to loop through the instances.
Both DescendOnce
and Descend
iterate over referenced children of an instance of types.IClass
.
The method [DescendOnce
], as it names suggests, stops after all the immediate children has been iterated over.
The method [Descend
] continues recursively to grand-children, grand-grand-children etc.
You have to supply a callback function which is applied on every instance that we iterate over.
If the callback function returns true
(as its return argument abort
), the iteration stops.
Here is a short example which shows how you can get all the properties from an environment whose ID-short contains the word another
:
package main
import (
"fmt"
aastypes "github.com/aas-core-works/aas-core3.0-golang/types"
"strings"
)
// Create a new instance of the `value` and return the pointer to it.
func NewString(value string) *string {
return &value
}
func main() {
// Prepare the environment
someElement := aastypes.NewProperty(
aastypes.DataTypeDefXSDString,
)
someElement.SetIDShort(
NewString("someProperty"),
)
anotherElement := aastypes.NewProperty(
aastypes.DataTypeDefXSDString,
)
anotherElement.SetIDShort(
NewString("anotherProperty"),
)
yetAnotherElement := aastypes.NewProperty(
aastypes.DataTypeDefXSDString,
)
yetAnotherElement.SetIDShort(
NewString("yetAnotherProperty"),
)
submodel := aastypes.NewSubmodel(
"some-unique-global-identifier",
)
submodel.SetSubmodelElements(
[]aastypes.ISubmodelElement{
someElement,
anotherElement,
yetAnotherElement,
},
)
environment := aastypes.NewEnvironment()
environment.SetSubmodels(
[]aastypes.ISubmodel{
submodel,
},
)
// Iterate using ``Descend``
environment.Descend(
func(that aastypes.IClass) (abort bool) {
if aastypes.IsProperty(that) {
idShort := that.(aastypes.IProperty).IDShort()
if idShort != nil &&
strings.Contains(strings.ToLower(*idShort), "another") {
fmt.Printf("%s\n", *idShort)
}
}
return
},
)
}
(See: Example IterateOverEnvironment)
Iterate over Enumeration Literals
Go does not treat enumerations as collections, but as a fixed tuple of constants. This means that there is no "native" way to iterate over enumerations using for-range-loop.
If the constants are consecutive numbers (using iota
), you can loop through a segment of literals by using for-loop with an incrementing integer.
The enumerations in our SDK are indeed always defined as integers using iota
(see Section design decisions), so that is one option.
Such an incrementing for-loop can be confusing for the reader, and potentially buggy. For example, if the order of your start and end literal in the enumeration ever change in the future, you end up with a non-loop, which is clearly a bug.
To avoid these bugs, we provide LiteralsOf*
slices that you can readily use in your code.
Though Go does not provide a concept of immutability, these slices are meant to be constant.
You should not change them in your code, only read them.
If you want to obtain the string representation of the literal, we provide the [stringification
] package.
The functions stringification.{enumeration name}ToString
give you back the string representation of the literal, and an ok
which is set to false
if the literal was an invalid number.
For the client's convenience, our SDK also implements the functions stringification.Must{enumeration name}ToString
which returns the string representation, or panics.
If you are certain that your code deals with only correct literals, Stringification.must{enumeration name}ToString
will spare you an is-ok check.
For example, see stringification.ModellingKindToString
and stringification.MustModellingKindToString
.
Here is a short example that illustrates how to loop over enumeration literals of the enumeration types.ModelingKind
using the slice types.LiteralsOfModellingKind
:
package main
import (
"fmt"
aastypes "github.com/aas-core-works/aas-core3.0-golang/types"
"strings"
)
// Create a new instance of the `value` and return the pointer to it.
func NewString(value string) *string {
return &value
}
func main() {
for _, literal := range aastypes.LiteralsOfModellingKind {
fmt.Printf(
"Literal as number: %d, literal as string: %s\n",
literal, aasstringification.MustModellingKindToString(literal),
)
}
// Output:
// Literal as number: 0, literal as string: Template
// Literal as number: 1, literal as string: Instance
}
(See: Example IterateOverEnumerationLiterals)
Verify
Our SDK allows you to verify that a model satisfies the constraints of the meta-model.
The verification logic is concentrated in the package verification
, and all it takes is a call to verification.Verify
function.
The function verification.Verify
will check that constraints in the given model element are satisfied, including the recursion into children elements.
You have to pass in a callback function to verification.Verify
which is applied on each reported error.
The verification stops if the callback function ever returns true
(as its only return variable abort
).
This is useful, for example, if you want to report only a certain number of errors.
Here is a short example snippet:
package main
import (
"fmt"
aastypes "github.com/aas-core-works/aas-core3.0-golang/types"
aasverification "github.com/aas-core-works/aas-core3.0-golang/verification"
)
// Create a new instance of the `value` and return the pointer to it.
func NewString(value string) *string {
return &value
}
func main() {
// Prepare the environment
someElement := aastypes.NewProperty(
aastypes.DataTypeDefXSDString,
)
// The ID-shorts must be proper variable names,
// but there is a dash (`-`) in this ID-short.
someElement.SetIDShort(
NewString("some-property"),
)
submodel := aastypes.NewSubmodel(
"some-unique-global-identifier",
)
submodel.SetSubmodelElements(
[]aastypes.ISubmodelElement{
someElement,
},
)
environment := aastypes.NewEnvironment()
environment.SetSubmodels(
[]aastypes.ISubmodel{
submodel,
},
)
// Verify
aasverification.Verify(
environment,
func(err *aasverification.VerificationError) (abort bool) {
fmt.Printf("%s\n", err.Error())
return
},
)
// Output:
// Submodels[0].SubmodelElements[0].IDShort: ID-short of Referables
// shall only feature letters, digits, underscore (``_``); starting mandatory
// with a letter. *I.e.* ``[a-zA-Z][a-zA-Z0-9_]*``.
}
(See: Example Verification)
Omitted Constraints
Not all constraints specified in the meta-model can be verified. Some constraints require external dependencies such as an AAS registry. Verifying the constraints with external dependencies is out-of-scope of our SDK, as we still lack standardized interfaces to those dependencies.
However, all the constraints which need no external dependency are verified.
For a full list of exception, please see the description of the package types
.
JSON de/serialization
Our SDK handles the de/serialization of the AAS models from and to JSON format through the package jsonization
.
Instead of de/serializing to and from strings or arrays of bytes, we de/serialize from JSON-able structures such as map[string]interface{}
or []map[string]interface{}
.
This allows the de/serialization to be more versatile so you are not restricted to JSON, but you can also use JSON-like formats such as YAML or binary JSON.
Serialize to JSON
To serialize, you call the function jsonization.ToJsonable
on an instance of types.IClass
which will convert it to a JSON-able map[string]interface{}
.
Here is a snippet that converts the environment first into a JSON-able object, and next converts the JSON-able object to text:
package main
import (
"encoding/json"
"fmt"
aasjsonization "github.com/aas-core-works/aas-core3.0-golang/jsonization"
aastypes "github.com/aas-core-works/aas-core3.0-golang/types"
)
func main() {
// Create a new instance of the `value` and return the pointer to it.
NewString := func(value string) *string {
return &value
}
// Prepare the environment
someElement := aastypes.NewProperty(
aastypes.DataTypeDefXSDString,
)
someElement.SetIDShort(
NewString("someProperty"),
)
someElement.SetValue(
NewString("some-value"),
)
submodel := aastypes.NewSubmodel(
"some-unique-global-identifier",
)
submodel.SetSubmodelElements(
[]aastypes.ISubmodelElement{
someElement,
},
)
environment := aastypes.NewEnvironment()
environment.SetSubmodels(
[]aastypes.ISubmodel{
submodel,
},
)
// Serialize to jsonable
var jsonable map[string]interface{}
var seriaErr *aasjsonization.SerializationError
jsonable, seriaErr = aasjsonization.ToJsonable(environment)
if seriaErr != nil {
panic(seriaErr.Error())
}
// Serialize jsonable to string
var bb []byte
var err error
bb, err = json.MarshalIndent(jsonable, "", " ")
if err != nil {
panic(err.Error())
}
text := string(bb)
fmt.Println(text)
// Output:
// {
// "submodels": [
// {
// "id": "some-unique-global-identifier",
// "modelType": "Submodel",
// "submodelElements": [
// {
// "idShort": "someProperty",
// "modelType": "Property",
// "value": "some-value",
// "valueType": "xs:string"
// }
// ]
// }
// ]
// }
}
(See: Example JsonizationTo)
De-serialize from JSON
Our SDK can convert a JSON-able structure back to an instance of types.IClass
.
To that end, you call the appropriate function jsonization.{class name}FromJsonable
.
For example, if you want to de-serialize an instance of types.IEnvironment
, call jsonization.EnvironmentFromJsonable
.
Note that the SDK cannot de-serialize classes automatically as the discriminator property modelType
is not included in the serializations for all the classes.
Without the discriminator property provided, we thus cannot know the actual type of the instance just from the serialization.
See this sections on discriminators in AAS Specs for more details.
Here is an example snippet to show you how to de-serialize an instance of types.IEnvironment
:
package main
import (
"encoding/json"
"fmt"
aasjsonization "github.com/aas-core-works/aas-core3.0-golang/jsonization"
aasstringification "github.com/aas-core-works/aas-core3.0-golang/stringification"
aastypes "github.com/aas-core-works/aas-core3.0-golang/types"
)
func main() {
text := `
{
"submodels": [
{
"id": "some-unique-global-identifier",
"modelType": "Submodel",
"submodelElements": [
{
"idShort": "someProperty",
"modelType": "Property",
"value": "some-value",
"valueType": "xs:string"
}
]
}
]
}`
bb := []byte(text)
var jsonable map[string]interface{}
var err error
err = json.Unmarshal(bb, &jsonable)
if err != nil {
panic(err.Error())
}
var environment aastypes.IEnvironment
var deseriaErr *aasjsonization.DeserializationError
environment, deseriaErr = aasjsonization.EnvironmentFromJsonable(
jsonable,
)
if deseriaErr != nil {
panic(deseriaErr.Error())
}
environment.Descend(
func(that aastypes.IClass) (abort bool) {
fmt.Printf(
"%s\n",
aasstringification.MustModelTypeToString(that.ModelType()),
)
return
},
)
// Output:
// Submodel
// Property
}
(See: Example JsonizationFrom)
XML de/serialization
The de/serialization of the AAS models is handled by the package xmlization
.
Serialize to XML
The XML serialization lives in xmlization
package.
We serialize the instances by writing tokens to xml.Encoder
.
While we could immediately return a string or write to a io.Writer
, writing tokens to xml.Encoder
allows you to better steer the format of the output.
For example, you can adjust the indention by calling xml.Encoder.Indent
.
Given an encoder, call the function [xmlization.Marshal
] on it together with your instance of types.IClass
that you want to serialize.
Here is a snippet that serializes an environment to XML:
package main
import (
"encoding/xml"
"fmt"
aastypes "github.com/aas-core-works/aas-core3.0-golang/types"
aasxmlization "github.com/aas-core-works/aas-core3.0-golang/xmlization"
"strings"
)
func main() {
// Create a new instance of the `value` and return the pointer to it.
NewString := func(value string) *string {
return &value
}
// Prepare the environment
someElement := aastypes.NewProperty(
aastypes.DataTypeDefXSDString,
)
someElement.SetIDShort(
NewString("someProperty"),
)
someElement.SetValue(
NewString("some-value"),
)
submodel := aastypes.NewSubmodel(
"some-unique-global-identifier",
)
submodel.SetSubmodelElements(
[]aastypes.ISubmodelElement{
someElement,
},
)
environment := aastypes.NewEnvironment()
environment.SetSubmodels(
[]aastypes.ISubmodel{
submodel,
},
)
// Serialize to XML
builder := new(strings.Builder)
encoder := xml.NewEncoder(builder)
encoder.Indent("", " ")
// We want to include the namespace in the root XML element.
withNamespace := true
var err error
err = aasxmlization.Marshal(encoder, environment, withNamespace)
if err != nil {
panic(err.Error())
}
text := builder.String()
fmt.Println(text)
// Output:
// <environment xmlns="https://admin-shell.io/aas/3/0">
// <submodels>
// <submodel>
// <id>some-unique-global-identifier</id>
// <submodelElements>
// <property>
// <idShort>someProperty</idShort>
// <valueType>xs:string</valueType>
// <value>some-value</value>
// </property>
// </submodelElements>
// </submodel>
// </submodels>
// </environment>
}
(See: Example XmlizationTo)
De-serialize from XML
For efficient one-pass de-serialization, we directly read tokens from xml.Decoder
.
The function xmlization.Unmarshal
de-serializes an instance from the given XML decoder.
Here is an example snippet to show you how to de-serialize an instance of types.IEnvironment
:
package main
import (
"encoding/xml"
"fmt"
aasstringification "github.com/aas-core-works/aas-core3.0-golang/stringification"
aastypes "github.com/aas-core-works/aas-core3.0-golang/types"
aasxmlization "github.com/aas-core-works/aas-core3.0-golang/xmlization"
"strings"
)
func main() {
text := `<environment xmlns="https://admin-shell.io/aas/3/0">
<submodels>
<submodel>
<id>some-unique-global-identifier</id>
<submodelElements>
<property>
<idShort>someProperty</idShort>
<valueType>xs:string</valueType>
<value>some-value</value>
</property>
</submodelElements>
</submodel>
</submodels>
</environment>`
reader := strings.NewReader(text)
decoder := xml.NewDecoder(reader)
var instance aastypes.IClass
var err error
instance, err = aasxmlization.Unmarshal(
decoder,
)
if err != nil {
panic(err.Error())
}
instance.Descend(
func(that aastypes.IClass) (abort bool) {
fmt.Printf(
"%s\n",
aasstringification.MustModelTypeToString(that.ModelType()),
)
return
},
)
// Output:
// Submodel
// Property
}
(See: Example XmlizationFrom)
Enhancing
In any complex application, creating, modifying and de/serializing AAS instances is not enough. You have to insert your custom application-specific data to the model in order for the model to be useful.
Take, for example, parent-child relationship.
The current library ignores it, and there is no easy way for you to find out to which types.ISubmodel
a particular types.ISubmodelElement
belongs to.
We did want to keep the types as simple as possible — the parent-child relationships can get tricky very soon if you have multiple environments with shared submodels etc. Instead of overcomplicating the code and making it barely traceable, we decided to keep it simple and frugal in features.
However, that is little solace if you are developing an GUI editor where you know for sure that there will be only one environment, and where parent-child relationships are crucial for so many tasks. What is more, parent-child relationships are not the only data that need to be intertwined — you probably want history, localized caches etc.
Hashtable?
There are different ways how application-specific data can be synced with the model.
One popular technique is to use Hashtable's and simply map model instances to your custom nuggets of data.
This works well if the data is read-only, and you can spare the cycles for the lookups (which is often acceptable as they run on average in time complexity O(1)
anyhow).
Otherwise, if you need to modify the data, maintaining the consistency between the Hashtable and your nuggets becomes difficult. For example, if you forget to remove the entries from the Hashtable when you remove the instances from the model, you might clog your garbage collector.
Wrapping
Hence, if you modify the data, you need to keep it close to the model instance. In dynamic languages, such as Python and JavaScript, you can simply add your custom fields to the object. This does not work in such a static language like Go.
One solution, usually called Decorator pattern, is to wrap or decorate the instances with your application-specific data. The decorated objects should satisfy both the interface of the original model and provide a way to retrieve your custom nuggets of information.
Writing wrappers for many classes in the AAS meta-model is a tedious task.
We therefore pre-generated the most of the boilerplate code in the package enhancing
.
In the context of decoration, we call your specific data enhancements. First, you need to specify how individual instances are enhanced, i.e. how to produce enhancements for each one of them. We call this an enhancement factory. Second, you need to recursively wrap your instances with the given enhancement factory.
The enhancing is generic and can work with any form of enhancement classes.
You need to specify your enhancement factory as a function which takes an instance of types.IClass
as input and returns either an enhancement, or false
as shouldEnhance
return value if you do not want to enhance the particular instance.
The methods enhancing.Wrap
and enhancing.Unwrap
perform the wrapping and unwrapping, respectively.
The method enhancing.MustUnwrap
is a shortcut method that spares you to write a non-nil check of enhancing.Unwrap
and the related panic if the instance has not been wrapped.
Example: Parent-Child Enhancement
Let us now consider the aforementioned example. We want to keep track of parent-child relationships in a model.
The following code snippets first constructs an environment for illustration.
Then we specify the enhancement such that each instance is initialized with the parent set to nil
.
Finally, we modify the enhancements such that they reflect the parent-child relationships.
package main
import (
"fmt"
aasenhancing "github.com/aas-core-works/aas-core3.0-golang/enhancing"
aastypes "github.com/aas-core-works/aas-core3.0-golang/types"
)
type ParentEnhancement struct {
Parent aastypes.IClass
}
type stack []aastypes.IClass
func (s *stack) Push(v aastypes.IClass) {
*s = append(*s, v)
}
func (s *stack) Pop() aastypes.IClass {
result := (*s)[len(*s)-1]
*s = (*s)[:len(*s)-1]
return result
}
func main() {
// Create a new instance of the `value` and return the pointer to it.
NewString := func(value string) *string {
return &value
}
// Prepare the environment
someElement := aastypes.NewProperty(
aastypes.DataTypeDefXSDString,
)
someElement.SetIDShort(
NewString("someProperty"),
)
someElement.SetValue(
NewString("some-value"),
)
submodel := aastypes.NewSubmodel(
"some-unique-global-identifier",
)
submodel.SetSubmodelElements(
[]aastypes.ISubmodelElement{
someElement,
},
)
var environment aastypes.IEnvironment
environment = aastypes.NewEnvironment()
environment.SetSubmodels(
[]aastypes.ISubmodel{
submodel,
},
)
// Wrap everything
factory := func(that aastypes.IClass) (enh *ParentEnhancement, shouldEnh bool) {
enh = &ParentEnhancement{}
shouldEnh = true
return
}
environment = aasenhancing.Wrap[*ParentEnhancement](
environment,
factory,
).(aastypes.IEnvironment)
// Initialize the parents
var s stack
s.Push(environment)
for len(s) > 0 {
instance := s.Pop()
instance.DescendOnce(
func(child aastypes.IClass) (abort bool) {
enh := aasenhancing.MustUnwrap[*ParentEnhancement](child)
enh.Parent = instance
s.Push(child)
return
},
)
}
// Retrieve the parent of the first submodel
parent := aasenhancing.MustUnwrap[*ParentEnhancement](
environment.Submodels()[0],
).Parent
fmt.Printf("%v\n", parent == environment)
// Output:
// true
}
(See: Example EnhancingParentChild)
Note that this approach is indeed more maintainable than the one with Hashtable, but you still need to take extra care. If you create new submodels and insert them into the environment, you have to make sure that you wrap them appropriately. If you move a submodel from one environment to another, you have to update the parent link manually etc.
Example: Selective Enhancement
We demonstrate now how you can selectively enhance only some instances in an types.IEnvironment
.
For example, let us assign a unique identifier to all instances which are referable. All the other instances are not enhanced.
package main
import (
"fmt"
aasenhancing "github.com/aas-core-works/aas-core3.0-golang/enhancing"
aasstringification "github.com/aas-core-works/aas-core3.0-golang/stringification"
aastypes "github.com/aas-core-works/aas-core3.0-golang/types"
)
type ReferableEnhancement struct {
ID int
}
func main() {
// Create a new instance of the `value` and return the pointer to it.
NewString := func(value string) *string {
return &value
}
// Prepare the environment
someElement := aastypes.NewProperty(
aastypes.DataTypeDefXSDString,
)
someElement.SetIDShort(
NewString("someProperty"),
)
someElement.SetValue(
NewString("some-value"),
)
administrativeInfo := aastypes.NewAdministrativeInformation()
administrativeInfo.SetVersion(
NewString("1.0"),
)
submodel := aastypes.NewSubmodel(
"some-unique-global-identifier",
)
submodel.SetSubmodelElements(
[]aastypes.ISubmodelElement{
someElement,
},
)
submodel.SetAdministration(administrativeInfo)
var environment aastypes.IEnvironment
environment = aastypes.NewEnvironment()
environment.SetSubmodels(
[]aastypes.ISubmodel{
submodel,
},
)
// Wrap everything
nextID := 0
factory := func(that aastypes.IClass) (enh *ReferableEnhancement, shouldEnh bool) {
if aastypes.IsReferable(that) {
enh = &ReferableEnhancement{ID: nextID}
shouldEnh = true
nextID++
}
return
}
environment = aasenhancing.Wrap[*ReferableEnhancement](
environment,
factory,
).(aastypes.IEnvironment)
environment.Descend(
func(that aastypes.IClass) (abort bool) {
enh, ok := aasenhancing.Unwrap[*ReferableEnhancement](that)
if ok {
fmt.Printf(
"%s ID: %d\n",
aasstringification.MustModelTypeToString(that.ModelType()),
enh.ID,
)
} else {
fmt.Printf(
"%s No ID\n",
aasstringification.MustModelTypeToString(that.ModelType()),
)
}
return
},
)
// Output:
// Submodel ID: 0
// AdministrativeInformation No ID
// Property ID: 1
}
(See: Example EnhancingReferable)
No Re-wraps Allowed
We panic on re-wraps of already wrapped instances to avoid costly iterations over the object trees. Additionally, we want to prevent bugs in many settings where the enhancement factory assigns unique identifiers to instances or performs non-idempotent operations.
Please let us know by [creating an issue] if you need re-wraps to be allowed, and please tell us more about your particular scenario.
API
For a detailed documentation of the API, see API documentation.
Design Decisions
We present here some of the choices we made during the design and implementation of the SDK. While it is not necessary to understand our thread of thought to use the SDK, we explain the rationale here behind why we structured and programmed the SDK the way we did. This should hopefully clear up some confusion, or ease the frustration, if you prefer certain features to be implemented differently.
Enumeration Literals as Numbers
We optimize the enumerations for look-ups and comparisons instead of string representation. Thus, we implement literals as numbers (instead of strings). For example, this makes lookups faster as hash values are directly computed on a numeric literal involving usually only a few arithmetic operations. In contrast, if the enumeration literals were listed as strings, the hash value of the literal would need to be computed by iterating through all the characters of the string.
Inheritance Hierarchy
The AAS meta-model uses multiple inheritance. However, Go supports no inheritance.
Instead of multiple inheritance we use interfaces and provide Is*
functions to dynamically decide the instance type at runtime.
All the interfaces inherit from the most general interface types.IClass
.
Please see Section Switch on Runtime Types how you can determine the model type at runtime.
Interface Names
It is common in Go to call the interfaces with an "-er" suffix (Reader
, Writer
, etc.), see the book "Effective Go".
This works well when you write code by hand, and can be creative.
In our setting where the code is generated mostly automatically, we could not easily avoid naming conflicts if we added the suffix "-er" indiscriminately.
Therefore, we opted to call all the interface with the prefix "I-" (IClass
, IEnvironment
etc.).
Contributing Guide
Issues
Please report bugs or feature requests by creating GitHub issues.
In Code
If you want to contribute in code, pull requests are welcome!
Please do create a new issue before you dive into coding. It can well be that we already started working on the feature, or that there are upstream or downstream complexities involved which you might not be aware of.
SDK Code Generation
The biggest part of the code has been automatically generated by aas-core-codegen. It probably makes most sense to change the generator rather than add new functionality. However, this needs to be decided on a case-by-case basis.
Test Code Generation
The code of the unit tests has been automatically generated using the Python scripts in the _dev_scripts/test_codegen/
directory.
To re-generate the test code, first create a virtual environment at the root of the repository:
python -m venv venv
Activate the virtual environment (in Windows):
venv\Scripts\activate
or in Linux:
source venv/bin/activate
Then install the dependencies:
pip3 install -e . _dev_scripts
Now you can run the generation scripts:
python _dev_scripts/test_codegen/generate_all.py
Test Data
The test data is automatically generated by aas-core3.0-testgen, and copied to this repository on every change.
Pull Requests
Feature branches. We develop using the feature branches, see this section of the Git book.
If you are a member of the development team, create a feature branch directly within the repository.
Otherwise, if you are a non-member contributor, fork the repository and create the feature branch in your forked repository. See [this GitHub tuturial] for more guidance.
Branch Prefix.
Please prefix the branch with your Github user name (e.g., mristin/Add-some-feature
).
Continuous Integration. GitHub will run the continuous integration (CI) automatically through GitHub actions. The CI includes checking the formatting, vetting the code, running the tests, etc.
Commit Messages
The commit messages follow the guidelines from https://chris.beams.io/posts/git-commit:
- Separate subject from body with a blank line,
- Limit the subject line to 50 characters,
- Capitalize the subject line,
- Do not end the subject line with a period,
- Use the imperative mood in the subject line,
- Wrap the body at 72 characters, and
- Use the body to explain what and why (instead of how).
Change Log
v1.0.0-rc2 (2023-06-28)
-
Update to aas-core-meta, codegen, testgen 44756fb, 607f65c, bf3720d7 (#9)
This is an important patch propagating pull request 275 in aas-core-meta which affected the constraints and their documentation.
v1.0.0-rc1 (2023-06-23)
- This is the initial version.
Directories ¶
Path | Synopsis |
---|---|
Package aastesting provides the functions and data structures shared across different tests.
|
Package aastesting provides the functions and data structures shared across different tests. |
Package common provides common functions shared among the other packages.
|
Package common provides common functions shared among the other packages. |
Package constants provides immutable values of the meta-model.
|
Package constants provides immutable values of the meta-model. |
Package enhancing allows for enhancement of model instances with your custom data.
|
Package enhancing allows for enhancement of model instances with your custom data. |
Package getting_started provides testable examples for Section "Getting Started" in the Readme.
|
Package getting_started provides testable examples for Section "Getting Started" in the Readme. |
Package jsonization de/serializes model instances to and from JSON.
|
Package jsonization de/serializes model instances to and from JSON. |
Package reporting provides structures and functions for reporting of errors.
|
Package reporting provides structures and functions for reporting of errors. |
Package stringification converts enumerations from and to string representations.
|
Package stringification converts enumerations from and to string representations. |
Package types provides the data structures corresponding to the meta-model.
|
Package types provides the data structures corresponding to the meta-model. |
Package verification allows you to verify model instances.
|
Package verification allows you to verify model instances. |
Package xmlization de/serializes model instances to and from XML.
|
Package xmlization de/serializes model instances to and from XML. |