From e23e198a15c661ae7192b9bf3bb4f4022d1bd9db Mon Sep 17 00:00:00 2001 From: Marko Ristin Date: Wed, 15 May 2024 22:24:45 +0200 Subject: [PATCH] Document the versatility of XML de-serialization (#26) We document in the Readme a couple of recipes how to make the XML de-serialization versatile with the respect to text prefix and XML attributes. Fixes #22, fixes #23. --- README.md | 219 ++++++++++++++++++ getting_started/xmlization_from_file_test.go | 97 ++++++++ .../xmlization_skip_attributes_test.go | 95 ++++++++ 3 files changed, 411 insertions(+) create mode 100644 getting_started/xmlization_from_file_test.go create mode 100644 getting_started/xmlization_skip_attributes_test.go diff --git a/README.md b/README.md index 57b1afb4..76d0fcef 100644 --- a/README.md +++ b/README.md @@ -837,6 +837,225 @@ func main() { (See: [Example XmlizationFrom](https://pkg.go.dev/github.com/aas-core-works/aas-core3.0-golang/getting_started#example-package-XmlizationFrom)) +##### Versatility to Different Sources + +We do not assume to know the source of the XML, and choose to be versatile with handling different sources (wire, file, element embedded in a larger XML document *etc.*). +To that end, we expect that [`xml.Decoder`] already points to the root XML element that you want to de-serialize from. + +For example, if you are decoding from a file that starts with a [Byte Order Mark (BOM)] and an XML declaration, you first need to read those yourself. +Here is an example: + +[Byte Order Mark (BOM)]: https://en.wikipedia.org/wiki/Byte_order_mark + +```go +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" + "log" + "strings" +) + +// TokenReaderSkipDeclaration reads tokens from a reader and +// skips the XML declarations. +type TokenReaderSkipDeclaration struct { + r xml.TokenReader +} + +func NewTokenReaderSkipDeclaration(r xml.TokenReader) *TokenReaderSkipDeclaration { + return &TokenReaderSkipDeclaration{r: r} +} + +func (trsd *TokenReaderSkipDeclaration) Token() (xml.Token, error) { + var token xml.Token + var err error + + for { + token, err = trsd.r.Token() + if err != nil { + return token, err + } + + if _, isProcInst := token.(xml.ProcInst); !isProcInst { + return token, err + } + } +} + +func main() { + // "\uFEFF" is a Byte Order Mark. + text := "\uFEFF" + ` + + + + some-unique-global-identifier + + + someProperty + xs:string + some-value + + + + +` + + reader := strings.NewReader(text) + + // Try to read the Byte Order Mark + var err error + rune, _, err := reader.ReadRune() + if err != nil { + log.Fatal(err) + } + if rune != '\uFEFF' { + reader.UnreadRune() + } + + decoder := xml.NewTokenDecoder( + NewTokenReaderSkipDeclaration( + xml.NewDecoder(reader), + ), + ) + + var instance aastypes.IClass + 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 XmlizationFromTextWithBOMAndXMLDeclaration](https://pkg.go.dev/github.com/aas-core-works/aas-core3.0-golang/getting_started#example-package-XmlizationFromTextWithBOMAndXMLDeclaration)) + +##### No Attributes Expected + +The XML specification of the AAS meta-model expects no attributes in the XML elements. +Consequently, we follow the specification and throw an error if there are any XML attributes present in a start element. + +If there are attributes in your XML documents, make sure you wrap the [`xml.TokenReader`] to undo them on the fly. +Here is an example snippet: + +```go +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" +) + +// TokenReaderNoAttributes reads tokens from a reader and +// removes any attributes in the start elements. +type TokenReaderNoAttributes struct { + r xml.TokenReader +} + +func NewTokenReaderNoAttributes(r xml.TokenReader) *TokenReaderNoAttributes { + return &TokenReaderNoAttributes{r: r} +} + +func (trna *TokenReaderNoAttributes) Token() (xml.Token, error) { + var token xml.Token + var err error + + for { + token, err = trna.r.Token() + if err != nil { + return token, err + } + + var startElement xml.StartElement + startElement, isStartElement := token.(xml.StartElement) + + if !isStartElement { + return token, err + } + + startElement.Attr = []xml.Attr{} + return startElement, err + } +} + +func main() { + text := ` + + + some-unique-global-identifier + + + someProperty + xs:string + some-value + + + + +` + + reader := strings.NewReader(text) + + // Use a decoder which skips the XML declarations + decoder := xml.NewTokenDecoder( + NewTokenReaderNoAttributes( + 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 XmlizationSkipAttributes](https://pkg.go.dev/github.com/aas-core-works/aas-core3.0-golang/getting_started#example-package-XmlizationSkipAttributes)) + ### Enhancing In any complex application, creating, modifying and de/serializing AAS instances is not enough. diff --git a/getting_started/xmlization_from_file_test.go b/getting_started/xmlization_from_file_test.go new file mode 100644 index 00000000..0cdc09b7 --- /dev/null +++ b/getting_started/xmlization_from_file_test.go @@ -0,0 +1,97 @@ +package getting_started_test + +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" + "log" + "strings" +) + +// TokenReaderSkipDeclaration reads tokens from a reader and +// skips the XML declarations. +type TokenReaderSkipDeclaration struct { + r xml.TokenReader +} + +func NewTokenReaderSkipDeclaration(r xml.TokenReader) *TokenReaderSkipDeclaration { + return &TokenReaderSkipDeclaration{r: r} +} + +func (trsd *TokenReaderSkipDeclaration) Token() (xml.Token, error) { + var token xml.Token + var err error + + for { + token, err = trsd.r.Token() + if err != nil { + return token, err + } + + if _, isProcInst := token.(xml.ProcInst); !isProcInst { + return token, err + } + } +} + +func Example_xmlizationFromTextWithBOMAndXMLDeclaration() { + // "\uFEFF" is a Byte Order Mark. + text := "\uFEFF" + ` + + + + some-unique-global-identifier + + + someProperty + xs:string + some-value + + + + +` + + reader := strings.NewReader(text) + + // Try to read the Byte Order Mark + var err error + rune, _, err := reader.ReadRune() + if err != nil { + log.Fatal(err) + } + if rune != '\uFEFF' { + reader.UnreadRune() + } + + // Use a decoder which skips the XML declarations + decoder := xml.NewTokenDecoder( + NewTokenReaderSkipDeclaration( + xml.NewDecoder(reader), + ), + ) + + var instance aastypes.IClass + 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 +} diff --git a/getting_started/xmlization_skip_attributes_test.go b/getting_started/xmlization_skip_attributes_test.go new file mode 100644 index 00000000..a181bfb6 --- /dev/null +++ b/getting_started/xmlization_skip_attributes_test.go @@ -0,0 +1,95 @@ +package getting_started_test + +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" +) + +// TokenReaderNoAttributes reads tokens from a reader and +// removes any attributes in the start elements. +type TokenReaderNoAttributes struct { + r xml.TokenReader +} + +func NewTokenReaderNoAttributes(r xml.TokenReader) *TokenReaderNoAttributes { + return &TokenReaderNoAttributes{r: r} +} + +func (trna *TokenReaderNoAttributes) Token() (xml.Token, error) { + var token xml.Token + var err error + + for { + token, err = trna.r.Token() + if err != nil { + return token, err + } + + var startElement xml.StartElement + startElement, isStartElement := token.(xml.StartElement) + + if !isStartElement { + return token, err + } + + startElement.Attr = []xml.Attr{} + return startElement, err + } +} + +func Example_xmlizationSkipAttributes() { + text := ` + + + some-unique-global-identifier + + + someProperty + xs:string + some-value + + + + +` + + reader := strings.NewReader(text) + + // Use a decoder which skips the XML declarations + decoder := xml.NewTokenDecoder( + NewTokenReaderNoAttributes( + 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 +}