From 6368d434e8e07139a8c5e6979efc4a0d7290af59 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 10:54:39 +0000 Subject: [PATCH 01/63] Add introduction --- docs/articles/intro.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/articles/intro.md diff --git a/docs/articles/intro.md b/docs/articles/intro.md new file mode 100644 index 0000000..65c713f --- /dev/null +++ b/docs/articles/intro.md @@ -0,0 +1,7 @@ +--- +uid: intro +--- + +## What is Bonsai.Sgen? + +`Bonsai.Sgen` is a code generator tool for the [Bonsai programming language](https://bonsai-rx.org/). It leverages [`json-schema`](https://json-schema.org/) as a standard to represent [record-like](https://en.wikipedia.org/wiki/Record_(computer_science)) structures, and automatically generates Bonsai-compatible isomorphic operators to create and manipulate these objects. From 44cc76558ea97b72c6900380b11a14979d4ebf5a Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 10:54:47 +0000 Subject: [PATCH 02/63] Add scaffold for articles --- docs/articles/advanced-usage.md | 0 docs/articles/basic-usage.md | 0 docs/articles/experimental.md | 0 docs/articles/getting-started.md | 101 +++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+) create mode 100644 docs/articles/advanced-usage.md create mode 100644 docs/articles/basic-usage.md create mode 100644 docs/articles/experimental.md create mode 100644 docs/articles/getting-started.md diff --git a/docs/articles/advanced-usage.md b/docs/articles/advanced-usage.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/articles/experimental.md b/docs/articles/experimental.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/articles/getting-started.md b/docs/articles/getting-started.md new file mode 100644 index 0000000..330f91a --- /dev/null +++ b/docs/articles/getting-started.md @@ -0,0 +1,101 @@ +--- +uid: getting-started +--- + +## Why should I care? + +`Bonsai.Sgen` attempts to solve the problem of writing boilerplate code to create represent data structures in Bonsai. Let's try to convince you by looking at a simple example. + +Let's we have a simple record-like object that represents a ´Person´: + + +| Field Name | Type | Description | +|------------|----------|---------------------------| +| age | int | The age of a person | +| first_name | string | The first name of the person | +| last_name | string | The last name of the person | +| dob | datetime | Date of birth | + + +If we want to represent this object in Bonsai, we have a few alternatives: + +1. Using a `DynamicClass` object: + +:::workflow +![Person as DynamicClass](~/workflows/person-example-dynamic-class.bonsai) +::: + +This approach is rather brittle as the representation of the record does not exist as a "first class citizen" and only at compile-time. This has a few implications one of which is the inability to create [Subject Sources](https://bonsai-rx.org/docs/articles/subjects.html#source-subjects) from the type. + +2. Modeling the object as a C# class using [Scripting Extensions](https://bonsai-rx.org/docs/articles/scripting-extensions.html): + +```Csharp +public class Person +{ + public int Age; + public string FirstName; + public string LastName; + public DateTime DOB; +} +``` + +This approach is more robust than the previous one, but it requires writing additional, boilerplate code to allow the creation of the object in Bonsai: + +```Csharp +using Bonsai; +using System; +using System.Reactive.Linq; + +public class CreatePerson : Source +{ + + public int Age { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public DateTime DOB { get; set; } + + public override IObservable Generate() + { + return Observable.Return(new Person + { + Age = Age, + FirstName = FirstName, + LastName = LastName, + DOB = DOB + }); + } +} +``` + +As you can probably tell, neither of these approaches is ideal when it comes to scale large projects. This is where `Bonsai.Sgen` comes in. + + +## Automatic generation of Bonsai code using Bonsai.Sgen + +We will expand this example later on, but for now, let's see how we can use `Bonsai.Sgen` to automatically generate the Bonsai code for the `Person` object. + +First, we need to define the schema of the object in a JSON file: + +```json +{ + "name": "Person", + "fields": [ + { + "name": "Age", + "type": "int", + }, + { + "name": "FirstName", + "type": "string", + }, + { + "name": "LastName", + "type": "string", + }, + { + "name": "DOB", + "type": "datetime", + } + ] +} +``` \ No newline at end of file From 38bb32803ccefb4aa64de2ff61d40107a27714e6 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 10:54:58 +0000 Subject: [PATCH 03/63] Add Extensions to example workflow --- docs/workflows/Extensions.csproj | 11 ++++++++ docs/workflows/Extensions/CreatePerson.cs | 31 +++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 docs/workflows/Extensions.csproj create mode 100644 docs/workflows/Extensions/CreatePerson.cs diff --git a/docs/workflows/Extensions.csproj b/docs/workflows/Extensions.csproj new file mode 100644 index 0000000..250d9b7 --- /dev/null +++ b/docs/workflows/Extensions.csproj @@ -0,0 +1,11 @@ + + + + net472 + + + + + + + \ No newline at end of file diff --git a/docs/workflows/Extensions/CreatePerson.cs b/docs/workflows/Extensions/CreatePerson.cs new file mode 100644 index 0000000..f0eb019 --- /dev/null +++ b/docs/workflows/Extensions/CreatePerson.cs @@ -0,0 +1,31 @@ +using Bonsai; +using System; +using System.Reactive.Linq; + +public class CreatePerson : Source +{ + + public int Age { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public DateTime DOB { get; set; } + + public override IObservable Generate() + { + return Observable.Return(new Person + { + Age = Age, + FirstName = FirstName, + LastName = LastName, + DOB = DOB + }); + } +} + +public class Person +{ + public int Age; + public string FirstName; + public string LastName; + public DateTime DOB; +} From e6f82af7648958adecdb50dc480b9cef37600751 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 10:55:14 +0000 Subject: [PATCH 04/63] Add dynamic-class example --- .../person-example-dynamic-class.bonsai | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 docs/workflows/person-example-dynamic-class.bonsai diff --git a/docs/workflows/person-example-dynamic-class.bonsai b/docs/workflows/person-example-dynamic-class.bonsai new file mode 100644 index 0000000..e6ddc50 --- /dev/null +++ b/docs/workflows/person-example-dynamic-class.bonsai @@ -0,0 +1,45 @@ + + + + + + + 0 + + + + + + + + + + + 0001-01-01T00:00:00 + + + + + + + new( +Item1 as Age, +Item2 as FirstName, +Item3 as LastName, +Item4 as DOB +) + + + + + + + + + + + \ No newline at end of file From 63306f8174abb705a7c4b83586c811c8a1046615 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 10:59:29 +0000 Subject: [PATCH 05/63] Ignore layout files inside workflow examples --- docs/workflows/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/workflows/.gitignore diff --git a/docs/workflows/.gitignore b/docs/workflows/.gitignore new file mode 100644 index 0000000..dfc8fe5 --- /dev/null +++ b/docs/workflows/.gitignore @@ -0,0 +1 @@ +*.bonsai.layout \ No newline at end of file From ecac0c9c67304bcfedd46028bc13c839a85444f6 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 11:00:36 +0000 Subject: [PATCH 06/63] Add getting started instructions --- docs/articles/intro.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/articles/intro.md b/docs/articles/intro.md index 65c713f..85d8603 100644 --- a/docs/articles/intro.md +++ b/docs/articles/intro.md @@ -5,3 +5,41 @@ uid: intro ## What is Bonsai.Sgen? `Bonsai.Sgen` is a code generator tool for the [Bonsai programming language](https://bonsai-rx.org/). It leverages [`json-schema`](https://json-schema.org/) as a standard to represent [record-like](https://en.wikipedia.org/wiki/Record_(computer_science)) structures, and automatically generates Bonsai-compatible isomorphic operators to create and manipulate these objects. + +## Getting started + +1. Navigate to the [Bonsai.Sgen NuGet tool package](https://www.nuget.org/packages/Bonsai.Sgen/) +2. Click `.NET CLI (Local)` and copy the two suggested commands. E.g.: + + ```cmd + dotnet new tool-manifest # if you are setting up this repo + dotnet tool install --local Bonsai.Sgen + ``` + +3. To view the tool help reference documentation, run: + + ```cmd + dotnet bonsai.sgen --help + ``` + +4. To generate YAML serialization classes from a schema file: + + ```cmd + dotnet bonsai.sgen --schema schema.json --serializer YamlDotNet + ``` + +5. To generate JSON serialization classes from a schema file: + + ```cmd + dotnet bonsai.sgen --schema schema.json --serializer NewtonsoftJson + ``` + +6. Copy the generated class file to your project `Extensions` folder. + +7. Add the necessary package references to your `Extensions.csproj` file. For example: + + ```xml + + + + \ No newline at end of file From c078d8cbbe6ce9f31aaa531481ddc5f2eace8d2e Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 11:06:29 +0000 Subject: [PATCH 07/63] Add Yaml and Json package references to Extensions csproj --- docs/workflows/Extensions.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/workflows/Extensions.csproj b/docs/workflows/Extensions.csproj index 250d9b7..8d17170 100644 --- a/docs/workflows/Extensions.csproj +++ b/docs/workflows/Extensions.csproj @@ -6,6 +6,8 @@ + + \ No newline at end of file From 7a58ec64534bb8617e4e684917c00d240ef012b5 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 11:08:36 +0000 Subject: [PATCH 08/63] Change title --- docs/articles/{getting-started.md => why-bonsai-sgen.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/articles/{getting-started.md => why-bonsai-sgen.md} (99%) diff --git a/docs/articles/getting-started.md b/docs/articles/why-bonsai-sgen.md similarity index 99% rename from docs/articles/getting-started.md rename to docs/articles/why-bonsai-sgen.md index 330f91a..da779dd 100644 --- a/docs/articles/getting-started.md +++ b/docs/articles/why-bonsai-sgen.md @@ -1,5 +1,5 @@ --- -uid: getting-started +uid: why-bonsai-sgen --- ## Why should I care? From 52c416d79fe4d6e01122d95d4f8d3be37d784359 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 11:11:52 +0000 Subject: [PATCH 09/63] Add environment package requirements --- .bonsai/Bonsai.config | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.bonsai/Bonsai.config b/.bonsai/Bonsai.config index ac40c8a..3db0977 100644 --- a/.bonsai/Bonsai.config +++ b/.bonsai/Bonsai.config @@ -5,15 +5,19 @@ + + + + @@ -25,19 +29,25 @@ + + + + + + From 192edb51a211896d7e50367092d6c897055c2b08 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 11:20:18 +0000 Subject: [PATCH 10/63] Add short sgen example --- docs/articles/why-bonsai-sgen.md | 54 ++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/docs/articles/why-bonsai-sgen.md b/docs/articles/why-bonsai-sgen.md index da779dd..938e6fa 100644 --- a/docs/articles/why-bonsai-sgen.md +++ b/docs/articles/why-bonsai-sgen.md @@ -78,24 +78,38 @@ First, we need to define the schema of the object in a JSON file: ```json { - "name": "Person", - "fields": [ - { - "name": "Age", - "type": "int", - }, - { - "name": "FirstName", - "type": "string", - }, - { - "name": "LastName", - "type": "string", - }, - { - "name": "DOB", - "type": "datetime", - } - ] + "title": "Person", + "type": "object", + "properties": { + "Age": { + "type": "integer" + }, + "FirstName": { + "type": "string" + }, + "LastName": { + "type": "string" + }, + "DOB": { + "type": "string", + "format": "date-time" + } + } } -``` \ No newline at end of file +``` + +Second, we need to run the `Bonsai.Sgen` tool to generate the Bonsai code: + +```cmd +dotnet bonsai.sgen --schema docs/workflows/person.json --output docs/workflows/Extensions/PersonSgen.cs +``` + +Finally, we can use the generated code in our Bonsai workflow: + +:::workflow +![Person as BonsaiSgen](~/workflows/person-example-bonsai-sgen.bonsai) +::: + +As you can probably tell, the `Bonsai.Sgen` approach is much more concise and less error-prone than the previous ones. It allows you to focus on the data structure itself and not on the boilerplate code required to create it in Bonsai. Moreover, as we will see later, the tool also automatically generates serialization and deserialization boilerplate code for the object, which can be very useful when working with external data sources. + +Finally, if one considers the `json-schema` as the "source of truth" for the data structure representation, it is possible to generate multiple representations of the object in different languages, ensuring interoperability. This can be very useful when working in a multi-language environment (e.g. running experiment in Bonsai and analysis in Python) and when sharing data structures across different projects. \ No newline at end of file From 9ce3162fdd74ae2fb03b48625583b9ffbc824d63 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 11:20:33 +0000 Subject: [PATCH 11/63] Add person json schema and generated bonsai classes --- docs/workflows/Extensions/PersonSgen.cs | 196 ++++++++++++++++++++++++ docs/workflows/person.json | 19 +++ 2 files changed, 215 insertions(+) create mode 100644 docs/workflows/Extensions/PersonSgen.cs create mode 100644 docs/workflows/person.json diff --git a/docs/workflows/Extensions/PersonSgen.cs b/docs/workflows/Extensions/PersonSgen.cs new file mode 100644 index 0000000..add8fe9 --- /dev/null +++ b/docs/workflows/Extensions/PersonSgen.cs @@ -0,0 +1,196 @@ +//---------------------- +// +// Generated using the NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.0.0) (http://NJsonSchema.org) +// +//---------------------- + + +namespace DataSchema +{ + #pragma warning disable // Disable all warnings + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class Person + { + + private int _age; + + private string _firstName; + + private string _lastName; + + private System.DateTimeOffset _dOB; + + public Person() + { + } + + protected Person(Person other) + { + _age = other._age; + _firstName = other._firstName; + _lastName = other._lastName; + _dOB = other._dOB; + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Age")] + public int Age + { + get + { + return _age; + } + set + { + _age = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="FirstName")] + public string FirstName + { + get + { + return _firstName; + } + set + { + _firstName = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="LastName")] + public string LastName + { + get + { + return _lastName; + } + set + { + _lastName = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="DOB")] + public System.DateTimeOffset DOB + { + get + { + return _dOB; + } + set + { + _dOB = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Person(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new Person(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("Age = " + _age + ", "); + stringBuilder.Append("FirstName = " + _firstName + ", "); + stringBuilder.Append("LastName = " + _lastName + ", "); + stringBuilder.Append("DOB = " + _dOB); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + /// + /// Serializes a sequence of data model objects into YAML strings. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Serializes a sequence of data model objects into YAML strings.")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + public partial class SerializeToYaml + { + + private System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.SerializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => serializer.Serialize(value)); + }); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + } + + + /// + /// Deserializes a sequence of YAML strings into data model objects. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Deserializes a sequence of YAML strings into data model objects.")] + [System.ComponentModel.DefaultPropertyAttribute("Type")] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + public partial class DeserializeFromYaml : Bonsai.Expressions.SingleArgumentExpressionBuilder + { + + public DeserializeFromYaml() + { + Type = new Bonsai.Expressions.TypeMapping(); + } + + public Bonsai.Expressions.TypeMapping Type { get; set; } + + public override System.Linq.Expressions.Expression Build(System.Collections.Generic.IEnumerable arguments) + { + var typeMapping = (Bonsai.Expressions.TypeMapping)Type; + var returnType = typeMapping.GetType().GetGenericArguments()[0]; + return System.Linq.Expressions.Expression.Call( + typeof(DeserializeFromYaml), + "Process", + new System.Type[] { returnType }, + System.Linq.Enumerable.Single(arguments)); + } + + private static System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.DeserializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => + { + var reader = new System.IO.StringReader(value); + var parser = new YamlDotNet.Core.MergingParser(new YamlDotNet.Core.Parser(reader)); + return serializer.Deserialize(parser); + }); + }); + } + } +} \ No newline at end of file diff --git a/docs/workflows/person.json b/docs/workflows/person.json new file mode 100644 index 0000000..ce4f2c3 --- /dev/null +++ b/docs/workflows/person.json @@ -0,0 +1,19 @@ +{ + "title": "Person", + "type": "object", + "properties": { + "Age": { + "type": "integer" + }, + "FirstName": { + "type": "string" + }, + "LastName": { + "type": "string" + }, + "DOB": { + "type": "string", + "format": "date-time" + } + } +} From d436f823654453f6a63bde24d8bb355df03e590e Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 12:07:33 +0000 Subject: [PATCH 12/63] Move section --- docs/articles/why-bonsai-sgen.md | 47 +------------------------------- 1 file changed, 1 insertion(+), 46 deletions(-) diff --git a/docs/articles/why-bonsai-sgen.md b/docs/articles/why-bonsai-sgen.md index 938e6fa..1fb2d7a 100644 --- a/docs/articles/why-bonsai-sgen.md +++ b/docs/articles/why-bonsai-sgen.md @@ -67,49 +67,4 @@ public class CreatePerson : Source } ``` -As you can probably tell, neither of these approaches is ideal when it comes to scale large projects. This is where `Bonsai.Sgen` comes in. - - -## Automatic generation of Bonsai code using Bonsai.Sgen - -We will expand this example later on, but for now, let's see how we can use `Bonsai.Sgen` to automatically generate the Bonsai code for the `Person` object. - -First, we need to define the schema of the object in a JSON file: - -```json -{ - "title": "Person", - "type": "object", - "properties": { - "Age": { - "type": "integer" - }, - "FirstName": { - "type": "string" - }, - "LastName": { - "type": "string" - }, - "DOB": { - "type": "string", - "format": "date-time" - } - } -} -``` - -Second, we need to run the `Bonsai.Sgen` tool to generate the Bonsai code: - -```cmd -dotnet bonsai.sgen --schema docs/workflows/person.json --output docs/workflows/Extensions/PersonSgen.cs -``` - -Finally, we can use the generated code in our Bonsai workflow: - -:::workflow -![Person as BonsaiSgen](~/workflows/person-example-bonsai-sgen.bonsai) -::: - -As you can probably tell, the `Bonsai.Sgen` approach is much more concise and less error-prone than the previous ones. It allows you to focus on the data structure itself and not on the boilerplate code required to create it in Bonsai. Moreover, as we will see later, the tool also automatically generates serialization and deserialization boilerplate code for the object, which can be very useful when working with external data sources. - -Finally, if one considers the `json-schema` as the "source of truth" for the data structure representation, it is possible to generate multiple representations of the object in different languages, ensuring interoperability. This can be very useful when working in a multi-language environment (e.g. running experiment in Bonsai and analysis in Python) and when sharing data structures across different projects. \ No newline at end of file +As you can probably tell, neither of these approaches is ideal when it comes to scale large projects. This is where `Bonsai.Sgen` comes in. \ No newline at end of file From 6f573ca22c3b3ee2f1843b5dad411b3915023ba9 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 12:07:51 +0000 Subject: [PATCH 13/63] Add usage examples --- docs/articles/basic-usage.md | 141 +++++++ docs/workflows/Extensions/PersonAndDogSgen.cs | 383 ++++++++++++++++++ docs/workflows/Extensions/PersonAndPetEnum.cs | 303 ++++++++++++++ docs/workflows/person-and-pet-enum.bonsai | 9 + .../person-example-bonsai-sgen.bonsai | 16 + docs/workflows/person_and_dog.json | 48 +++ docs/workflows/person_and_pet_enum.json | 38 ++ .../personand-dog-example-bonsai-sgen.bonsai | 21 + ...example-nested-building-bonsai-sgen.bonsai | 55 +++ 9 files changed, 1014 insertions(+) create mode 100644 docs/workflows/Extensions/PersonAndDogSgen.cs create mode 100644 docs/workflows/Extensions/PersonAndPetEnum.cs create mode 100644 docs/workflows/person-and-pet-enum.bonsai create mode 100644 docs/workflows/person-example-bonsai-sgen.bonsai create mode 100644 docs/workflows/person_and_dog.json create mode 100644 docs/workflows/person_and_pet_enum.json create mode 100644 docs/workflows/personand-dog-example-bonsai-sgen.bonsai create mode 100644 docs/workflows/personand-dog-example-nested-building-bonsai-sgen.bonsai diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index e69de29..44fc347 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -0,0 +1,141 @@ +--- +uid: basic-usage +--- + + +## Automatic generation of Bonsai code using Bonsai.Sgen + +We will expand this example later on, but for now, let's see how we can use `Bonsai.Sgen` to automatically generate the Bonsai code for the `Person` object. + +First, we need to define the schema of the object in a JSON file: + +[person](~/workflows/person.json) + +```json +{ + "title": "Person", + "type": "object", + "properties": { + "Age": { "type": "integer" }, + "FirstName": { "type": "string" }, + "LastName": { "type": "string" }, + "DOB": { "type": "string", "format": "date-time" } + } +} +``` + +Second, we need to run the `Bonsai.Sgen` tool to generate the Bonsai code: + +```cmd +dotnet bonsai.sgen --schema docs/workflows/person.json --output docs/workflows/Extensions/PersonSgen.cs +``` + +Finally, we can use the generated code in our Bonsai workflow: + +:::workflow +![Person as BonsaiSgen](~/workflows/person-example-bonsai-sgen.bonsai) +::: + +As you can probably tell, the `Bonsai.Sgen` approach is much more concise and less error-prone than the previous ones. It allows you to focus on the data structure itself and not on the boilerplate code required to create it in Bonsai. Moreover, as we will see later, the tool also automatically generates serialization and deserialization boilerplate code for the object, which can be very useful when working with external data sources. + +Finally, if one considers the `json-schema` as the "source of truth" for the data structure representation, it is possible to generate multiple representations of the object in different languages, ensuring interoperability. This can be very useful when working in a multi-language environment (e.g. running experiment in Bonsai and analysis in Python) and when sharing data structures across different projects. + +## Multiple objects + +The previous example shows how a single record can be modelled. In practice, projects often require modelling different types of objects. This is where `Bonsai.Sgen` shines, as it allows you to generate multiple objects from a single schema file: + +[person_and_dog](~/workflows/person_and_dog.json) + +```json +{ + "title": "PersonAndPet", + "definitions": { + "Person": { + "title": "Person", + "type": "object", + "properties": { + "Age": { "type": "integer" }, + "FirstName": { "type": "string" }, + "LastName": { "type": "string" }, + "DOB": { "type": "string", "format": "date-time" } + } + }, + "Dog": { + "title": "Dog", + "type": "object", + "properties": { + "Name": { "type": "string" }, + "Breed": { "type": "string" }, + "Age": { "type": "integer" } + } + } + }, + "type": "object", + "properties": { + "owner": { "$ref": "#/definitions/Person" }, + "pet": { "$ref": "#/definitions/Dog" } + } +} +``` + +```cmd +dotnet bonsai.sgen --schema docs/workflows/person_and_dog.json --output docs/workflows/Extensions/PersonAndDogSgen.cs --namespace PersonAndDog +``` + +:::workflow +![PersonAndDog as BonsaiSgen](~/workflows/person-and-dog-example-bonsai-sgen.bonsai) +::: + +A few things worth noting in this example: + +- The schema file contains two definitions: `Person` and `Dog` that give rise to two operators (`Person` and `Dog`) in the generated code. +- A third definition `PersonAndPet` is used to combine the two objects into a single record. This can be omitted as we will see later by using the `x-abstract` property. +- The `--namespace` flag is used to specify the namespace of the generated code. This is useful to prevent name clashes between different schemas (e.g. `PersonAndDog.Person` and `Person` from the previous example). +- Both `Person` and `Dog` `json-schema` objects are passed as references. This is extremely important as it allows the reuse of the same definition in multiple places. This is particularly useful when working with complex data structures that share common fields. If definitions are passed in-line (i.e. redefined each time), Bonsai.Sgen may not be able to correctly identify them as the same object. + +## Nested objects + +The previous example highlights the simplicity of generating Bonsai code for simple objects. However, the real power of `Bonsai.Sgen` comes when dealing with more complex data structures, such as nested objects. Moreover, Bonsai syntax lends itself quite nicely to represent these nested structures as well as build them: + + +:::workflow +![PersonAndDog building as BonsaiSgen](~/workflows/person-and-dog-nested-building-example-bonsai-sgen.bonsai) +::: + +## Enums + +`Bonsai.Sgen` also supports the generation of enums using the `enum` type in the `json-schema`: + +We can replace the `Pet` object in the previous example with an [`enum`](https://json-schema.org/understanding-json-schema/reference/enum): +[Pet as Enum](~/workflows/person_and_pet_enum.json). + +```json +(...) + { + "Pet": { + "title": "Pet", + "type": "string", + "enum": ["Dog", "Cat", "Fish", "Bird", "Reptile"] + } + }, + "type": "object", + "properties": { + "owner": {"$ref": "#/definitions/Person"}, + "pet": {"$ref": "#/definitions/Pet"} + } +``` + +> [!TIP] +> In certain cases, it may be useful to use `x-enum-names` to specify the rendered names of the enum values. +> Example: +> +> ```json +> { +> "MyIntEnum": { +> "enum": [0, 1, 2, 3, 4], +> "title": "MyIntEnum", +> "type": "integer", +> "x-enumNames": ["None", "One", "Two", "Three", "Four"] +> } +> } +> ``` diff --git a/docs/workflows/Extensions/PersonAndDogSgen.cs b/docs/workflows/Extensions/PersonAndDogSgen.cs new file mode 100644 index 0000000..022b9a6 --- /dev/null +++ b/docs/workflows/Extensions/PersonAndDogSgen.cs @@ -0,0 +1,383 @@ +//---------------------- +// +// Generated using the NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.0.0) (http://NJsonSchema.org) +// +//---------------------- + + +namespace PersonAndDog +{ + #pragma warning disable // Disable all warnings + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class Person + { + + private int _age; + + private string _firstName; + + private string _lastName; + + private System.DateTimeOffset _dOB; + + public Person() + { + } + + protected Person(Person other) + { + _age = other._age; + _firstName = other._firstName; + _lastName = other._lastName; + _dOB = other._dOB; + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Age")] + public int Age + { + get + { + return _age; + } + set + { + _age = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="FirstName")] + public string FirstName + { + get + { + return _firstName; + } + set + { + _firstName = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="LastName")] + public string LastName + { + get + { + return _lastName; + } + set + { + _lastName = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="DOB")] + public System.DateTimeOffset DOB + { + get + { + return _dOB; + } + set + { + _dOB = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Person(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new Person(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("Age = " + _age + ", "); + stringBuilder.Append("FirstName = " + _firstName + ", "); + stringBuilder.Append("LastName = " + _lastName + ", "); + stringBuilder.Append("DOB = " + _dOB); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class Dog + { + + private string _name; + + private string _breed; + + private int _age; + + public Dog() + { + } + + protected Dog(Dog other) + { + _name = other._name; + _breed = other._breed; + _age = other._age; + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Name")] + public string Name + { + get + { + return _name; + } + set + { + _name = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Breed")] + public string Breed + { + get + { + return _breed; + } + set + { + _breed = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Age")] + public int Age + { + get + { + return _age; + } + set + { + _age = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Dog(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new Dog(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("Name = " + _name + ", "); + stringBuilder.Append("Breed = " + _breed + ", "); + stringBuilder.Append("Age = " + _age); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class PersonAndPet + { + + private Person _owner; + + private Dog _pet; + + public PersonAndPet() + { + } + + protected PersonAndPet(PersonAndPet other) + { + _owner = other._owner; + _pet = other._pet; + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="owner")] + public Person Owner + { + get + { + return _owner; + } + set + { + _owner = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="pet")] + public Dog Pet + { + get + { + return _pet; + } + set + { + _pet = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new PersonAndPet(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new PersonAndPet(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("owner = " + _owner + ", "); + stringBuilder.Append("pet = " + _pet); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + /// + /// Serializes a sequence of data model objects into YAML strings. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Serializes a sequence of data model objects into YAML strings.")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + public partial class SerializeToYaml + { + + private System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.SerializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => serializer.Serialize(value)); + }); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + } + + + /// + /// Deserializes a sequence of YAML strings into data model objects. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Deserializes a sequence of YAML strings into data model objects.")] + [System.ComponentModel.DefaultPropertyAttribute("Type")] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + public partial class DeserializeFromYaml : Bonsai.Expressions.SingleArgumentExpressionBuilder + { + + public DeserializeFromYaml() + { + Type = new Bonsai.Expressions.TypeMapping(); + } + + public Bonsai.Expressions.TypeMapping Type { get; set; } + + public override System.Linq.Expressions.Expression Build(System.Collections.Generic.IEnumerable arguments) + { + var typeMapping = (Bonsai.Expressions.TypeMapping)Type; + var returnType = typeMapping.GetType().GetGenericArguments()[0]; + return System.Linq.Expressions.Expression.Call( + typeof(DeserializeFromYaml), + "Process", + new System.Type[] { returnType }, + System.Linq.Enumerable.Single(arguments)); + } + + private static System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.DeserializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => + { + var reader = new System.IO.StringReader(value); + var parser = new YamlDotNet.Core.MergingParser(new YamlDotNet.Core.Parser(reader)); + return serializer.Deserialize(parser); + }); + }); + } + } +} \ No newline at end of file diff --git a/docs/workflows/Extensions/PersonAndPetEnum.cs b/docs/workflows/Extensions/PersonAndPetEnum.cs new file mode 100644 index 0000000..d1bac00 --- /dev/null +++ b/docs/workflows/Extensions/PersonAndPetEnum.cs @@ -0,0 +1,303 @@ +//---------------------- +// +// Generated using the NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.0.0) (http://NJsonSchema.org) +// +//---------------------- + + +namespace PersonAndPetEnum +{ + #pragma warning disable // Disable all warnings + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class Person + { + + private int _age; + + private string _firstName; + + private string _lastName; + + private System.DateTimeOffset _dOB; + + public Person() + { + } + + protected Person(Person other) + { + _age = other._age; + _firstName = other._firstName; + _lastName = other._lastName; + _dOB = other._dOB; + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Age")] + public int Age + { + get + { + return _age; + } + set + { + _age = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="FirstName")] + public string FirstName + { + get + { + return _firstName; + } + set + { + _firstName = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="LastName")] + public string LastName + { + get + { + return _lastName; + } + set + { + _lastName = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="DOB")] + public System.DateTimeOffset DOB + { + get + { + return _dOB; + } + set + { + _dOB = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Person(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new Person(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("Age = " + _age + ", "); + stringBuilder.Append("FirstName = " + _firstName + ", "); + stringBuilder.Append("LastName = " + _lastName + ", "); + stringBuilder.Append("DOB = " + _dOB); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + public enum Pet + { + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Dog")] + Dog = 0, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Cat")] + Cat = 1, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Fish")] + Fish = 2, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Bird")] + Bird = 3, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Reptile")] + Reptile = 4, + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class PersonAndPet + { + + private Person _owner; + + private Pet _pet; + + public PersonAndPet() + { + } + + protected PersonAndPet(PersonAndPet other) + { + _owner = other._owner; + _pet = other._pet; + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="owner")] + public Person Owner + { + get + { + return _owner; + } + set + { + _owner = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="pet")] + public Pet Pet + { + get + { + return _pet; + } + set + { + _pet = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new PersonAndPet(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new PersonAndPet(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("owner = " + _owner + ", "); + stringBuilder.Append("pet = " + _pet); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + /// + /// Serializes a sequence of data model objects into YAML strings. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Serializes a sequence of data model objects into YAML strings.")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + public partial class SerializeToYaml + { + + private System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.SerializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => serializer.Serialize(value)); + }); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + } + + + /// + /// Deserializes a sequence of YAML strings into data model objects. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Deserializes a sequence of YAML strings into data model objects.")] + [System.ComponentModel.DefaultPropertyAttribute("Type")] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + public partial class DeserializeFromYaml : Bonsai.Expressions.SingleArgumentExpressionBuilder + { + + public DeserializeFromYaml() + { + Type = new Bonsai.Expressions.TypeMapping(); + } + + public Bonsai.Expressions.TypeMapping Type { get; set; } + + public override System.Linq.Expressions.Expression Build(System.Collections.Generic.IEnumerable arguments) + { + var typeMapping = (Bonsai.Expressions.TypeMapping)Type; + var returnType = typeMapping.GetType().GetGenericArguments()[0]; + return System.Linq.Expressions.Expression.Call( + typeof(DeserializeFromYaml), + "Process", + new System.Type[] { returnType }, + System.Linq.Enumerable.Single(arguments)); + } + + private static System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.DeserializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => + { + var reader = new System.IO.StringReader(value); + var parser = new YamlDotNet.Core.MergingParser(new YamlDotNet.Core.Parser(reader)); + return serializer.Deserialize(parser); + }); + }); + } + } +} \ No newline at end of file diff --git a/docs/workflows/person-and-pet-enum.bonsai b/docs/workflows/person-and-pet-enum.bonsai new file mode 100644 index 0000000..eb14530 --- /dev/null +++ b/docs/workflows/person-and-pet-enum.bonsai @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/docs/workflows/person-example-bonsai-sgen.bonsai b/docs/workflows/person-example-bonsai-sgen.bonsai new file mode 100644 index 0000000..631776e --- /dev/null +++ b/docs/workflows/person-example-bonsai-sgen.bonsai @@ -0,0 +1,16 @@ + + + + + + + 0 + + + + + + \ No newline at end of file diff --git a/docs/workflows/person_and_dog.json b/docs/workflows/person_and_dog.json new file mode 100644 index 0000000..6f343e8 --- /dev/null +++ b/docs/workflows/person_and_dog.json @@ -0,0 +1,48 @@ +{ + "title": "PersonAndPet", + "definitions": { + "Person": { + "title": "Person", + "type": "object", + "properties": { + "Age": { + "type": "integer" + }, + "FirstName": { + "type": "string" + }, + "LastName": { + "type": "string" + }, + "DOB": { + "type": "string", + "format": "date-time" + } + } + }, + "Dog": { + "title": "Dog", + "type": "object", + "properties": { + "Name": { + "type": "string" + }, + "Breed": { + "type": "string" + }, + "Age": { + "type": "integer" + } + } + } + }, + "type": "object", + "properties": { + "owner": { + "$ref": "#/definitions/Person" + }, + "pet": { + "$ref": "#/definitions/Dog" + } + } +} diff --git a/docs/workflows/person_and_pet_enum.json b/docs/workflows/person_and_pet_enum.json new file mode 100644 index 0000000..78234c0 --- /dev/null +++ b/docs/workflows/person_and_pet_enum.json @@ -0,0 +1,38 @@ +{ + "title": "PersonAndPet", + "definitions": { + "Person": { + "title": "Person", + "type": "object", + "properties": { + "Age": { + "type": "integer" + }, + "FirstName": { + "type": "string" + }, + "LastName": { + "type": "string" + }, + "DOB": { + "type": "string", + "format": "date-time" + } + } + }, + "Pet": { + "title": "Pet", + "type": "string", + "enum": ["Dog", "Cat", "Fish", "Bird", "Reptile"] + } + }, + "type": "object", + "properties": { + "owner": { + "$ref": "#/definitions/Person" + }, + "pet": { + "$ref": "#/definitions/Pet" + } + } +} diff --git a/docs/workflows/personand-dog-example-bonsai-sgen.bonsai b/docs/workflows/personand-dog-example-bonsai-sgen.bonsai new file mode 100644 index 0000000..62d3bcc --- /dev/null +++ b/docs/workflows/personand-dog-example-bonsai-sgen.bonsai @@ -0,0 +1,21 @@ + + + + + + + 0 + + + + + 0 + + + + + + \ No newline at end of file diff --git a/docs/workflows/personand-dog-example-nested-building-bonsai-sgen.bonsai b/docs/workflows/personand-dog-example-nested-building-bonsai-sgen.bonsai new file mode 100644 index 0000000..43236bc --- /dev/null +++ b/docs/workflows/personand-dog-example-nested-building-bonsai-sgen.bonsai @@ -0,0 +1,55 @@ + + + + + + + 0 + + + + + + + + + + 0 + + + + + + + + + + + + Owner + + + Age + + + Pet + + + Breed + + + + + + + + + + + + + + \ No newline at end of file From f3463af446bf241f384a17b546b99db7ddfdcd52 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 12:08:31 +0000 Subject: [PATCH 14/63] Remove unecessary label --- docs/articles/basic-usage.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index 44fc347..3f9efff 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -127,7 +127,6 @@ We can replace the `Pet` object in the previous example with an [`enum`](https:/ > [!TIP] > In certain cases, it may be useful to use `x-enum-names` to specify the rendered names of the enum values. -> Example: > > ```json > { From 91a8c700f0d1f286945c12b438e8b3007194482d Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 12:14:48 +0000 Subject: [PATCH 15/63] Refactor file names --- docs/articles/basic-usage.md | 4 ++-- docs/workflows/{person_and_dog.json => person-and-dog.json} | 0 .../{person_and_pet_enum.json => person-and-pet-enum.json} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename docs/workflows/{person_and_dog.json => person-and-dog.json} (100%) rename docs/workflows/{person_and_pet_enum.json => person-and-pet-enum.json} (100%) diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index 3f9efff..a828107 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -44,7 +44,7 @@ Finally, if one considers the `json-schema` as the "source of truth" for the dat The previous example shows how a single record can be modelled. In practice, projects often require modelling different types of objects. This is where `Bonsai.Sgen` shines, as it allows you to generate multiple objects from a single schema file: -[person_and_dog](~/workflows/person_and_dog.json) +[person_and_dog](~/workflows/person-and-dog.json) ```json { @@ -107,7 +107,7 @@ The previous example highlights the simplicity of generating Bonsai code for sim `Bonsai.Sgen` also supports the generation of enums using the `enum` type in the `json-schema`: We can replace the `Pet` object in the previous example with an [`enum`](https://json-schema.org/understanding-json-schema/reference/enum): -[Pet as Enum](~/workflows/person_and_pet_enum.json). +[Pet as Enum](~/workflows/person-and-pet-enum.json). ```json (...) diff --git a/docs/workflows/person_and_dog.json b/docs/workflows/person-and-dog.json similarity index 100% rename from docs/workflows/person_and_dog.json rename to docs/workflows/person-and-dog.json diff --git a/docs/workflows/person_and_pet_enum.json b/docs/workflows/person-and-pet-enum.json similarity index 100% rename from docs/workflows/person_and_pet_enum.json rename to docs/workflows/person-and-pet-enum.json From 584105b03eb66c756aa710866d40fcf879de1c43 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 12:20:49 +0000 Subject: [PATCH 16/63] Add array manipulation example --- docs/articles/basic-usage.md | 24 ++ .../workflows/Extensions/PersonAndPetsEnum.cs | 303 ++++++++++++++++++ docs/workflows/person-and-pets-enum.bonsai | 32 ++ docs/workflows/person-and-pets-enum.json | 41 +++ 4 files changed, 400 insertions(+) create mode 100644 docs/workflows/Extensions/PersonAndPetsEnum.cs create mode 100644 docs/workflows/person-and-pets-enum.bonsai create mode 100644 docs/workflows/person-and-pets-enum.json diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index a828107..7e4dffc 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -107,6 +107,7 @@ The previous example highlights the simplicity of generating Bonsai code for sim `Bonsai.Sgen` also supports the generation of enums using the `enum` type in the `json-schema`: We can replace the `Pet` object in the previous example with an [`enum`](https://json-schema.org/understanding-json-schema/reference/enum): + [Pet as Enum](~/workflows/person-and-pet-enum.json). ```json @@ -125,6 +126,12 @@ We can replace the `Pet` object in the previous example with an [`enum`](https:/ } ``` +In Bonsai, they can be manipulated as [`Enum`](https://learn.microsoft.com/en-us/dotnet/api/system.enum?view=net-9.0) types: + +:::workflow +![Person and Pets](~/workflows/person-and-pet-enum.bonsai) +::: + > [!TIP] > In certain cases, it may be useful to use `x-enum-names` to specify the rendered names of the enum values. > @@ -138,3 +145,20 @@ We can replace the `Pet` object in the previous example with an [`enum`](https:/ > } > } > ``` + +## Lists + +`Bonsai.Sgen` also supports the generation of lists using the `array` type in the `json-schema`: + +```json + "pets": { + "type": "array", + "items": {"$ref": "#/definitions/Pet"} + } +``` + +`json-schema` `array`s will be rendered as [`List`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1?view=net-9.0) in the generated code and can be manipulated (and created) as such. + +:::workflow +![Person and Pets](~/workflows/person-and-pets-enum.bonsai) +::: diff --git a/docs/workflows/Extensions/PersonAndPetsEnum.cs b/docs/workflows/Extensions/PersonAndPetsEnum.cs new file mode 100644 index 0000000..518625b --- /dev/null +++ b/docs/workflows/Extensions/PersonAndPetsEnum.cs @@ -0,0 +1,303 @@ +//---------------------- +// +// Generated using the NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.0.0) (http://NJsonSchema.org) +// +//---------------------- + + +namespace PersonAndPetsEnum +{ + #pragma warning disable // Disable all warnings + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class Person + { + + private int _age; + + private string _firstName; + + private string _lastName; + + private System.DateTimeOffset _dOB; + + public Person() + { + } + + protected Person(Person other) + { + _age = other._age; + _firstName = other._firstName; + _lastName = other._lastName; + _dOB = other._dOB; + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Age")] + public int Age + { + get + { + return _age; + } + set + { + _age = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="FirstName")] + public string FirstName + { + get + { + return _firstName; + } + set + { + _firstName = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="LastName")] + public string LastName + { + get + { + return _lastName; + } + set + { + _lastName = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="DOB")] + public System.DateTimeOffset DOB + { + get + { + return _dOB; + } + set + { + _dOB = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Person(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new Person(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("Age = " + _age + ", "); + stringBuilder.Append("FirstName = " + _firstName + ", "); + stringBuilder.Append("LastName = " + _lastName + ", "); + stringBuilder.Append("DOB = " + _dOB); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + public enum Pet + { + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Dog")] + Dog = 0, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Cat")] + Cat = 1, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Fish")] + Fish = 2, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Bird")] + Bird = 3, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Reptile")] + Reptile = 4, + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class PersonAndPets + { + + private Person _owner; + + private System.Collections.Generic.List _pets = new System.Collections.Generic.List(); + + public PersonAndPets() + { + } + + protected PersonAndPets(PersonAndPets other) + { + _owner = other._owner; + _pets = other._pets; + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="owner")] + public Person Owner + { + get + { + return _owner; + } + set + { + _owner = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="pets")] + public System.Collections.Generic.List Pets + { + get + { + return _pets; + } + set + { + _pets = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new PersonAndPets(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new PersonAndPets(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("owner = " + _owner + ", "); + stringBuilder.Append("pets = " + _pets); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + /// + /// Serializes a sequence of data model objects into YAML strings. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Serializes a sequence of data model objects into YAML strings.")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + public partial class SerializeToYaml + { + + private System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.SerializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => serializer.Serialize(value)); + }); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + } + + + /// + /// Deserializes a sequence of YAML strings into data model objects. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Deserializes a sequence of YAML strings into data model objects.")] + [System.ComponentModel.DefaultPropertyAttribute("Type")] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + public partial class DeserializeFromYaml : Bonsai.Expressions.SingleArgumentExpressionBuilder + { + + public DeserializeFromYaml() + { + Type = new Bonsai.Expressions.TypeMapping(); + } + + public Bonsai.Expressions.TypeMapping Type { get; set; } + + public override System.Linq.Expressions.Expression Build(System.Collections.Generic.IEnumerable arguments) + { + var typeMapping = (Bonsai.Expressions.TypeMapping)Type; + var returnType = typeMapping.GetType().GetGenericArguments()[0]; + return System.Linq.Expressions.Expression.Call( + typeof(DeserializeFromYaml), + "Process", + new System.Type[] { returnType }, + System.Linq.Enumerable.Single(arguments)); + } + + private static System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.DeserializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => + { + var reader = new System.IO.StringReader(value); + var parser = new YamlDotNet.Core.MergingParser(new YamlDotNet.Core.Parser(reader)); + return serializer.Deserialize(parser); + }); + }); + } + } +} \ No newline at end of file diff --git a/docs/workflows/person-and-pets-enum.bonsai b/docs/workflows/person-and-pets-enum.bonsai new file mode 100644 index 0000000..7b9df4f --- /dev/null +++ b/docs/workflows/person-and-pets-enum.bonsai @@ -0,0 +1,32 @@ + + + + + + + + + Owner + + + Pets + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/workflows/person-and-pets-enum.json b/docs/workflows/person-and-pets-enum.json new file mode 100644 index 0000000..0f0f950 --- /dev/null +++ b/docs/workflows/person-and-pets-enum.json @@ -0,0 +1,41 @@ +{ + "title": "PersonAndPets", + "definitions": { + "Person": { + "title": "Person", + "type": "object", + "properties": { + "Age": { + "type": "integer" + }, + "FirstName": { + "type": "string" + }, + "LastName": { + "type": "string" + }, + "DOB": { + "type": "string", + "format": "date-time" + } + } + }, + "Pet": { + "title": "Pet", + "type": "string", + "enum": ["Dog", "Cat", "Fish", "Bird", "Reptile"] + } + }, + "type": "object", + "properties": { + "owner": { + "$ref": "#/definitions/Person" + }, + "pets": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + } +} From dd4e59c0176523af1290ce17deeef45b63615f2e Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 12:53:13 +0000 Subject: [PATCH 17/63] Add nullable example --- docs/articles/basic-usage.md | 22 ++ .../Extensions/PersonAndPetEnumNullable.cs | 303 ++++++++++++++++++ .../person-and-pet-enum-nullable.bonsai | 66 ++++ .../person-and-pet-enum-nullable.json | 41 +++ 4 files changed, 432 insertions(+) create mode 100644 docs/workflows/Extensions/PersonAndPetEnumNullable.cs create mode 100644 docs/workflows/person-and-pet-enum-nullable.bonsai create mode 100644 docs/workflows/person-and-pet-enum-nullable.json diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index 7e4dffc..82ac423 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -162,3 +162,25 @@ In Bonsai, they can be manipulated as [`Enum`](https://learn.microsoft.com/en-us :::workflow ![Person and Pets](~/workflows/person-and-pets-enum.bonsai) ::: + +## Nullable types + +`json-schema` supports the `null` type, which can be used to represent nullable types. The standard is a bit loose in this regard, but `Bonsai.Sgen` will generate a nullable-T if the json-schema represents it using the `oneOf` keyword: + +```json + "pet": { + "oneOf": [ + {"$ref": "#/definitions/Pet"}, + {"type": "null"} + ] + } +``` + +For value types, the generated code will render a [Nullable value type](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/nullable-value-types) type. This type will expose two properties: `HasValue` and `Value`, that can be used to test and manipulate the type, respectively. + +For reference types, the generated code will not render a nullable type since reference types are already nullable in C#. A consumer can test for `null` to determine if the value is present by simply using an `ExpressionTransform` operator with `it == null`.: + +:::workflow +![Nullable pet](~/workflows/person-and-pets-enum-nullable.bonsai) +::: + diff --git a/docs/workflows/Extensions/PersonAndPetEnumNullable.cs b/docs/workflows/Extensions/PersonAndPetEnumNullable.cs new file mode 100644 index 0000000..4d8f22c --- /dev/null +++ b/docs/workflows/Extensions/PersonAndPetEnumNullable.cs @@ -0,0 +1,303 @@ +//---------------------- +// +// Generated using the NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.0.0) (http://NJsonSchema.org) +// +//---------------------- + + +namespace PersonAndPetEnumNullable +{ + #pragma warning disable // Disable all warnings + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class Person + { + + private int _age; + + private string _firstName; + + private string _lastName; + + private System.DateTimeOffset _dOB; + + public Person() + { + } + + protected Person(Person other) + { + _age = other._age; + _firstName = other._firstName; + _lastName = other._lastName; + _dOB = other._dOB; + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Age")] + public int Age + { + get + { + return _age; + } + set + { + _age = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="FirstName")] + public string FirstName + { + get + { + return _firstName; + } + set + { + _firstName = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="LastName")] + public string LastName + { + get + { + return _lastName; + } + set + { + _lastName = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="DOB")] + public System.DateTimeOffset DOB + { + get + { + return _dOB; + } + set + { + _dOB = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Person(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new Person(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("Age = " + _age + ", "); + stringBuilder.Append("FirstName = " + _firstName + ", "); + stringBuilder.Append("LastName = " + _lastName + ", "); + stringBuilder.Append("DOB = " + _dOB); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + public enum Pet + { + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Dog")] + Dog = 0, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Cat")] + Cat = 1, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Fish")] + Fish = 2, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Bird")] + Bird = 3, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Reptile")] + Reptile = 4, + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class PersonAndPet + { + + private Person _owner; + + private Pet? _pet; + + public PersonAndPet() + { + } + + protected PersonAndPet(PersonAndPet other) + { + _owner = other._owner; + _pet = other._pet; + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="owner")] + public Person Owner + { + get + { + return _owner; + } + set + { + _owner = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="pet")] + public Pet? Pet + { + get + { + return _pet; + } + set + { + _pet = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new PersonAndPet(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new PersonAndPet(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("owner = " + _owner + ", "); + stringBuilder.Append("pet = " + _pet); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + /// + /// Serializes a sequence of data model objects into YAML strings. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Serializes a sequence of data model objects into YAML strings.")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + public partial class SerializeToYaml + { + + private System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.SerializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => serializer.Serialize(value)); + }); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + } + + + /// + /// Deserializes a sequence of YAML strings into data model objects. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Deserializes a sequence of YAML strings into data model objects.")] + [System.ComponentModel.DefaultPropertyAttribute("Type")] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + public partial class DeserializeFromYaml : Bonsai.Expressions.SingleArgumentExpressionBuilder + { + + public DeserializeFromYaml() + { + Type = new Bonsai.Expressions.TypeMapping(); + } + + public Bonsai.Expressions.TypeMapping Type { get; set; } + + public override System.Linq.Expressions.Expression Build(System.Collections.Generic.IEnumerable arguments) + { + var typeMapping = (Bonsai.Expressions.TypeMapping)Type; + var returnType = typeMapping.GetType().GetGenericArguments()[0]; + return System.Linq.Expressions.Expression.Call( + typeof(DeserializeFromYaml), + "Process", + new System.Type[] { returnType }, + System.Linq.Enumerable.Single(arguments)); + } + + private static System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.DeserializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => + { + var reader = new System.IO.StringReader(value); + var parser = new YamlDotNet.Core.MergingParser(new YamlDotNet.Core.Parser(reader)); + return serializer.Deserialize(parser); + }); + }); + } + } +} \ No newline at end of file diff --git a/docs/workflows/person-and-pet-enum-nullable.bonsai b/docs/workflows/person-and-pet-enum-nullable.bonsai new file mode 100644 index 0000000..dda837d --- /dev/null +++ b/docs/workflows/person-and-pet-enum-nullable.bonsai @@ -0,0 +1,66 @@ + + + + + + + + + Value Type + + + + Pet + + + HasValue + + + + Source1 + + + HasValue + + + + + + + + + + + Value + + + Reference Type + + + + + + + Owner + + + IsNull + it == null + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/workflows/person-and-pet-enum-nullable.json b/docs/workflows/person-and-pet-enum-nullable.json new file mode 100644 index 0000000..f8e7342 --- /dev/null +++ b/docs/workflows/person-and-pet-enum-nullable.json @@ -0,0 +1,41 @@ +{ + "title": "PersonAndPet", + "definitions": { + "Person": { + "title": "Person", + "type": "object", + "properties": { + "Age": { + "type": "integer" + }, + "FirstName": { + "type": "string" + }, + "LastName": { + "type": "string" + }, + "DOB": { + "type": "string", + "format": "date-time" + } + } + }, + "Pet": { + "title": "Pet", + "type": "string", + "enum": ["Dog", "Cat", "Fish", "Bird", "Reptile"] + } + }, + "type": "object", + "properties": { + "owner": { + "$ref": "#/definitions/Person" + }, + "pet": { + "oneOf": [ + {"$ref": "#/definitions/Pet"}, + {"type": "null"} + ] + } + } +} From fb418e68f2e4c8d81c076b9eb4d4630f7cf9ef33 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 13:00:55 +0000 Subject: [PATCH 18/63] Add required field section --- docs/articles/basic-usage.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index 82ac423..575cf16 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -184,3 +184,15 @@ For reference types, the generated code will not render a nullable type since re ![Nullable pet](~/workflows/person-and-pets-enum-nullable.bonsai) ::: +## Required fields + +`json-schema` supports the [`required`](https://json-schema.org/learn/getting-started-step-by-step#define-required-properties) keyword to specify which fields are required. By default, all fields are optional. This can be useful to enforce the presence of certain fields in the object at deserialization time. However, `Bonsai.Sgen` will not generate any code to enforce this requirement during object construction, only at deserialization. It is up to the user to ensure that the object is correctly populated before using it. + +> [!Note] +> Some confusion maybe arise about the distinction between `null` and `required`. This is all the more confusing since different languages and libraries may refer to these concepts differently. For the sake of this tool (and honestly in general) the following definitions are used: + +> - `nullable` means that the field can be `null` or type `T` +> - `required` means that the field must be present in the object at deserialization time +> - An object can be `nullable` and `required` at the same time. This means it MUST be defined in the object, but it can be defined as `null`. +> - An object can be `not required` and `nullable`. This does NOT mean that the object is, by default, `null`. It means that the object should have a default value, which can in theory be `null`. +> - An object can be `not required` and `not nullable`. This means that the object must have a default value, which cannot be `null`. \ No newline at end of file From 4511b873006a98977e43d9dea60ac125b5f78080 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 28 Dec 2024 18:11:41 +0000 Subject: [PATCH 19/63] Add documentation on Unions --- docs/articles/advanced-usage.md | 73 +++ .../Extensions/PersonAndDiscriminatedPets.cs | 530 ++++++++++++++++++ .../person-and-discriminated-pets.json | 84 +++ .../person-pet-discriminated-union.bonsai | 73 +++ 4 files changed, 760 insertions(+) create mode 100644 docs/workflows/Extensions/PersonAndDiscriminatedPets.cs create mode 100644 docs/workflows/person-and-discriminated-pets.json create mode 100644 docs/workflows/person-pet-discriminated-union.bonsai diff --git a/docs/articles/advanced-usage.md b/docs/articles/advanced-usage.md index e69de29..3263724 100644 --- a/docs/articles/advanced-usage.md +++ b/docs/articles/advanced-usage.md @@ -0,0 +1,73 @@ +--- +uid: advanced-usage +--- + + +## Unions + +In the previous examples, we have seen how create object properties of a single type. However, in practice, data structures' fields can often be represented by one of several types. We have actually seen a special case of this behavior in the previous nullable example, where a field can be either a value of a given type or `null` (or an Union between type `T` and `null`). + +Similarly, `json-schema` allows union types to be defined using the `oneOf` keyword. For example, consider the following schema: + +```json +{ + "title": "MyPet", + "type": "object", + "properties": { + "FooProperty": { + "oneOf": [ + { "type": "string" }, + { "type": "number" } + ] + } + } +} +``` + +If we run `Bonsai.Sgen` on this schema, we will get the following signature for the `FooProperty` property: + +```csharp +public object FooProperty +``` + +This is because while the `oneOf` keyword is supported by the `Bonsai.Sgen` tool, for statically typed languages like `C#` and `Bonsai`, we need to know the exact type of the property at compile time. As a result, we opt to "up-cast" the property to the most general type that can represent all the possible types in the union (`object`). It is up to the user to down-cast the property to the correct type at runtime. + + +## Tagged Unions + +At this point, you might be wondering if there is a way to represent union types in a more type-safe way in json-schema. The answer is yes, and the way to do it is by using [`discriminated unions`](https://en.wikipedia.org/wiki/Tagged_union) (or `tagged union`). The syntax for discriminated unions is not supported by vanilla `json-schema`, but it is supported by the [`OpenAPI` standard](https://swagger.io/docs/specification/v3_0/data-models/inheritance-and-polymorphism/#discriminator), which is a superset of `json-schema`. The key idea behind discriminated unions is to add a `discriminator` field to the schema that specifies the property that will be used to determine the type of the object at runtime. + +For example, a `Pet` object that can be either a `Dog` or a `Cat` can be represented as follows: + +[person](~/workflows/person-and-discriminated-pets.json) + +```json +"Pet": { + "discriminator": { + "mapping": { + "cat": "#/definitions/Cat", + "dog": "#/definitions/Dog" + }, + "propertyName": "pet_type" + }, + "oneOf": [ + { + "$ref": "#/definitions/Dog" + }, + { + "$ref": "#/definitions/Cat" + } + ] + } +``` + +In `C#`, `Bonsai.Sgen` will generate a root type `Pet` that will be inherited by the `Dog` and `Cat` types (since in the worst case scenario, the discriminated property must be shared). The `Pet` type will have a `pet_type` property that will be used to downcast to the proper type at runtime. At this point we can open our example in `Bonsai` and see how the `Pet` type is represented in the workflow. + +As you can see below, we still get a `Pet` type. Better than `object` but still not a `Dog` or `Cat` type. Fortunately, `Bonsai.Sgen` will generate an operator that can be used to filter and downcast the `Pet` objects to the correct type at runtime. These are called `Match` operators. After adding a `MatchPet` to our workflow we can select the desired target type which will allow us access to the properties of the `Dog` or `Cat` type. Conversely, we can also upcast a `Dog` or `Cat` to a `Pet` leaving the `MatchPet` operator's `Type` property empty. + +:::workflow +![Discriminated Unions](~/workflows/person-pet-discriminated-union.bonsai) +::: + +> [!Important] +> In is general advisable to use references in the `oneOf` syntax. Not only does this decision make your `json-schema` significantly smaller, it will also help `Bonsai.Sgen` generate the correct class hierarchy if multiple unions are present in the schema. If you use inline objects, `Bonsai.Sgen` will likely have to generate a new root class for each union, which can lead to a lot of duplicated code and a more complex object hierarchy. \ No newline at end of file diff --git a/docs/workflows/Extensions/PersonAndDiscriminatedPets.cs b/docs/workflows/Extensions/PersonAndDiscriminatedPets.cs new file mode 100644 index 0000000..a0bcd35 --- /dev/null +++ b/docs/workflows/Extensions/PersonAndDiscriminatedPets.cs @@ -0,0 +1,530 @@ +//---------------------- +// +// Generated using the NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.0.0) (http://NJsonSchema.org) +// +//---------------------- + + +namespace PersonAndDiscriminatedPets +{ + #pragma warning disable // Disable all warnings + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.3.0.0 (Newtonsoft.Json v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class Cat : Pet + { + + private int _age; + + private bool _canMeow = true; + + public Cat() + { + } + + protected Cat(Cat other) : + base(other) + { + _age = other._age; + _canMeow = other._canMeow; + } + + [Newtonsoft.Json.JsonPropertyAttribute("age", Required=Newtonsoft.Json.Required.Always)] + public int Age + { + get + { + return _age; + } + set + { + _age = value; + } + } + + [Newtonsoft.Json.JsonPropertyAttribute("can_meow")] + public bool CanMeow + { + get + { + return _canMeow; + } + set + { + _canMeow = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Cat(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new Cat(this)); + } + + protected override bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + if (base.PrintMembers(stringBuilder)) + { + stringBuilder.Append(", "); + } + stringBuilder.Append("age = " + _age + ", "); + stringBuilder.Append("can_meow = " + _canMeow); + return true; + } + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.3.0.0 (Newtonsoft.Json v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class Dog : Pet + { + + private int _age; + + private bool _canBark = true; + + public Dog() + { + } + + protected Dog(Dog other) : + base(other) + { + _age = other._age; + _canBark = other._canBark; + } + + [Newtonsoft.Json.JsonPropertyAttribute("age", Required=Newtonsoft.Json.Required.Always)] + public int Age + { + get + { + return _age; + } + set + { + _age = value; + } + } + + [Newtonsoft.Json.JsonPropertyAttribute("can_bark")] + public bool CanBark + { + get + { + return _canBark; + } + set + { + _canBark = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Dog(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new Dog(this)); + } + + protected override bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + if (base.PrintMembers(stringBuilder)) + { + stringBuilder.Append(", "); + } + stringBuilder.Append("age = " + _age + ", "); + stringBuilder.Append("can_bark = " + _canBark); + return true; + } + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.3.0.0 (Newtonsoft.Json v13.0.0.0)")] + [Newtonsoft.Json.JsonConverter(typeof(JsonInheritanceConverter), "pet_type")] + [JsonInheritanceAttribute("dog", typeof(Dog))] + [JsonInheritanceAttribute("cat", typeof(Cat))] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class Pet + { + + public Pet() + { + } + + protected Pet(Pet other) + { + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Pet(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new Pet(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + return false; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.3.0.0 (Newtonsoft.Json v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class PersonAndPet + { + + private string _owner; + + private Pet _pet; + + public PersonAndPet() + { + } + + protected PersonAndPet(PersonAndPet other) + { + _owner = other._owner; + _pet = other._pet; + } + + [Newtonsoft.Json.JsonPropertyAttribute("owner", Required=Newtonsoft.Json.Required.Always)] + public string Owner + { + get + { + return _owner; + } + set + { + _owner = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [Newtonsoft.Json.JsonPropertyAttribute("pet", Required=Newtonsoft.Json.Required.Always)] + public Pet Pet + { + get + { + return _pet; + } + set + { + _pet = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new PersonAndPet(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new PersonAndPet(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("owner = " + _owner + ", "); + stringBuilder.Append("pet = " + _pet); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.9.0.0 (Newtonsoft.Json v13.0.0.0)")] + [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Interface, AllowMultiple = true)] + internal class JsonInheritanceAttribute : System.Attribute + { + public JsonInheritanceAttribute(string key, System.Type type) + { + Key = key; + Type = type; + } + + public string Key { get; private set; } + + public System.Type Type { get; private set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.9.0.0 (Newtonsoft.Json v13.0.0.0)")] + public class JsonInheritanceConverter : Newtonsoft.Json.JsonConverter + { + internal static readonly string DefaultDiscriminatorName = "discriminator"; + + private readonly string _discriminatorName; + + [System.ThreadStatic] + private static bool _isReading; + + [System.ThreadStatic] + private static bool _isWriting; + + public JsonInheritanceConverter() + { + _discriminatorName = DefaultDiscriminatorName; + } + + public JsonInheritanceConverter(string discriminatorName) + { + _discriminatorName = discriminatorName; + } + + public string DiscriminatorName { get { return _discriminatorName; } } + + public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) + { + try + { + _isWriting = true; + + var jObject = Newtonsoft.Json.Linq.JObject.FromObject(value, serializer); + jObject.AddFirst(new Newtonsoft.Json.Linq.JProperty(_discriminatorName, GetSubtypeDiscriminator(value.GetType()))); + writer.WriteToken(jObject.CreateReader()); + } + finally + { + _isWriting = false; + } + } + + public override bool CanWrite + { + get + { + if (_isWriting) + { + _isWriting = false; + return false; + } + return true; + } + } + + public override bool CanRead + { + get + { + if (_isReading) + { + _isReading = false; + return false; + } + return true; + } + } + + public override bool CanConvert(System.Type objectType) + { + return true; + } + + public override object ReadJson(Newtonsoft.Json.JsonReader reader, System.Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer) + { + var jObject = serializer.Deserialize(reader); + if (jObject == null) + return null; + + var discriminatorValue = jObject.GetValue(_discriminatorName); + var discriminator = discriminatorValue != null ? Newtonsoft.Json.Linq.Extensions.Value(discriminatorValue) : null; + var subtype = GetObjectSubtype(objectType, discriminator); + + var objectContract = serializer.ContractResolver.ResolveContract(subtype) as Newtonsoft.Json.Serialization.JsonObjectContract; + if (objectContract == null || System.Linq.Enumerable.All(objectContract.Properties, p => p.PropertyName != _discriminatorName)) + { + jObject.Remove(_discriminatorName); + } + + try + { + _isReading = true; + return serializer.Deserialize(jObject.CreateReader(), subtype); + } + finally + { + _isReading = false; + } + } + + private System.Type GetObjectSubtype(System.Type objectType, string discriminator) + { + foreach (var attribute in System.Reflection.CustomAttributeExtensions.GetCustomAttributes(System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType), true)) + { + if (attribute.Key == discriminator) + return attribute.Type; + } + + return objectType; + } + + private string GetSubtypeDiscriminator(System.Type objectType) + { + foreach (var attribute in System.Reflection.CustomAttributeExtensions.GetCustomAttributes(System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType), true)) + { + if (attribute.Type == objectType) + return attribute.Key; + } + + return objectType.Name; + } + } + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.3.0.0 (Newtonsoft.Json v13.0.0.0)")] + [System.ComponentModel.DefaultPropertyAttribute("Type")] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Combinator)] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + public partial class MatchPet : Bonsai.Expressions.SingleArgumentExpressionBuilder + { + + public Bonsai.Expressions.TypeMapping Type { get; set; } + + public override System.Linq.Expressions.Expression Build(System.Collections.Generic.IEnumerable arguments) + { + var typeMapping = Type; + var returnType = typeMapping != null ? typeMapping.GetType().GetGenericArguments()[0] : typeof(Pet); + return System.Linq.Expressions.Expression.Call( + typeof(MatchPet), + "Process", + new System.Type[] { returnType }, + System.Linq.Enumerable.Single(arguments)); + } + + + private static System.IObservable Process(System.IObservable source) + where TResult : Pet + { + return System.Reactive.Linq.Observable.Create(observer => + { + var sourceObserver = System.Reactive.Observer.Create( + value => + { + var match = value as TResult; + if (match != null) observer.OnNext(match); + }, + observer.OnError, + observer.OnCompleted); + return System.ObservableExtensions.SubscribeSafe(source, sourceObserver); + }); + } + } + + + /// + /// Serializes a sequence of data model objects into JSON strings. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.3.0.0 (Newtonsoft.Json v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Serializes a sequence of data model objects into JSON strings.")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + public partial class SerializeToJson + { + + private System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, value => Newtonsoft.Json.JsonConvert.SerializeObject(value)); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + } + + + /// + /// Deserializes a sequence of JSON strings into data model objects. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.3.0.0 (Newtonsoft.Json v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Deserializes a sequence of JSON strings into data model objects.")] + [System.ComponentModel.DefaultPropertyAttribute("Type")] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + public partial class DeserializeFromJson : Bonsai.Expressions.SingleArgumentExpressionBuilder + { + + public DeserializeFromJson() + { + Type = new Bonsai.Expressions.TypeMapping(); + } + + public Bonsai.Expressions.TypeMapping Type { get; set; } + + public override System.Linq.Expressions.Expression Build(System.Collections.Generic.IEnumerable arguments) + { + var typeMapping = (Bonsai.Expressions.TypeMapping)Type; + var returnType = typeMapping.GetType().GetGenericArguments()[0]; + return System.Linq.Expressions.Expression.Call( + typeof(DeserializeFromJson), + "Process", + new System.Type[] { returnType }, + System.Linq.Enumerable.Single(arguments)); + } + + private static System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, value => Newtonsoft.Json.JsonConvert.DeserializeObject(value)); + } + } +} diff --git a/docs/workflows/person-and-discriminated-pets.json b/docs/workflows/person-and-discriminated-pets.json new file mode 100644 index 0000000..73af29c --- /dev/null +++ b/docs/workflows/person-and-discriminated-pets.json @@ -0,0 +1,84 @@ +{ + "definitions": { + "Cat": { + "properties": { + "pet_type": { + "const": "cat", + "default": "cat", + "title": "Pet Type", + "type": "string" + }, + "age": { + "title": "Age", + "type": "integer" + }, + "can_meow": { + "default": true, + "title": "Can Meow", + "type": "boolean" + } + }, + "required": [ + "age" + ], + "title": "Cat", + "type": "object" + }, + "Dog": { + "properties": { + "pet_type": { + "const": "dog", + "default": "dog", + "title": "Pet Type", + "type": "string" + }, + "age": { + "title": "Age", + "type": "integer" + }, + "can_bark": { + "default": true, + "title": "Can Bark", + "type": "boolean" + } + }, + "required": [ + "age" + ], + "title": "Dog", + "type": "object" + }, + "Pet": { + "discriminator": { + "mapping": { + "cat": "#/definitions/Cat", + "dog": "#/definitions/Dog" + }, + "propertyName": "pet_type" + }, + "oneOf": [ + { + "$ref": "#/definitions/Dog" + }, + { + "$ref": "#/definitions/Cat" + } + ] + } + }, + "properties": { + "owner": { + "title": "Owner", + "type": "string" + }, + "pet": { + "$ref": "#/definitions/Pet" + } + }, + "required": [ + "owner", + "pet" + ], + "title": "PersonAndPet", + "type": "object" +} \ No newline at end of file diff --git a/docs/workflows/person-pet-discriminated-union.bonsai b/docs/workflows/person-pet-discriminated-union.bonsai new file mode 100644 index 0000000..453acb7 --- /dev/null +++ b/docs/workflows/person-pet-discriminated-union.bonsai @@ -0,0 +1,73 @@ + + + + + + + + + Pet + + + + + + CanBark + + + + + + CanMeow + + + + 0 + true + + + + + + true + 0 + + + + + + + + + + + CanBark + + + + + + CanMeow + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 416a5de80627dcba50532e3e59169c079a9a7685 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 28 Dec 2024 18:31:00 +0000 Subject: [PATCH 20/63] Add section on partials --- docs/articles/advanced-usage.md | 35 ++++++++++++++++++- .../Extensions/CatExtensionMethods.cs | 14 ++++++++ docs/workflows/sum-cats.bonsai | 32 +++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 docs/workflows/Extensions/CatExtensionMethods.cs create mode 100644 docs/workflows/sum-cats.bonsai diff --git a/docs/articles/advanced-usage.md b/docs/articles/advanced-usage.md index 3263724..b040677 100644 --- a/docs/articles/advanced-usage.md +++ b/docs/articles/advanced-usage.md @@ -70,4 +70,37 @@ As you can see below, we still get a `Pet` type. Better than `object` but still ::: > [!Important] -> In is general advisable to use references in the `oneOf` syntax. Not only does this decision make your `json-schema` significantly smaller, it will also help `Bonsai.Sgen` generate the correct class hierarchy if multiple unions are present in the schema. If you use inline objects, `Bonsai.Sgen` will likely have to generate a new root class for each union, which can lead to a lot of duplicated code and a more complex object hierarchy. \ No newline at end of file +> In is general advisable to use references in the `oneOf` syntax. Not only does this decision make your `json-schema` significantly smaller, it will also help `Bonsai.Sgen` generate the correct class hierarchy if multiple unions are present in the schema. If you use inline objects, `Bonsai.Sgen` will likely have to generate a new root class for each union, which can lead to a lot of duplicated code and a more complex object hierarchy. + + + +## Extending generated code with `partial` classes + +Since `Bonsai.Sgen` will generate proper `class` for each object in the schema, it is possible to use these types to create custom operators and methods using the `Scriping Extensions` feature of `Bonsai`. However, sometimes we may want to extend the features of the generated classes directly... + +For those that inspected the general `C#` code, you will notice that all classes are marked as [`partial`](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods). This is a feature of `C#` that allows a class to be split. This was a deliberate design choice to allow users to extend the generated code. However, because it is usually always a bad idea to modify generated code directly (e.g. we may want to regenerate it in the future), `partial` classes allows modification to be made in a separate file. + +Suppose we want to sum `Cats`, we can overload the operator with a small method in a separate file:: + +```csharp +namespace PersonAndDiscriminatedPets +{ + partial class Cat{ + public static Cat operator +(Cat c1, Cat c2) + { + return new Cat + { + CanMeow = c1.CanMeow || c2.CanMeow, + Age = c1.Age + c2.Age + }; + } + } +} +``` + +In `Bonsai`, we can now use the `Add` operator to sum `Cats`: + + +:::workflow +![Discriminated Unions](~/workflows/sum-cats.bonsai) +::: diff --git a/docs/workflows/Extensions/CatExtensionMethods.cs b/docs/workflows/Extensions/CatExtensionMethods.cs new file mode 100644 index 0000000..4fd051d --- /dev/null +++ b/docs/workflows/Extensions/CatExtensionMethods.cs @@ -0,0 +1,14 @@ + +namespace PersonAndDiscriminatedPets +{ + partial class Cat{ + public static Cat operator +(Cat c1, Cat c2) + { + return new Cat + { + CanMeow = c1.CanMeow || c2.CanMeow, + Age = c1.Age + c2.Age + }; + } + } +} diff --git a/docs/workflows/sum-cats.bonsai b/docs/workflows/sum-cats.bonsai new file mode 100644 index 0000000..0e3df9e --- /dev/null +++ b/docs/workflows/sum-cats.bonsai @@ -0,0 +1,32 @@ + + + + + + + 0 + true + + + + + 0 + true + + + + + + + + + + + + + + \ No newline at end of file From 33722ae97c767ce012579cf4ccca5e3d84626d24 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 28 Dec 2024 18:34:47 +0000 Subject: [PATCH 21/63] Add x-abstract documentation --- docs/articles/advanced-usage.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/articles/advanced-usage.md b/docs/articles/advanced-usage.md index b040677..ceb7fce 100644 --- a/docs/articles/advanced-usage.md +++ b/docs/articles/advanced-usage.md @@ -104,3 +104,8 @@ In `Bonsai`, we can now use the `Add` operator to sum `Cats`: :::workflow ![Discriminated Unions](~/workflows/sum-cats.bonsai) ::: + + +## Other supported tags + +- `x-abstract`: This tag is used to mark a class as abstract. An abstract class will not be generated as an operator in Bonsai. This may be useful for root classes of unions that may never need to be manipulated in Bonsai. \ No newline at end of file From 3f4db64ee9dca3c41ef9139c0171c6671e880b5d Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 28 Dec 2024 18:49:06 +0000 Subject: [PATCH 22/63] Add (de)serialization examples --- .bonsai/Bonsai.config | 3 ++ docs/articles/basic-usage.md | 25 ++++++++++- docs/articles/experimental.md | 0 docs/articles/intro.md | 2 +- docs/workflows/serialization-example.bonsai | 48 +++++++++++++++++++++ 5 files changed, 76 insertions(+), 2 deletions(-) delete mode 100644 docs/articles/experimental.md create mode 100644 docs/workflows/serialization-example.bonsai diff --git a/.bonsai/Bonsai.config b/.bonsai/Bonsai.config index 3db0977..1fa28c4 100644 --- a/.bonsai/Bonsai.config +++ b/.bonsai/Bonsai.config @@ -7,6 +7,7 @@ + @@ -31,6 +32,7 @@ + @@ -39,6 +41,7 @@ + diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index 575cf16..d782e6c 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -195,4 +195,27 @@ For reference types, the generated code will not render a nullable type since re > - `required` means that the field must be present in the object at deserialization time > - An object can be `nullable` and `required` at the same time. This means it MUST be defined in the object, but it can be defined as `null`. > - An object can be `not required` and `nullable`. This does NOT mean that the object is, by default, `null`. It means that the object should have a default value, which can in theory be `null`. -> - An object can be `not required` and `not nullable`. This means that the object must have a default value, which cannot be `null`. \ No newline at end of file +> - An object can be `not required` and `not nullable`. This means that the object must have a default value, which cannot be `null`. + + +## Serialization and Deserialization + +One of the biggest perks of using json-schema to represent our objects is the guaranteed that all records are (de)serializable. This means that we can go from a text-based format (great specification and logging) to a `C#` type seamlessly, and vice-versa. `Bonsai.Sgen` will optionally generate (de)serialization operators for all objects in the schema if the `--serializer` property is not `None`. Currently, two formats are supported out of the box: `Json` (via [`NewtonsoftJson`](https://github.com/JamesNK/Newtonsoft.Json)) and `yaml` (via [`YamlDotNet`](https://github.com/aaubry/YamlDotNet)). + +The two operations are afforded via the `SerializeToYaml` (or `SerializeToJson`) and `DeserializeFromYaml` (or `DeserializeFromJson`) operators, respectively. + +`SerializeToYaml` will take a `T` object (known to the namespace) and return a `string` representation of the object. +`DeserializeFromYaml` will take a `string` and return a `T` object. If validation fails, the operator will throw an exception. + +:::workflow +![(de)serialization](~/workflows/serialization-example.bonsai) +::: + +> [!Tip] +> Remember to add the necessary package references to your `Extensions.csproj` file depending on the serializer you want to use! +> ```xml +> +> +> +> +> ``` \ No newline at end of file diff --git a/docs/articles/experimental.md b/docs/articles/experimental.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/articles/intro.md b/docs/articles/intro.md index 85d8603..1ee7386 100644 --- a/docs/articles/intro.md +++ b/docs/articles/intro.md @@ -4,7 +4,7 @@ uid: intro ## What is Bonsai.Sgen? -`Bonsai.Sgen` is a code generator tool for the [Bonsai programming language](https://bonsai-rx.org/). It leverages [`json-schema`](https://json-schema.org/) as a standard to represent [record-like](https://en.wikipedia.org/wiki/Record_(computer_science)) structures, and automatically generates Bonsai-compatible isomorphic operators to create and manipulate these objects. +`Bonsai.Sgen` is a code generator tool for the [Bonsai programming language](https://bonsai-rx.org/). It leverages [`json-schema`](https://json-schema.org/) as a standard to represent [record-like](https://en.wikipedia.org/wiki/Record_(computer_science)) structures, and automatically generates Bonsai-compatible isomorphic operators to create and manipulate these objects. It builds on top of the [`NJsonSchema` library](https://github.com/RicoSuter/NJsonSchema) by providing further customization of the generated code as well as bonsai-specific features. ## Getting started diff --git a/docs/workflows/serialization-example.bonsai b/docs/workflows/serialization-example.bonsai new file mode 100644 index 0000000..a6285d5 --- /dev/null +++ b/docs/workflows/serialization-example.bonsai @@ -0,0 +1,48 @@ + + + + + + Serialization + + + + + + + + + + + false + false + + + + Deserialization + + + + + + + + + + Pet.HasValue + + + + + + + + + + + + \ No newline at end of file From fa64e254fd7995342f771e3556fdc3c627c58c0e Mon Sep 17 00:00:00 2001 From: glopesdev Date: Sun, 16 Mar 2025 20:13:13 +0000 Subject: [PATCH 23/63] Include all articles in toc and add missing files --- README.md | 2 +- docs/articles/advanced-usage.md | 5 +-- docs/articles/basic-usage.md | 15 +++---- docs/articles/intro.md | 45 ------------------- docs/articles/toc.yml | 5 ++- docs/articles/why-bonsai-sgen.md | 10 ++--- docs/workflows/.gitignore | 1 + ... => person-and-dog-nested-building.bonsai} | 0 ...nsai-sgen.bonsai => person-and-dog.bonsai} | 0 9 files changed, 16 insertions(+), 67 deletions(-) delete mode 100644 docs/articles/intro.md rename docs/workflows/{personand-dog-example-nested-building-bonsai-sgen.bonsai => person-and-dog-nested-building.bonsai} (100%) rename docs/workflows/{personand-dog-example-bonsai-sgen.bonsai => person-and-dog.bonsai} (100%) diff --git a/README.md b/README.md index eba8df6..3ddfa3a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Serializer Generator Tool -Tool for automatically generating YAML / JSON serialization classes and constructor operators from schema files. +`Bonsai.Sgen` is a code generator tool for the [Bonsai](https://bonsai-rx.org/) programming language. It leverages [JSON Schema](https://json-schema.org/) as a standard to specify [record data types](https://en.wikipedia.org/wiki/Record_(computer_science)), and automatically generates operators to create and manipulate these objects. It builds on top of [NJsonSchema](https://github.com/RicoSuter/NJsonSchema) by providing further customization of the generated code as well as Bonsai-specific features. ## Getting Started diff --git a/docs/articles/advanced-usage.md b/docs/articles/advanced-usage.md index ceb7fce..653ffae 100644 --- a/docs/articles/advanced-usage.md +++ b/docs/articles/advanced-usage.md @@ -1,7 +1,4 @@ ---- -uid: advanced-usage ---- - +# Advanced Usage ## Unions diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index d782e6c..6cdff37 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -1,7 +1,4 @@ ---- -uid: basic-usage ---- - +# Basic Usage ## Automatic generation of Bonsai code using Bonsai.Sgen @@ -83,7 +80,7 @@ dotnet bonsai.sgen --schema docs/workflows/person_and_dog.json --output docs/wor ``` :::workflow -![PersonAndDog as BonsaiSgen](~/workflows/person-and-dog-example-bonsai-sgen.bonsai) +![Person And Dog](~/workflows/person-and-dog.bonsai) ::: A few things worth noting in this example: @@ -99,7 +96,7 @@ The previous example highlights the simplicity of generating Bonsai code for sim :::workflow -![PersonAndDog building as BonsaiSgen](~/workflows/person-and-dog-nested-building-example-bonsai-sgen.bonsai) +![Person And Dog Nested Building](~/workflows/person-and-dog-nested-building.bonsai) ::: ## Enums @@ -181,7 +178,7 @@ For value types, the generated code will render a [Nullable value type](https:// For reference types, the generated code will not render a nullable type since reference types are already nullable in C#. A consumer can test for `null` to determine if the value is present by simply using an `ExpressionTransform` operator with `it == null`.: :::workflow -![Nullable pet](~/workflows/person-and-pets-enum-nullable.bonsai) +![Nullable pet](~/workflows/person-and-pet-enum-nullable.bonsai) ::: ## Required fields @@ -215,7 +212,7 @@ The two operations are afforded via the `SerializeToYaml` (or `SerializeToJson`) > Remember to add the necessary package references to your `Extensions.csproj` file depending on the serializer you want to use! > ```xml > -> +> > -> +> > ``` \ No newline at end of file diff --git a/docs/articles/intro.md b/docs/articles/intro.md deleted file mode 100644 index 1ee7386..0000000 --- a/docs/articles/intro.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -uid: intro ---- - -## What is Bonsai.Sgen? - -`Bonsai.Sgen` is a code generator tool for the [Bonsai programming language](https://bonsai-rx.org/). It leverages [`json-schema`](https://json-schema.org/) as a standard to represent [record-like](https://en.wikipedia.org/wiki/Record_(computer_science)) structures, and automatically generates Bonsai-compatible isomorphic operators to create and manipulate these objects. It builds on top of the [`NJsonSchema` library](https://github.com/RicoSuter/NJsonSchema) by providing further customization of the generated code as well as bonsai-specific features. - -## Getting started - -1. Navigate to the [Bonsai.Sgen NuGet tool package](https://www.nuget.org/packages/Bonsai.Sgen/) -2. Click `.NET CLI (Local)` and copy the two suggested commands. E.g.: - - ```cmd - dotnet new tool-manifest # if you are setting up this repo - dotnet tool install --local Bonsai.Sgen - ``` - -3. To view the tool help reference documentation, run: - - ```cmd - dotnet bonsai.sgen --help - ``` - -4. To generate YAML serialization classes from a schema file: - - ```cmd - dotnet bonsai.sgen --schema schema.json --serializer YamlDotNet - ``` - -5. To generate JSON serialization classes from a schema file: - - ```cmd - dotnet bonsai.sgen --schema schema.json --serializer NewtonsoftJson - ``` - -6. Copy the generated class file to your project `Extensions` folder. - -7. Add the necessary package references to your `Extensions.csproj` file. For example: - - ```xml - - - - \ No newline at end of file diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml index ef45d5b..982cfb5 100644 --- a/docs/articles/toc.yml +++ b/docs/articles/toc.yml @@ -1 +1,4 @@ -- href: ../index.md \ No newline at end of file +- href: ../index.md +- href: why-bonsai-sgen.md +- href: basic-usage.md +- href: advanced-usage.md \ No newline at end of file diff --git a/docs/articles/why-bonsai-sgen.md b/docs/articles/why-bonsai-sgen.md index 1fb2d7a..29ac5e3 100644 --- a/docs/articles/why-bonsai-sgen.md +++ b/docs/articles/why-bonsai-sgen.md @@ -1,10 +1,6 @@ ---- -uid: why-bonsai-sgen ---- +# Why Bonsai Sgen? -## Why should I care? - -`Bonsai.Sgen` attempts to solve the problem of writing boilerplate code to create represent data structures in Bonsai. Let's try to convince you by looking at a simple example. +`Bonsai.Sgen` attempts to solve the problem of writing boilerplate code to model domain-specific data in Bonsai. Let's try to convince you by looking at a simple example. Let's we have a simple record-like object that represents a ´Person´: @@ -19,7 +15,7 @@ Let's we have a simple record-like object that represents a ´Person´: If we want to represent this object in Bonsai, we have a few alternatives: -1. Using a `DynamicClass` object: +1. Using an [`ExpressionTransform`](xref:Bonsai.Scripting.Expressions.ExpressionTransform) with a [Data Object Initializer](https://bonsai-rx.org/docs/api/Bonsai.Scripting.Expressions.ExpressionTransform.html#data-object-initializers): :::workflow ![Person as DynamicClass](~/workflows/person-example-dynamic-class.bonsai) diff --git a/docs/workflows/.gitignore b/docs/workflows/.gitignore index dfc8fe5..8d5ed47 100644 --- a/docs/workflows/.gitignore +++ b/docs/workflows/.gitignore @@ -1 +1,2 @@ +*.svg *.bonsai.layout \ No newline at end of file diff --git a/docs/workflows/personand-dog-example-nested-building-bonsai-sgen.bonsai b/docs/workflows/person-and-dog-nested-building.bonsai similarity index 100% rename from docs/workflows/personand-dog-example-nested-building-bonsai-sgen.bonsai rename to docs/workflows/person-and-dog-nested-building.bonsai diff --git a/docs/workflows/personand-dog-example-bonsai-sgen.bonsai b/docs/workflows/person-and-dog.bonsai similarity index 100% rename from docs/workflows/personand-dog-example-bonsai-sgen.bonsai rename to docs/workflows/person-and-dog.bonsai From 2a6e8d015c9bda63520c918bebff0296e2ae641d Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 16 Mar 2025 17:17:10 -0700 Subject: [PATCH 24/63] Improve clarity and consistency --- docs/articles/advanced-usage.md | 64 +++++++++++++--------------- docs/articles/basic-usage.md | 73 ++++++++++++++++---------------- docs/articles/why-bonsai-sgen.md | 22 +++++----- 3 files changed, 75 insertions(+), 84 deletions(-) diff --git a/docs/articles/advanced-usage.md b/docs/articles/advanced-usage.md index 653ffae..ade2af2 100644 --- a/docs/articles/advanced-usage.md +++ b/docs/articles/advanced-usage.md @@ -1,10 +1,10 @@ -# Advanced Usage +# Advanced usage ## Unions -In the previous examples, we have seen how create object properties of a single type. However, in practice, data structures' fields can often be represented by one of several types. We have actually seen a special case of this behavior in the previous nullable example, where a field can be either a value of a given type or `null` (or an Union between type `T` and `null`). +In the previous examples, we have seen how create object properties of a single type. However, in practice, data structures' fields can often be represented by one of several types. We have actually seen a special case of this behavior in the previous nullable example, where a field can be either a value of a given type `T` or `null` (or an Union between type `T` and `null`). -Similarly, `json-schema` allows union types to be defined using the `oneOf` keyword. For example, consider the following schema: +`json-schema` allows union types using the `oneOf` keyword. For example: ```json { @@ -21,18 +21,19 @@ Similarly, `json-schema` allows union types to be defined using the `oneOf` keyw } ``` -If we run `Bonsai.Sgen` on this schema, we will get the following signature for the `FooProperty` property: +Running `Bonsai.Sgen` on this schema generates the following type signature for `FooProperty`: ```csharp public object FooProperty ``` -This is because while the `oneOf` keyword is supported by the `Bonsai.Sgen` tool, for statically typed languages like `C#` and `Bonsai`, we need to know the exact type of the property at compile time. As a result, we opt to "up-cast" the property to the most general type that can represent all the possible types in the union (`object`). It is up to the user to down-cast the property to the correct type at runtime. +While `oneOf` is supported, statically typed languages like `C#` require the exact type at compile time. Thus, the property is "up-cast" to `object`, and users must down-cast it to the correct type at runtime. +## Tagged-Unions ## Tagged Unions -At this point, you might be wondering if there is a way to represent union types in a more type-safe way in json-schema. The answer is yes, and the way to do it is by using [`discriminated unions`](https://en.wikipedia.org/wiki/Tagged_union) (or `tagged union`). The syntax for discriminated unions is not supported by vanilla `json-schema`, but it is supported by the [`OpenAPI` standard](https://swagger.io/docs/specification/v3_0/data-models/inheritance-and-polymorphism/#discriminator), which is a superset of `json-schema`. The key idea behind discriminated unions is to add a `discriminator` field to the schema that specifies the property that will be used to determine the type of the object at runtime. +Unions types can be made type-aware by using[`tagged unions`](https://en.wikipedia.org/wiki/Tagged_union) (or `discriminated unions`). The syntax for tagged unions is not part of the `json-schema` specification, but it is supported by the [`OpenAPI` standard](https://swagger.io/docs/specification/v3_0/data-models/inheritance-and-polymorphism/#discriminator), which is a superset of `json-schema`. The key idea behind tagged unions is to add a `discriminator` field to the schema that specifies the property that will be used to determine the type of the object at runtime. For example, a `Pet` object that can be either a `Dog` or a `Cat` can be represented as follows: @@ -40,69 +41,62 @@ For example, a `Pet` object that can be either a `Dog` or a `Cat` can be represe ```json "Pet": { - "discriminator": { - "mapping": { - "cat": "#/definitions/Cat", - "dog": "#/definitions/Dog" - }, - "propertyName": "pet_type" - }, - "oneOf": [ - { - "$ref": "#/definitions/Dog" - }, - { - "$ref": "#/definitions/Cat" - } - ] - } + "discriminator": { + "mapping": { + "cat": "#/definitions/Cat", + "dog": "#/definitions/Dog" + }, + "propertyName": "pet_type" + }, + "oneOf": [ + { "$ref": "#/definitions/Dog" }, + { "$ref": "#/definitions/Cat" } + ] +} ``` In `C#`, `Bonsai.Sgen` will generate a root type `Pet` that will be inherited by the `Dog` and `Cat` types (since in the worst case scenario, the discriminated property must be shared). The `Pet` type will have a `pet_type` property that will be used to downcast to the proper type at runtime. At this point we can open our example in `Bonsai` and see how the `Pet` type is represented in the workflow. -As you can see below, we still get a `Pet` type. Better than `object` but still not a `Dog` or `Cat` type. Fortunately, `Bonsai.Sgen` will generate an operator that can be used to filter and downcast the `Pet` objects to the correct type at runtime. These are called `Match` operators. After adding a `MatchPet` to our workflow we can select the desired target type which will allow us access to the properties of the `Dog` or `Cat` type. Conversely, we can also upcast a `Dog` or `Cat` to a `Pet` leaving the `MatchPet` operator's `Type` property empty. +As you can see below, we still get a `Pet` type. Better than `object` but still not a `Dog` or `Cat` type. Fortunately, `Bonsai.Sgen` will generate an operator that can be used to filter and downcast the `Pet` objects to the correct type at runtime. These are called `Match` operators. `MatchPet` can be used to select the desired target type which will allow us access to the properties of the `Dog` or `Cat` subtypes. Conversely, we can also upcast a `Dog` or `Cat` to a `Pet` by leaving the `MatchPet` operator's `Type` property empty. :::workflow ![Discriminated Unions](~/workflows/person-pet-discriminated-union.bonsai) ::: > [!Important] -> In is general advisable to use references in the `oneOf` syntax. Not only does this decision make your `json-schema` significantly smaller, it will also help `Bonsai.Sgen` generate the correct class hierarchy if multiple unions are present in the schema. If you use inline objects, `Bonsai.Sgen` will likely have to generate a new root class for each union, which can lead to a lot of duplicated code and a more complex object hierarchy. +> In is advisable to use references in the `oneOf` syntax. Not only does this decision make your `json-schema` significantly smaller, it will also help `Bonsai.Sgen` generate the correct class hierarchy if multiple unions are present in the schema. If you use inline objects, `Bonsai.Sgen` will likely have to generate a new root class for each union, which can lead to a lot of duplicated code and a more complex object hierarchy. ## Extending generated code with `partial` classes -Since `Bonsai.Sgen` will generate proper `class` for each object in the schema, it is possible to use these types to create custom operators and methods using the `Scriping Extensions` feature of `Bonsai`. However, sometimes we may want to extend the features of the generated classes directly... - -For those that inspected the general `C#` code, you will notice that all classes are marked as [`partial`](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods). This is a feature of `C#` that allows a class to be split. This was a deliberate design choice to allow users to extend the generated code. However, because it is usually always a bad idea to modify generated code directly (e.g. we may want to regenerate it in the future), `partial` classes allows modification to be made in a separate file. +Generated classes are marked as [`partial`](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods), allowing you to extend them without modifying the generated code directly. -Suppose we want to sum `Cats`, we can overload the operator with a small method in a separate file:: +For example, to add an operator for summing `Cat` objects: ```csharp namespace PersonAndDiscriminatedPets { - partial class Cat{ + partial class Cat + { public static Cat operator +(Cat c1, Cat c2) { return new Cat { - CanMeow = c1.CanMeow || c2.CanMeow, - Age = c1.Age + c2.Age + CanMeow = c1.CanMeow || c2.CanMeow, + Age = c1.Age + c2.Age }; } } } ``` -In `Bonsai`, we can now use the `Add` operator to sum `Cats`: - +In Bonsai, use the `Add` operator to sum `Cat` objects: :::workflow ![Discriminated Unions](~/workflows/sum-cats.bonsai) ::: - ## Other supported tags -- `x-abstract`: This tag is used to mark a class as abstract. An abstract class will not be generated as an operator in Bonsai. This may be useful for root classes of unions that may never need to be manipulated in Bonsai. \ No newline at end of file +- `x-abstract`: Marks a class as abstract, preventing it from being generated as an operator in Bonsai. diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index 6cdff37..f3ecc13 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -1,10 +1,10 @@ -# Basic Usage +# Basic usage ## Automatic generation of Bonsai code using Bonsai.Sgen -We will expand this example later on, but for now, let's see how we can use `Bonsai.Sgen` to automatically generate the Bonsai code for the `Person` object. +We will expand this example later, but for now, let's see how to use `Bonsai.Sgen` to automatically generate Bonsai code for the `Person` object. -First, we need to define the schema of the object in a JSON file: +First, define the schema of the object in a JSON file: [person](~/workflows/person.json) @@ -21,25 +21,25 @@ First, we need to define the schema of the object in a JSON file: } ``` -Second, we need to run the `Bonsai.Sgen` tool to generate the Bonsai code: +Next, run the `Bonsai.Sgen` tool to generate the Bonsai code: ```cmd dotnet bonsai.sgen --schema docs/workflows/person.json --output docs/workflows/Extensions/PersonSgen.cs ``` -Finally, we can use the generated code in our Bonsai workflow: +Finally, use the generated code in your Bonsai workflow: :::workflow ![Person as BonsaiSgen](~/workflows/person-example-bonsai-sgen.bonsai) ::: -As you can probably tell, the `Bonsai.Sgen` approach is much more concise and less error-prone than the previous ones. It allows you to focus on the data structure itself and not on the boilerplate code required to create it in Bonsai. Moreover, as we will see later, the tool also automatically generates serialization and deserialization boilerplate code for the object, which can be very useful when working with external data sources. +The `Bonsai.Sgen` approach is concise and less error-prone, allowing you to focus on the data structure itself rather than boilerplate code. Additionally, the tool automatically generates, type-aware, serialization and deserialization operators, which are useful when working with external data sources. -Finally, if one considers the `json-schema` as the "source of truth" for the data structure representation, it is possible to generate multiple representations of the object in different languages, ensuring interoperability. This can be very useful when working in a multi-language environment (e.g. running experiment in Bonsai and analysis in Python) and when sharing data structures across different projects. +By treating the `json-schema` as the "source of truth," you can generate multiple representations of the object in different languages, ensuring interoperability. This is particularly useful in multi-language environments (e.g., running experiments in Bonsai and analysis in Python) and when sharing data structures across projects. ## Multiple objects -The previous example shows how a single record can be modelled. In practice, projects often require modelling different types of objects. This is where `Bonsai.Sgen` shines, as it allows you to generate multiple objects from a single schema file: +The previous example demonstrates modeling a single record. In practice, projects often require modeling multiple object types. This is where `Bonsai.Sgen` excels, allowing you to generate multiple objects from a single schema file: [person_and_dog](~/workflows/person-and-dog.json) @@ -88,13 +88,12 @@ A few things worth noting in this example: - The schema file contains two definitions: `Person` and `Dog` that give rise to two operators (`Person` and `Dog`) in the generated code. - A third definition `PersonAndPet` is used to combine the two objects into a single record. This can be omitted as we will see later by using the `x-abstract` property. - The `--namespace` flag is used to specify the namespace of the generated code. This is useful to prevent name clashes between different schemas (e.g. `PersonAndDog.Person` and `Person` from the previous example). -- Both `Person` and `Dog` `json-schema` objects are passed as references. This is extremely important as it allows the reuse of the same definition in multiple places. This is particularly useful when working with complex data structures that share common fields. If definitions are passed in-line (i.e. redefined each time), Bonsai.Sgen may not be able to correctly identify them as the same object. +- Both `Person` and `Dog` are passed as references. If definitions are instead passed in-line (i.e. redefined each time), Bonsai.Sgen may not be able to correctly identify them as the same object, and may thus generate multiple classes of the same object. ## Nested objects The previous example highlights the simplicity of generating Bonsai code for simple objects. However, the real power of `Bonsai.Sgen` comes when dealing with more complex data structures, such as nested objects. Moreover, Bonsai syntax lends itself quite nicely to represent these nested structures as well as build them: - :::workflow ![Person And Dog Nested Building](~/workflows/person-and-dog-nested-building.bonsai) ::: @@ -109,18 +108,19 @@ We can replace the `Pet` object in the previous example with an [`enum`](https:/ ```json (...) - { - "Pet": { - "title": "Pet", - "type": "string", - "enum": ["Dog", "Cat", "Fish", "Bird", "Reptile"] - } - }, - "type": "object", - "properties": { - "owner": {"$ref": "#/definitions/Person"}, - "pet": {"$ref": "#/definitions/Pet"} + +{ + "Pet": { + "title": "Pet", + "type": "string", + "enum": ["Dog", "Cat", "Fish", "Bird", "Reptile"] } +}, +"type": "object", +"properties": { + "owner": {"$ref": "#/definitions/Person"}, + "pet": {"$ref": "#/definitions/Pet"} +} ``` In Bonsai, they can be manipulated as [`Enum`](https://learn.microsoft.com/en-us/dotnet/api/system.enum?view=net-9.0) types: @@ -148,10 +148,10 @@ In Bonsai, they can be manipulated as [`Enum`](https://learn.microsoft.com/en-us `Bonsai.Sgen` also supports the generation of lists using the `array` type in the `json-schema`: ```json - "pets": { - "type": "array", - "items": {"$ref": "#/definitions/Pet"} - } +"pets": { + "type": "array", + "items": {"$ref": "#/definitions/Pet"} +} ``` `json-schema` `array`s will be rendered as [`List`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1?view=net-9.0) in the generated code and can be manipulated (and created) as such. @@ -165,17 +165,17 @@ In Bonsai, they can be manipulated as [`Enum`](https://learn.microsoft.com/en-us `json-schema` supports the `null` type, which can be used to represent nullable types. The standard is a bit loose in this regard, but `Bonsai.Sgen` will generate a nullable-T if the json-schema represents it using the `oneOf` keyword: ```json - "pet": { - "oneOf": [ - {"$ref": "#/definitions/Pet"}, - {"type": "null"} - ] - } +"pet": { + "oneOf": [ + {"$ref": "#/definitions/Pet"}, + {"type": "null"} + ] +} ``` For value types, the generated code will render a [Nullable value type](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/nullable-value-types) type. This type will expose two properties: `HasValue` and `Value`, that can be used to test and manipulate the type, respectively. -For reference types, the generated code will not render a nullable type since reference types are already nullable in C#. A consumer can test for `null` to determine if the value is present by simply using an `ExpressionTransform` operator with `it == null`.: +For reference types, the generated code will not render a nullable type since reference types are already nullable in C#. A consumer can test for `null` to determine if the value is present by simply using an `ExpressionTransform` operator with `it == null`: :::workflow ![Nullable pet](~/workflows/person-and-pet-enum-nullable.bonsai) @@ -186,20 +186,19 @@ For reference types, the generated code will not render a nullable type since re `json-schema` supports the [`required`](https://json-schema.org/learn/getting-started-step-by-step#define-required-properties) keyword to specify which fields are required. By default, all fields are optional. This can be useful to enforce the presence of certain fields in the object at deserialization time. However, `Bonsai.Sgen` will not generate any code to enforce this requirement during object construction, only at deserialization. It is up to the user to ensure that the object is correctly populated before using it. > [!Note] -> Some confusion maybe arise about the distinction between `null` and `required`. This is all the more confusing since different languages and libraries may refer to these concepts differently. For the sake of this tool (and honestly in general) the following definitions are used: - +> Some confusion may arise about the distinction between `null` and `required`. This is all the more confusing since different languages and libraries may refer to these concepts differently. For the sake of this tool (and honestly in general) the following definitions are used: +> > - `nullable` means that the field can be `null` or type `T` > - `required` means that the field must be present in the object at deserialization time > - An object can be `nullable` and `required` at the same time. This means it MUST be defined in the object, but it can be defined as `null`. > - An object can be `not required` and `nullable`. This does NOT mean that the object is, by default, `null`. It means that the object should have a default value, which can in theory be `null`. > - An object can be `not required` and `not nullable`. This means that the object must have a default value, which cannot be `null`. - ## Serialization and Deserialization -One of the biggest perks of using json-schema to represent our objects is the guaranteed that all records are (de)serializable. This means that we can go from a text-based format (great specification and logging) to a `C#` type seamlessly, and vice-versa. `Bonsai.Sgen` will optionally generate (de)serialization operators for all objects in the schema if the `--serializer` property is not `None`. Currently, two formats are supported out of the box: `Json` (via [`NewtonsoftJson`](https://github.com/JamesNK/Newtonsoft.Json)) and `yaml` (via [`YamlDotNet`](https://github.com/aaubry/YamlDotNet)). +One of the biggest perks of using json-schema to represent our objects is the guarantee that all records are (de)serializable. This means that we can go from a text-based format (great for specification and logging) to a `C#` type seamlessly, and vice-versa. `Bonsai.Sgen` will optionally generate (de)serialization operators for all objects in the schema if the `--serializer` property is not `None`. Currently, two formats are supported out of the box: `Json` (via [`NewtonsoftJson`](https://github.com/JamesNK/Newtonsoft.Json)) and `yaml` (via [`YamlDotNet`](https://github.com/aaubry/YamlDotNet)). -The two operations are afforded via the `SerializeToYaml` (or `SerializeToJson`) and `DeserializeFromYaml` (or `DeserializeFromJson`) operators, respectively. +The two operations are afforded via the `SerializeToYaml` (or `SerializeToJson`) and `DeserializeFromYaml` (or `DeserializeFromJson`) operators, respectively. `SerializeToYaml` will take a `T` object (known to the namespace) and return a `string` representation of the object. `DeserializeFromYaml` will take a `string` and return a `T` object. If validation fails, the operator will throw an exception. diff --git a/docs/articles/why-bonsai-sgen.md b/docs/articles/why-bonsai-sgen.md index 29ac5e3..7933339 100644 --- a/docs/articles/why-bonsai-sgen.md +++ b/docs/articles/why-bonsai-sgen.md @@ -1,19 +1,17 @@ # Why Bonsai Sgen? -`Bonsai.Sgen` attempts to solve the problem of writing boilerplate code to model domain-specific data in Bonsai. Let's try to convince you by looking at a simple example. +`Bonsai.Sgen` addresses the challenge of writing boilerplate code to model domain-specific data in Bonsai. Let's explore this with a simple example. -Let's we have a simple record-like object that represents a ´Person´: +Suppose we have a record-like object that represents a `Person`: - -| Field Name | Type | Description | -|------------|----------|---------------------------| -| age | int | The age of a person | +| Field Name | Type | Description | +|------------|----------|------------------------------| +| age | int | The age of a person | | first_name | string | The first name of the person | | last_name | string | The last name of the person | -| dob | datetime | Date of birth | - +| dob | datetime | Date of birth | -If we want to represent this object in Bonsai, we have a few alternatives: +To represent this object in Bonsai, we have a few options: 1. Using an [`ExpressionTransform`](xref:Bonsai.Scripting.Expressions.ExpressionTransform) with a [Data Object Initializer](https://bonsai-rx.org/docs/api/Bonsai.Scripting.Expressions.ExpressionTransform.html#data-object-initializers): @@ -21,7 +19,7 @@ If we want to represent this object in Bonsai, we have a few alternatives: ![Person as DynamicClass](~/workflows/person-example-dynamic-class.bonsai) ::: -This approach is rather brittle as the representation of the record does not exist as a "first class citizen" and only at compile-time. This has a few implications one of which is the inability to create [Subject Sources](https://bonsai-rx.org/docs/articles/subjects.html#source-subjects) from the type. +This approach is brittle because the record representation exists only at compile-time and not as a "first-class citizen." This limitation prevents the creation of [Subject Sources](https://bonsai-rx.org/docs/articles/subjects.html#source-subjects) from the type. 2. Modeling the object as a C# class using [Scripting Extensions](https://bonsai-rx.org/docs/articles/scripting-extensions.html): @@ -35,7 +33,7 @@ public class Person } ``` -This approach is more robust than the previous one, but it requires writing additional, boilerplate code to allow the creation of the object in Bonsai: +While more robust, this approach requires additional boilerplate code to enable object creation in Bonsai: ```Csharp using Bonsai; @@ -63,4 +61,4 @@ public class CreatePerson : Source } ``` -As you can probably tell, neither of these approaches is ideal when it comes to scale large projects. This is where `Bonsai.Sgen` comes in. \ No newline at end of file +As you can see, neither approach scales well for large projects. This is where `Bonsai.Sgen` comes in. \ No newline at end of file From cf8bb1732524e252a69c875a4a7934ce992524b5 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Mon, 17 Mar 2025 08:52:06 -0700 Subject: [PATCH 25/63] Change title --- docs/articles/why-bonsai-sgen.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/articles/why-bonsai-sgen.md b/docs/articles/why-bonsai-sgen.md index 7933339..2ae723c 100644 --- a/docs/articles/why-bonsai-sgen.md +++ b/docs/articles/why-bonsai-sgen.md @@ -1,4 +1,6 @@ -# Why Bonsai Sgen? +# Why use Bonsai.Sgen? + +## Data schemas `Bonsai.Sgen` addresses the challenge of writing boilerplate code to model domain-specific data in Bonsai. Let's explore this with a simple example. From 3ad39cc68682723dfdb2808cc864b6f709511155 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Mon, 17 Mar 2025 09:07:05 -0700 Subject: [PATCH 26/63] Improve clarity and formatting --- docs/articles/advanced-usage.md | 8 ++-- docs/articles/basic-usage.md | 8 ++-- docs/articles/why-bonsai-sgen.md | 68 ++++++++++++++++---------------- 3 files changed, 41 insertions(+), 43 deletions(-) diff --git a/docs/articles/advanced-usage.md b/docs/articles/advanced-usage.md index ade2af2..26281d6 100644 --- a/docs/articles/advanced-usage.md +++ b/docs/articles/advanced-usage.md @@ -31,9 +31,7 @@ While `oneOf` is supported, statically typed languages like `C#` require the exa ## Tagged-Unions -## Tagged Unions - -Unions types can be made type-aware by using[`tagged unions`](https://en.wikipedia.org/wiki/Tagged_union) (or `discriminated unions`). The syntax for tagged unions is not part of the `json-schema` specification, but it is supported by the [`OpenAPI` standard](https://swagger.io/docs/specification/v3_0/data-models/inheritance-and-polymorphism/#discriminator), which is a superset of `json-schema`. The key idea behind tagged unions is to add a `discriminator` field to the schema that specifies the property that will be used to determine the type of the object at runtime. +Unions types can be made type-aware by using [`tagged unions`](https://en.wikipedia.org/wiki/Tagged_union) (or `discriminated unions`). The syntax for tagged unions is not part of the `json-schema` specification, but it is supported by the [`OpenAPI` standard](https://swagger.io/docs/specification/v3_0/data-models/inheritance-and-polymorphism/#discriminator), which is a superset of `json-schema`. The key idea behind tagged unions is to add a `discriminator` field to the schema that specifies the property that will be used to determine the type of the object at runtime. For example, a `Pet` object that can be either a `Dog` or a `Cat` can be represented as follows: @@ -64,7 +62,7 @@ As you can see below, we still get a `Pet` type. Better than `object` but still ::: > [!Important] -> In is advisable to use references in the `oneOf` syntax. Not only does this decision make your `json-schema` significantly smaller, it will also help `Bonsai.Sgen` generate the correct class hierarchy if multiple unions are present in the schema. If you use inline objects, `Bonsai.Sgen` will likely have to generate a new root class for each union, which can lead to a lot of duplicated code and a more complex object hierarchy. +> In is strongly recommended to use references with the `oneOf` syntax. Not only does this decision make your `json-schema` significantly smaller, it will also help `Bonsai.Sgen` generate the correct class hierarchy if multiple unions are present in the schema. If you use inline objects, `Bonsai.Sgen` will likely have to generate a new root class for each union, which can lead to a lot of duplicated code and a more complex object hierarchy. @@ -97,6 +95,6 @@ In Bonsai, use the `Add` operator to sum `Cat` objects: ![Discriminated Unions](~/workflows/sum-cats.bonsai) ::: -## Other supported tags +## Supported tags - `x-abstract`: Marks a class as abstract, preventing it from being generated as an operator in Bonsai. diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index f3ecc13..dde4765 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -23,7 +23,7 @@ First, define the schema of the object in a JSON file: Next, run the `Bonsai.Sgen` tool to generate the Bonsai code: -```cmd +```shell dotnet bonsai.sgen --schema docs/workflows/person.json --output docs/workflows/Extensions/PersonSgen.cs ``` @@ -33,7 +33,7 @@ Finally, use the generated code in your Bonsai workflow: ![Person as BonsaiSgen](~/workflows/person-example-bonsai-sgen.bonsai) ::: -The `Bonsai.Sgen` approach is concise and less error-prone, allowing you to focus on the data structure itself rather than boilerplate code. Additionally, the tool automatically generates, type-aware, serialization and deserialization operators, which are useful when working with external data sources. +The `Bonsai.Sgen` approach is concise and less error-prone, allowing you to focus on the data structure itself rather than boilerplate code. Additionally, the tool automatically generates, type-aware, [serialization and deserialization operators](#serialization-and-deserialization), which are useful when working with external data sources. By treating the `json-schema` as the "source of truth," you can generate multiple representations of the object in different languages, ensuring interoperability. This is particularly useful in multi-language environments (e.g., running experiments in Bonsai and analysis in Python) and when sharing data structures across projects. @@ -75,7 +75,7 @@ The previous example demonstrates modeling a single record. In practice, project } ``` -```cmd +```shell dotnet bonsai.sgen --schema docs/workflows/person_and_dog.json --output docs/workflows/Extensions/PersonAndDogSgen.cs --namespace PersonAndDog ``` @@ -92,7 +92,7 @@ A few things worth noting in this example: ## Nested objects -The previous example highlights the simplicity of generating Bonsai code for simple objects. However, the real power of `Bonsai.Sgen` comes when dealing with more complex data structures, such as nested objects. Moreover, Bonsai syntax lends itself quite nicely to represent these nested structures as well as build them: +The real power of `Bonsai.Sgen` comes when dealing with more complex data structures, such as nested objects. Bonsai syntax lends itself quite nicely to represent, as well as compose and manipulate them: :::workflow ![Person And Dog Nested Building](~/workflows/person-and-dog-nested-building.bonsai) diff --git a/docs/articles/why-bonsai-sgen.md b/docs/articles/why-bonsai-sgen.md index 2ae723c..6c86cd2 100644 --- a/docs/articles/why-bonsai-sgen.md +++ b/docs/articles/why-bonsai-sgen.md @@ -17,50 +17,50 @@ To represent this object in Bonsai, we have a few options: 1. Using an [`ExpressionTransform`](xref:Bonsai.Scripting.Expressions.ExpressionTransform) with a [Data Object Initializer](https://bonsai-rx.org/docs/api/Bonsai.Scripting.Expressions.ExpressionTransform.html#data-object-initializers): -:::workflow -![Person as DynamicClass](~/workflows/person-example-dynamic-class.bonsai) -::: + :::workflow + ![Person as DynamicClass](~/workflows/person-example-dynamic-class.bonsai) + ::: -This approach is brittle because the record representation exists only at compile-time and not as a "first-class citizen." This limitation prevents the creation of [Subject Sources](https://bonsai-rx.org/docs/articles/subjects.html#source-subjects) from the type. + This approach is brittle because the record representation exists only at compile-time and not as a "first-class citizen". For instance, this limitation prevents the creation of [Subject Sources](https://bonsai-rx.org/docs/articles/subjects.html#source-subjects) from the type. 2. Modeling the object as a C# class using [Scripting Extensions](https://bonsai-rx.org/docs/articles/scripting-extensions.html): -```Csharp -public class Person -{ - public int Age; - public string FirstName; - public string LastName; - public DateTime DOB; -} -``` + ```Csharp + public class Person + { + public int Age; + public string FirstName; + public string LastName; + public DateTime DOB; + } + ``` -While more robust, this approach requires additional boilerplate code to enable object creation in Bonsai: + While more robust, this approach requires additional boilerplate code to enable object creation in Bonsai: -```Csharp -using Bonsai; -using System; -using System.Reactive.Linq; + ```Csharp + using Bonsai; + using System; + using System.Reactive.Linq; -public class CreatePerson : Source -{ + public class CreatePerson : Source + { - public int Age { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - public DateTime DOB { get; set; } + public int Age { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public DateTime DOB { get; set; } - public override IObservable Generate() - { - return Observable.Return(new Person + public override IObservable Generate() { - Age = Age, - FirstName = FirstName, - LastName = LastName, - DOB = DOB - }); + return Observable.Return(new Person + { + Age = Age, + FirstName = FirstName, + LastName = LastName, + DOB = DOB + }); + } } -} -``` + ``` As you can see, neither approach scales well for large projects. This is where `Bonsai.Sgen` comes in. \ No newline at end of file From 44d266345aebbd1514c11aa12ea502af4302d9e8 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Mon, 17 Mar 2025 09:15:05 -0700 Subject: [PATCH 27/63] Add missing workflow --- docs/workflows/person-and-pet-enum.bonsai | 28 +++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/workflows/person-and-pet-enum.bonsai b/docs/workflows/person-and-pet-enum.bonsai index eb14530..a4b6fc4 100644 --- a/docs/workflows/person-and-pet-enum.bonsai +++ b/docs/workflows/person-and-pet-enum.bonsai @@ -1,9 +1,33 @@  - - + + + + + + Pet + + + + Dog + + + + + + + Value + Dog + + + + + + + \ No newline at end of file From 47ba15e8cbc3e0fcb1263aebbf089cd308d3a1e7 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 20 Mar 2025 09:38:53 -0700 Subject: [PATCH 28/63] Refactor from @banchan86 comments --- docs/articles/advanced-usage.md | 2 +- docs/articles/basic-usage.md | 12 ++++++++---- docs/articles/why-bonsai-sgen.md | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/articles/advanced-usage.md b/docs/articles/advanced-usage.md index 26281d6..30c8479 100644 --- a/docs/articles/advanced-usage.md +++ b/docs/articles/advanced-usage.md @@ -68,7 +68,7 @@ As you can see below, we still get a `Pet` type. Better than `object` but still ## Extending generated code with `partial` classes -Generated classes are marked as [`partial`](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods), allowing you to extend them without modifying the generated code directly. +Generated classes are marked as [`partial`](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods), allowing you to extend them without modifying the generated code directly. This can be done by placing the new `.cs` file in the [`Extensions`](https://bonsai-rx.org/docs/articles/scripting-extensions.html) folder of your project. For example, to add an operator for summing `Cat` objects: diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index dde4765..226038e 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -1,5 +1,9 @@ # Basic usage +> [!TIP] +> It is strongly recommend to be familiar with [Bonsai Scripting Extensions](https://bonsai-rx.org/docs/articles/scripting-extensions.html) before using this tool. + + ## Automatic generation of Bonsai code using Bonsai.Sgen We will expand this example later, but for now, let's see how to use `Bonsai.Sgen` to automatically generate Bonsai code for the `Person` object. @@ -24,7 +28,7 @@ First, define the schema of the object in a JSON file: Next, run the `Bonsai.Sgen` tool to generate the Bonsai code: ```shell -dotnet bonsai.sgen --schema docs/workflows/person.json --output docs/workflows/Extensions/PersonSgen.cs +dotnet bonsai.sgen --schema person.json --output Extensions/PersonSgen.cs ``` Finally, use the generated code in your Bonsai workflow: @@ -76,7 +80,7 @@ The previous example demonstrates modeling a single record. In practice, project ``` ```shell -dotnet bonsai.sgen --schema docs/workflows/person_and_dog.json --output docs/workflows/Extensions/PersonAndDogSgen.cs --namespace PersonAndDog +dotnet bonsai.sgen --schema person-and-dog.json --output Extensions/PersonAndDogSgen.cs --namespace PersonAndDog ``` :::workflow @@ -86,7 +90,7 @@ dotnet bonsai.sgen --schema docs/workflows/person_and_dog.json --output docs/wor A few things worth noting in this example: - The schema file contains two definitions: `Person` and `Dog` that give rise to two operators (`Person` and `Dog`) in the generated code. -- A third definition `PersonAndPet` is used to combine the two objects into a single record. This can be omitted as we will see later by using the `x-abstract` property. +- A third definition `PersonAndPet` is used to combine the two objects into a single record. - The `--namespace` flag is used to specify the namespace of the generated code. This is useful to prevent name clashes between different schemas (e.g. `PersonAndDog.Person` and `Person` from the previous example). - Both `Person` and `Dog` are passed as references. If definitions are instead passed in-line (i.e. redefined each time), Bonsai.Sgen may not be able to correctly identify them as the same object, and may thus generate multiple classes of the same object. @@ -175,7 +179,7 @@ In Bonsai, they can be manipulated as [`Enum`](https://learn.microsoft.com/en-us For value types, the generated code will render a [Nullable value type](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/nullable-value-types) type. This type will expose two properties: `HasValue` and `Value`, that can be used to test and manipulate the type, respectively. -For reference types, the generated code will not render a nullable type since reference types are already nullable in C#. A consumer can test for `null` to determine if the value is present by simply using an `ExpressionTransform` operator with `it == null`: +For reference types, the generated code will not render a nullable type since reference types are already nullable in C#. A data consumer application can test for `null` to determine if the value is present by simply using an `ExpressionTransform` operator with `it == null`: :::workflow ![Nullable pet](~/workflows/person-and-pet-enum-nullable.bonsai) diff --git a/docs/articles/why-bonsai-sgen.md b/docs/articles/why-bonsai-sgen.md index 6c86cd2..116702c 100644 --- a/docs/articles/why-bonsai-sgen.md +++ b/docs/articles/why-bonsai-sgen.md @@ -15,7 +15,7 @@ Suppose we have a record-like object that represents a `Person`: To represent this object in Bonsai, we have a few options: -1. Using an [`ExpressionTransform`](xref:Bonsai.Scripting.Expressions.ExpressionTransform) with a [Data Object Initializer](https://bonsai-rx.org/docs/api/Bonsai.Scripting.Expressions.ExpressionTransform.html#data-object-initializers): +1. Using an [`ExpressionTransform`](xref:Bonsai.Scripting.Expressions.ExpressionTransform) with a [Data Object Initializer](xref:Bonsai.Scripting.Expressions.ExpressionTransform#data-object-initializers): :::workflow ![Person as DynamicClass](~/workflows/person-example-dynamic-class.bonsai) From e0fa627fecf16ce00bb28348c1c658d9e592918d Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 10:54:39 +0000 Subject: [PATCH 29/63] Add introduction --- docs/articles/intro.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/articles/intro.md diff --git a/docs/articles/intro.md b/docs/articles/intro.md new file mode 100644 index 0000000..65c713f --- /dev/null +++ b/docs/articles/intro.md @@ -0,0 +1,7 @@ +--- +uid: intro +--- + +## What is Bonsai.Sgen? + +`Bonsai.Sgen` is a code generator tool for the [Bonsai programming language](https://bonsai-rx.org/). It leverages [`json-schema`](https://json-schema.org/) as a standard to represent [record-like](https://en.wikipedia.org/wiki/Record_(computer_science)) structures, and automatically generates Bonsai-compatible isomorphic operators to create and manipulate these objects. From 8db6f4802682f7039973aae6ba8b35a7efad28d0 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 10:54:47 +0000 Subject: [PATCH 30/63] Add scaffold for articles --- docs/articles/advanced-usage.md | 0 docs/articles/basic-usage.md | 0 docs/articles/experimental.md | 0 docs/articles/getting-started.md | 101 +++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+) create mode 100644 docs/articles/advanced-usage.md create mode 100644 docs/articles/basic-usage.md create mode 100644 docs/articles/experimental.md create mode 100644 docs/articles/getting-started.md diff --git a/docs/articles/advanced-usage.md b/docs/articles/advanced-usage.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/articles/experimental.md b/docs/articles/experimental.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/articles/getting-started.md b/docs/articles/getting-started.md new file mode 100644 index 0000000..330f91a --- /dev/null +++ b/docs/articles/getting-started.md @@ -0,0 +1,101 @@ +--- +uid: getting-started +--- + +## Why should I care? + +`Bonsai.Sgen` attempts to solve the problem of writing boilerplate code to create represent data structures in Bonsai. Let's try to convince you by looking at a simple example. + +Let's we have a simple record-like object that represents a ´Person´: + + +| Field Name | Type | Description | +|------------|----------|---------------------------| +| age | int | The age of a person | +| first_name | string | The first name of the person | +| last_name | string | The last name of the person | +| dob | datetime | Date of birth | + + +If we want to represent this object in Bonsai, we have a few alternatives: + +1. Using a `DynamicClass` object: + +:::workflow +![Person as DynamicClass](~/workflows/person-example-dynamic-class.bonsai) +::: + +This approach is rather brittle as the representation of the record does not exist as a "first class citizen" and only at compile-time. This has a few implications one of which is the inability to create [Subject Sources](https://bonsai-rx.org/docs/articles/subjects.html#source-subjects) from the type. + +2. Modeling the object as a C# class using [Scripting Extensions](https://bonsai-rx.org/docs/articles/scripting-extensions.html): + +```Csharp +public class Person +{ + public int Age; + public string FirstName; + public string LastName; + public DateTime DOB; +} +``` + +This approach is more robust than the previous one, but it requires writing additional, boilerplate code to allow the creation of the object in Bonsai: + +```Csharp +using Bonsai; +using System; +using System.Reactive.Linq; + +public class CreatePerson : Source +{ + + public int Age { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public DateTime DOB { get; set; } + + public override IObservable Generate() + { + return Observable.Return(new Person + { + Age = Age, + FirstName = FirstName, + LastName = LastName, + DOB = DOB + }); + } +} +``` + +As you can probably tell, neither of these approaches is ideal when it comes to scale large projects. This is where `Bonsai.Sgen` comes in. + + +## Automatic generation of Bonsai code using Bonsai.Sgen + +We will expand this example later on, but for now, let's see how we can use `Bonsai.Sgen` to automatically generate the Bonsai code for the `Person` object. + +First, we need to define the schema of the object in a JSON file: + +```json +{ + "name": "Person", + "fields": [ + { + "name": "Age", + "type": "int", + }, + { + "name": "FirstName", + "type": "string", + }, + { + "name": "LastName", + "type": "string", + }, + { + "name": "DOB", + "type": "datetime", + } + ] +} +``` \ No newline at end of file From 14f63488efdd57b47f48f2fc7e68b5f6088d65e9 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 10:54:58 +0000 Subject: [PATCH 31/63] Add Extensions to example workflow --- docs/workflows/Extensions.csproj | 11 ++++++++ docs/workflows/Extensions/CreatePerson.cs | 31 +++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 docs/workflows/Extensions.csproj create mode 100644 docs/workflows/Extensions/CreatePerson.cs diff --git a/docs/workflows/Extensions.csproj b/docs/workflows/Extensions.csproj new file mode 100644 index 0000000..250d9b7 --- /dev/null +++ b/docs/workflows/Extensions.csproj @@ -0,0 +1,11 @@ + + + + net472 + + + + + + + \ No newline at end of file diff --git a/docs/workflows/Extensions/CreatePerson.cs b/docs/workflows/Extensions/CreatePerson.cs new file mode 100644 index 0000000..f0eb019 --- /dev/null +++ b/docs/workflows/Extensions/CreatePerson.cs @@ -0,0 +1,31 @@ +using Bonsai; +using System; +using System.Reactive.Linq; + +public class CreatePerson : Source +{ + + public int Age { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public DateTime DOB { get; set; } + + public override IObservable Generate() + { + return Observable.Return(new Person + { + Age = Age, + FirstName = FirstName, + LastName = LastName, + DOB = DOB + }); + } +} + +public class Person +{ + public int Age; + public string FirstName; + public string LastName; + public DateTime DOB; +} From 3e43650d50b9b0c235f89c3721380731db037214 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 10:55:14 +0000 Subject: [PATCH 32/63] Add dynamic-class example --- .../person-example-dynamic-class.bonsai | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 docs/workflows/person-example-dynamic-class.bonsai diff --git a/docs/workflows/person-example-dynamic-class.bonsai b/docs/workflows/person-example-dynamic-class.bonsai new file mode 100644 index 0000000..e6ddc50 --- /dev/null +++ b/docs/workflows/person-example-dynamic-class.bonsai @@ -0,0 +1,45 @@ + + + + + + + 0 + + + + + + + + + + + 0001-01-01T00:00:00 + + + + + + + new( +Item1 as Age, +Item2 as FirstName, +Item3 as LastName, +Item4 as DOB +) + + + + + + + + + + + \ No newline at end of file From e12394d9ba92ed7d9499c8b4401715d51d96f7cb Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 11:00:36 +0000 Subject: [PATCH 33/63] Add getting started instructions --- docs/articles/intro.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/articles/intro.md b/docs/articles/intro.md index 65c713f..85d8603 100644 --- a/docs/articles/intro.md +++ b/docs/articles/intro.md @@ -5,3 +5,41 @@ uid: intro ## What is Bonsai.Sgen? `Bonsai.Sgen` is a code generator tool for the [Bonsai programming language](https://bonsai-rx.org/). It leverages [`json-schema`](https://json-schema.org/) as a standard to represent [record-like](https://en.wikipedia.org/wiki/Record_(computer_science)) structures, and automatically generates Bonsai-compatible isomorphic operators to create and manipulate these objects. + +## Getting started + +1. Navigate to the [Bonsai.Sgen NuGet tool package](https://www.nuget.org/packages/Bonsai.Sgen/) +2. Click `.NET CLI (Local)` and copy the two suggested commands. E.g.: + + ```cmd + dotnet new tool-manifest # if you are setting up this repo + dotnet tool install --local Bonsai.Sgen + ``` + +3. To view the tool help reference documentation, run: + + ```cmd + dotnet bonsai.sgen --help + ``` + +4. To generate YAML serialization classes from a schema file: + + ```cmd + dotnet bonsai.sgen --schema schema.json --serializer YamlDotNet + ``` + +5. To generate JSON serialization classes from a schema file: + + ```cmd + dotnet bonsai.sgen --schema schema.json --serializer NewtonsoftJson + ``` + +6. Copy the generated class file to your project `Extensions` folder. + +7. Add the necessary package references to your `Extensions.csproj` file. For example: + + ```xml + + + + \ No newline at end of file From fc83c056fdda7320b31f9554f60bc7edb4f18921 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 11:06:29 +0000 Subject: [PATCH 34/63] Add Yaml and Json package references to Extensions csproj --- docs/workflows/Extensions.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/workflows/Extensions.csproj b/docs/workflows/Extensions.csproj index 250d9b7..8d17170 100644 --- a/docs/workflows/Extensions.csproj +++ b/docs/workflows/Extensions.csproj @@ -6,6 +6,8 @@ + + \ No newline at end of file From 9a9bba4b9a910a1a834291d7f3c13a56717152bf Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 11:08:36 +0000 Subject: [PATCH 35/63] Change title --- docs/articles/{getting-started.md => why-bonsai-sgen.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/articles/{getting-started.md => why-bonsai-sgen.md} (99%) diff --git a/docs/articles/getting-started.md b/docs/articles/why-bonsai-sgen.md similarity index 99% rename from docs/articles/getting-started.md rename to docs/articles/why-bonsai-sgen.md index 330f91a..da779dd 100644 --- a/docs/articles/getting-started.md +++ b/docs/articles/why-bonsai-sgen.md @@ -1,5 +1,5 @@ --- -uid: getting-started +uid: why-bonsai-sgen --- ## Why should I care? From 3046c137cab032474356397568fc4e5ce6738ec2 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 11:11:52 +0000 Subject: [PATCH 36/63] Add environment package requirements --- .bonsai/Bonsai.config | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.bonsai/Bonsai.config b/.bonsai/Bonsai.config index ac40c8a..3db0977 100644 --- a/.bonsai/Bonsai.config +++ b/.bonsai/Bonsai.config @@ -5,15 +5,19 @@ + + + + @@ -25,19 +29,25 @@ + + + + + + From b8929468c7d1f91ab97403282780bfb7b35d9045 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 11:20:18 +0000 Subject: [PATCH 37/63] Add short sgen example --- docs/articles/why-bonsai-sgen.md | 54 ++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/docs/articles/why-bonsai-sgen.md b/docs/articles/why-bonsai-sgen.md index da779dd..938e6fa 100644 --- a/docs/articles/why-bonsai-sgen.md +++ b/docs/articles/why-bonsai-sgen.md @@ -78,24 +78,38 @@ First, we need to define the schema of the object in a JSON file: ```json { - "name": "Person", - "fields": [ - { - "name": "Age", - "type": "int", - }, - { - "name": "FirstName", - "type": "string", - }, - { - "name": "LastName", - "type": "string", - }, - { - "name": "DOB", - "type": "datetime", - } - ] + "title": "Person", + "type": "object", + "properties": { + "Age": { + "type": "integer" + }, + "FirstName": { + "type": "string" + }, + "LastName": { + "type": "string" + }, + "DOB": { + "type": "string", + "format": "date-time" + } + } } -``` \ No newline at end of file +``` + +Second, we need to run the `Bonsai.Sgen` tool to generate the Bonsai code: + +```cmd +dotnet bonsai.sgen --schema docs/workflows/person.json --output docs/workflows/Extensions/PersonSgen.cs +``` + +Finally, we can use the generated code in our Bonsai workflow: + +:::workflow +![Person as BonsaiSgen](~/workflows/person-example-bonsai-sgen.bonsai) +::: + +As you can probably tell, the `Bonsai.Sgen` approach is much more concise and less error-prone than the previous ones. It allows you to focus on the data structure itself and not on the boilerplate code required to create it in Bonsai. Moreover, as we will see later, the tool also automatically generates serialization and deserialization boilerplate code for the object, which can be very useful when working with external data sources. + +Finally, if one considers the `json-schema` as the "source of truth" for the data structure representation, it is possible to generate multiple representations of the object in different languages, ensuring interoperability. This can be very useful when working in a multi-language environment (e.g. running experiment in Bonsai and analysis in Python) and when sharing data structures across different projects. \ No newline at end of file From 68399cc7509de1b405c42309d5b7a521bb5443f2 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 11:20:33 +0000 Subject: [PATCH 38/63] Add person json schema and generated bonsai classes --- docs/workflows/Extensions/PersonSgen.cs | 196 ++++++++++++++++++++++++ docs/workflows/person.json | 19 +++ 2 files changed, 215 insertions(+) create mode 100644 docs/workflows/Extensions/PersonSgen.cs create mode 100644 docs/workflows/person.json diff --git a/docs/workflows/Extensions/PersonSgen.cs b/docs/workflows/Extensions/PersonSgen.cs new file mode 100644 index 0000000..add8fe9 --- /dev/null +++ b/docs/workflows/Extensions/PersonSgen.cs @@ -0,0 +1,196 @@ +//---------------------- +// +// Generated using the NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.0.0) (http://NJsonSchema.org) +// +//---------------------- + + +namespace DataSchema +{ + #pragma warning disable // Disable all warnings + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class Person + { + + private int _age; + + private string _firstName; + + private string _lastName; + + private System.DateTimeOffset _dOB; + + public Person() + { + } + + protected Person(Person other) + { + _age = other._age; + _firstName = other._firstName; + _lastName = other._lastName; + _dOB = other._dOB; + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Age")] + public int Age + { + get + { + return _age; + } + set + { + _age = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="FirstName")] + public string FirstName + { + get + { + return _firstName; + } + set + { + _firstName = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="LastName")] + public string LastName + { + get + { + return _lastName; + } + set + { + _lastName = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="DOB")] + public System.DateTimeOffset DOB + { + get + { + return _dOB; + } + set + { + _dOB = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Person(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new Person(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("Age = " + _age + ", "); + stringBuilder.Append("FirstName = " + _firstName + ", "); + stringBuilder.Append("LastName = " + _lastName + ", "); + stringBuilder.Append("DOB = " + _dOB); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + /// + /// Serializes a sequence of data model objects into YAML strings. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Serializes a sequence of data model objects into YAML strings.")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + public partial class SerializeToYaml + { + + private System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.SerializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => serializer.Serialize(value)); + }); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + } + + + /// + /// Deserializes a sequence of YAML strings into data model objects. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Deserializes a sequence of YAML strings into data model objects.")] + [System.ComponentModel.DefaultPropertyAttribute("Type")] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + public partial class DeserializeFromYaml : Bonsai.Expressions.SingleArgumentExpressionBuilder + { + + public DeserializeFromYaml() + { + Type = new Bonsai.Expressions.TypeMapping(); + } + + public Bonsai.Expressions.TypeMapping Type { get; set; } + + public override System.Linq.Expressions.Expression Build(System.Collections.Generic.IEnumerable arguments) + { + var typeMapping = (Bonsai.Expressions.TypeMapping)Type; + var returnType = typeMapping.GetType().GetGenericArguments()[0]; + return System.Linq.Expressions.Expression.Call( + typeof(DeserializeFromYaml), + "Process", + new System.Type[] { returnType }, + System.Linq.Enumerable.Single(arguments)); + } + + private static System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.DeserializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => + { + var reader = new System.IO.StringReader(value); + var parser = new YamlDotNet.Core.MergingParser(new YamlDotNet.Core.Parser(reader)); + return serializer.Deserialize(parser); + }); + }); + } + } +} \ No newline at end of file diff --git a/docs/workflows/person.json b/docs/workflows/person.json new file mode 100644 index 0000000..ce4f2c3 --- /dev/null +++ b/docs/workflows/person.json @@ -0,0 +1,19 @@ +{ + "title": "Person", + "type": "object", + "properties": { + "Age": { + "type": "integer" + }, + "FirstName": { + "type": "string" + }, + "LastName": { + "type": "string" + }, + "DOB": { + "type": "string", + "format": "date-time" + } + } +} From 9126430fb911cdfe19c888949f20a64adf0c952c Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 12:07:33 +0000 Subject: [PATCH 39/63] Move section --- docs/articles/why-bonsai-sgen.md | 47 +------------------------------- 1 file changed, 1 insertion(+), 46 deletions(-) diff --git a/docs/articles/why-bonsai-sgen.md b/docs/articles/why-bonsai-sgen.md index 938e6fa..1fb2d7a 100644 --- a/docs/articles/why-bonsai-sgen.md +++ b/docs/articles/why-bonsai-sgen.md @@ -67,49 +67,4 @@ public class CreatePerson : Source } ``` -As you can probably tell, neither of these approaches is ideal when it comes to scale large projects. This is where `Bonsai.Sgen` comes in. - - -## Automatic generation of Bonsai code using Bonsai.Sgen - -We will expand this example later on, but for now, let's see how we can use `Bonsai.Sgen` to automatically generate the Bonsai code for the `Person` object. - -First, we need to define the schema of the object in a JSON file: - -```json -{ - "title": "Person", - "type": "object", - "properties": { - "Age": { - "type": "integer" - }, - "FirstName": { - "type": "string" - }, - "LastName": { - "type": "string" - }, - "DOB": { - "type": "string", - "format": "date-time" - } - } -} -``` - -Second, we need to run the `Bonsai.Sgen` tool to generate the Bonsai code: - -```cmd -dotnet bonsai.sgen --schema docs/workflows/person.json --output docs/workflows/Extensions/PersonSgen.cs -``` - -Finally, we can use the generated code in our Bonsai workflow: - -:::workflow -![Person as BonsaiSgen](~/workflows/person-example-bonsai-sgen.bonsai) -::: - -As you can probably tell, the `Bonsai.Sgen` approach is much more concise and less error-prone than the previous ones. It allows you to focus on the data structure itself and not on the boilerplate code required to create it in Bonsai. Moreover, as we will see later, the tool also automatically generates serialization and deserialization boilerplate code for the object, which can be very useful when working with external data sources. - -Finally, if one considers the `json-schema` as the "source of truth" for the data structure representation, it is possible to generate multiple representations of the object in different languages, ensuring interoperability. This can be very useful when working in a multi-language environment (e.g. running experiment in Bonsai and analysis in Python) and when sharing data structures across different projects. \ No newline at end of file +As you can probably tell, neither of these approaches is ideal when it comes to scale large projects. This is where `Bonsai.Sgen` comes in. \ No newline at end of file From 4f47854d39749e5a3f8f735b718cc46f4a63190a Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 12:07:51 +0000 Subject: [PATCH 40/63] Add usage examples --- docs/articles/basic-usage.md | 141 +++++++ docs/workflows/Extensions/PersonAndDogSgen.cs | 383 ++++++++++++++++++ docs/workflows/Extensions/PersonAndPetEnum.cs | 303 ++++++++++++++ docs/workflows/person-and-pet-enum.bonsai | 9 + .../person-example-bonsai-sgen.bonsai | 16 + docs/workflows/person_and_dog.json | 48 +++ docs/workflows/person_and_pet_enum.json | 38 ++ .../personand-dog-example-bonsai-sgen.bonsai | 21 + ...example-nested-building-bonsai-sgen.bonsai | 55 +++ 9 files changed, 1014 insertions(+) create mode 100644 docs/workflows/Extensions/PersonAndDogSgen.cs create mode 100644 docs/workflows/Extensions/PersonAndPetEnum.cs create mode 100644 docs/workflows/person-and-pet-enum.bonsai create mode 100644 docs/workflows/person-example-bonsai-sgen.bonsai create mode 100644 docs/workflows/person_and_dog.json create mode 100644 docs/workflows/person_and_pet_enum.json create mode 100644 docs/workflows/personand-dog-example-bonsai-sgen.bonsai create mode 100644 docs/workflows/personand-dog-example-nested-building-bonsai-sgen.bonsai diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index e69de29..44fc347 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -0,0 +1,141 @@ +--- +uid: basic-usage +--- + + +## Automatic generation of Bonsai code using Bonsai.Sgen + +We will expand this example later on, but for now, let's see how we can use `Bonsai.Sgen` to automatically generate the Bonsai code for the `Person` object. + +First, we need to define the schema of the object in a JSON file: + +[person](~/workflows/person.json) + +```json +{ + "title": "Person", + "type": "object", + "properties": { + "Age": { "type": "integer" }, + "FirstName": { "type": "string" }, + "LastName": { "type": "string" }, + "DOB": { "type": "string", "format": "date-time" } + } +} +``` + +Second, we need to run the `Bonsai.Sgen` tool to generate the Bonsai code: + +```cmd +dotnet bonsai.sgen --schema docs/workflows/person.json --output docs/workflows/Extensions/PersonSgen.cs +``` + +Finally, we can use the generated code in our Bonsai workflow: + +:::workflow +![Person as BonsaiSgen](~/workflows/person-example-bonsai-sgen.bonsai) +::: + +As you can probably tell, the `Bonsai.Sgen` approach is much more concise and less error-prone than the previous ones. It allows you to focus on the data structure itself and not on the boilerplate code required to create it in Bonsai. Moreover, as we will see later, the tool also automatically generates serialization and deserialization boilerplate code for the object, which can be very useful when working with external data sources. + +Finally, if one considers the `json-schema` as the "source of truth" for the data structure representation, it is possible to generate multiple representations of the object in different languages, ensuring interoperability. This can be very useful when working in a multi-language environment (e.g. running experiment in Bonsai and analysis in Python) and when sharing data structures across different projects. + +## Multiple objects + +The previous example shows how a single record can be modelled. In practice, projects often require modelling different types of objects. This is where `Bonsai.Sgen` shines, as it allows you to generate multiple objects from a single schema file: + +[person_and_dog](~/workflows/person_and_dog.json) + +```json +{ + "title": "PersonAndPet", + "definitions": { + "Person": { + "title": "Person", + "type": "object", + "properties": { + "Age": { "type": "integer" }, + "FirstName": { "type": "string" }, + "LastName": { "type": "string" }, + "DOB": { "type": "string", "format": "date-time" } + } + }, + "Dog": { + "title": "Dog", + "type": "object", + "properties": { + "Name": { "type": "string" }, + "Breed": { "type": "string" }, + "Age": { "type": "integer" } + } + } + }, + "type": "object", + "properties": { + "owner": { "$ref": "#/definitions/Person" }, + "pet": { "$ref": "#/definitions/Dog" } + } +} +``` + +```cmd +dotnet bonsai.sgen --schema docs/workflows/person_and_dog.json --output docs/workflows/Extensions/PersonAndDogSgen.cs --namespace PersonAndDog +``` + +:::workflow +![PersonAndDog as BonsaiSgen](~/workflows/person-and-dog-example-bonsai-sgen.bonsai) +::: + +A few things worth noting in this example: + +- The schema file contains two definitions: `Person` and `Dog` that give rise to two operators (`Person` and `Dog`) in the generated code. +- A third definition `PersonAndPet` is used to combine the two objects into a single record. This can be omitted as we will see later by using the `x-abstract` property. +- The `--namespace` flag is used to specify the namespace of the generated code. This is useful to prevent name clashes between different schemas (e.g. `PersonAndDog.Person` and `Person` from the previous example). +- Both `Person` and `Dog` `json-schema` objects are passed as references. This is extremely important as it allows the reuse of the same definition in multiple places. This is particularly useful when working with complex data structures that share common fields. If definitions are passed in-line (i.e. redefined each time), Bonsai.Sgen may not be able to correctly identify them as the same object. + +## Nested objects + +The previous example highlights the simplicity of generating Bonsai code for simple objects. However, the real power of `Bonsai.Sgen` comes when dealing with more complex data structures, such as nested objects. Moreover, Bonsai syntax lends itself quite nicely to represent these nested structures as well as build them: + + +:::workflow +![PersonAndDog building as BonsaiSgen](~/workflows/person-and-dog-nested-building-example-bonsai-sgen.bonsai) +::: + +## Enums + +`Bonsai.Sgen` also supports the generation of enums using the `enum` type in the `json-schema`: + +We can replace the `Pet` object in the previous example with an [`enum`](https://json-schema.org/understanding-json-schema/reference/enum): +[Pet as Enum](~/workflows/person_and_pet_enum.json). + +```json +(...) + { + "Pet": { + "title": "Pet", + "type": "string", + "enum": ["Dog", "Cat", "Fish", "Bird", "Reptile"] + } + }, + "type": "object", + "properties": { + "owner": {"$ref": "#/definitions/Person"}, + "pet": {"$ref": "#/definitions/Pet"} + } +``` + +> [!TIP] +> In certain cases, it may be useful to use `x-enum-names` to specify the rendered names of the enum values. +> Example: +> +> ```json +> { +> "MyIntEnum": { +> "enum": [0, 1, 2, 3, 4], +> "title": "MyIntEnum", +> "type": "integer", +> "x-enumNames": ["None", "One", "Two", "Three", "Four"] +> } +> } +> ``` diff --git a/docs/workflows/Extensions/PersonAndDogSgen.cs b/docs/workflows/Extensions/PersonAndDogSgen.cs new file mode 100644 index 0000000..022b9a6 --- /dev/null +++ b/docs/workflows/Extensions/PersonAndDogSgen.cs @@ -0,0 +1,383 @@ +//---------------------- +// +// Generated using the NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.0.0) (http://NJsonSchema.org) +// +//---------------------- + + +namespace PersonAndDog +{ + #pragma warning disable // Disable all warnings + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class Person + { + + private int _age; + + private string _firstName; + + private string _lastName; + + private System.DateTimeOffset _dOB; + + public Person() + { + } + + protected Person(Person other) + { + _age = other._age; + _firstName = other._firstName; + _lastName = other._lastName; + _dOB = other._dOB; + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Age")] + public int Age + { + get + { + return _age; + } + set + { + _age = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="FirstName")] + public string FirstName + { + get + { + return _firstName; + } + set + { + _firstName = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="LastName")] + public string LastName + { + get + { + return _lastName; + } + set + { + _lastName = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="DOB")] + public System.DateTimeOffset DOB + { + get + { + return _dOB; + } + set + { + _dOB = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Person(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new Person(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("Age = " + _age + ", "); + stringBuilder.Append("FirstName = " + _firstName + ", "); + stringBuilder.Append("LastName = " + _lastName + ", "); + stringBuilder.Append("DOB = " + _dOB); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class Dog + { + + private string _name; + + private string _breed; + + private int _age; + + public Dog() + { + } + + protected Dog(Dog other) + { + _name = other._name; + _breed = other._breed; + _age = other._age; + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Name")] + public string Name + { + get + { + return _name; + } + set + { + _name = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Breed")] + public string Breed + { + get + { + return _breed; + } + set + { + _breed = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Age")] + public int Age + { + get + { + return _age; + } + set + { + _age = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Dog(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new Dog(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("Name = " + _name + ", "); + stringBuilder.Append("Breed = " + _breed + ", "); + stringBuilder.Append("Age = " + _age); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class PersonAndPet + { + + private Person _owner; + + private Dog _pet; + + public PersonAndPet() + { + } + + protected PersonAndPet(PersonAndPet other) + { + _owner = other._owner; + _pet = other._pet; + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="owner")] + public Person Owner + { + get + { + return _owner; + } + set + { + _owner = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="pet")] + public Dog Pet + { + get + { + return _pet; + } + set + { + _pet = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new PersonAndPet(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new PersonAndPet(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("owner = " + _owner + ", "); + stringBuilder.Append("pet = " + _pet); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + /// + /// Serializes a sequence of data model objects into YAML strings. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Serializes a sequence of data model objects into YAML strings.")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + public partial class SerializeToYaml + { + + private System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.SerializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => serializer.Serialize(value)); + }); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + } + + + /// + /// Deserializes a sequence of YAML strings into data model objects. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Deserializes a sequence of YAML strings into data model objects.")] + [System.ComponentModel.DefaultPropertyAttribute("Type")] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + public partial class DeserializeFromYaml : Bonsai.Expressions.SingleArgumentExpressionBuilder + { + + public DeserializeFromYaml() + { + Type = new Bonsai.Expressions.TypeMapping(); + } + + public Bonsai.Expressions.TypeMapping Type { get; set; } + + public override System.Linq.Expressions.Expression Build(System.Collections.Generic.IEnumerable arguments) + { + var typeMapping = (Bonsai.Expressions.TypeMapping)Type; + var returnType = typeMapping.GetType().GetGenericArguments()[0]; + return System.Linq.Expressions.Expression.Call( + typeof(DeserializeFromYaml), + "Process", + new System.Type[] { returnType }, + System.Linq.Enumerable.Single(arguments)); + } + + private static System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.DeserializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => + { + var reader = new System.IO.StringReader(value); + var parser = new YamlDotNet.Core.MergingParser(new YamlDotNet.Core.Parser(reader)); + return serializer.Deserialize(parser); + }); + }); + } + } +} \ No newline at end of file diff --git a/docs/workflows/Extensions/PersonAndPetEnum.cs b/docs/workflows/Extensions/PersonAndPetEnum.cs new file mode 100644 index 0000000..d1bac00 --- /dev/null +++ b/docs/workflows/Extensions/PersonAndPetEnum.cs @@ -0,0 +1,303 @@ +//---------------------- +// +// Generated using the NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.0.0) (http://NJsonSchema.org) +// +//---------------------- + + +namespace PersonAndPetEnum +{ + #pragma warning disable // Disable all warnings + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class Person + { + + private int _age; + + private string _firstName; + + private string _lastName; + + private System.DateTimeOffset _dOB; + + public Person() + { + } + + protected Person(Person other) + { + _age = other._age; + _firstName = other._firstName; + _lastName = other._lastName; + _dOB = other._dOB; + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Age")] + public int Age + { + get + { + return _age; + } + set + { + _age = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="FirstName")] + public string FirstName + { + get + { + return _firstName; + } + set + { + _firstName = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="LastName")] + public string LastName + { + get + { + return _lastName; + } + set + { + _lastName = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="DOB")] + public System.DateTimeOffset DOB + { + get + { + return _dOB; + } + set + { + _dOB = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Person(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new Person(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("Age = " + _age + ", "); + stringBuilder.Append("FirstName = " + _firstName + ", "); + stringBuilder.Append("LastName = " + _lastName + ", "); + stringBuilder.Append("DOB = " + _dOB); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + public enum Pet + { + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Dog")] + Dog = 0, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Cat")] + Cat = 1, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Fish")] + Fish = 2, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Bird")] + Bird = 3, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Reptile")] + Reptile = 4, + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class PersonAndPet + { + + private Person _owner; + + private Pet _pet; + + public PersonAndPet() + { + } + + protected PersonAndPet(PersonAndPet other) + { + _owner = other._owner; + _pet = other._pet; + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="owner")] + public Person Owner + { + get + { + return _owner; + } + set + { + _owner = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="pet")] + public Pet Pet + { + get + { + return _pet; + } + set + { + _pet = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new PersonAndPet(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new PersonAndPet(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("owner = " + _owner + ", "); + stringBuilder.Append("pet = " + _pet); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + /// + /// Serializes a sequence of data model objects into YAML strings. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Serializes a sequence of data model objects into YAML strings.")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + public partial class SerializeToYaml + { + + private System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.SerializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => serializer.Serialize(value)); + }); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + } + + + /// + /// Deserializes a sequence of YAML strings into data model objects. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Deserializes a sequence of YAML strings into data model objects.")] + [System.ComponentModel.DefaultPropertyAttribute("Type")] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + public partial class DeserializeFromYaml : Bonsai.Expressions.SingleArgumentExpressionBuilder + { + + public DeserializeFromYaml() + { + Type = new Bonsai.Expressions.TypeMapping(); + } + + public Bonsai.Expressions.TypeMapping Type { get; set; } + + public override System.Linq.Expressions.Expression Build(System.Collections.Generic.IEnumerable arguments) + { + var typeMapping = (Bonsai.Expressions.TypeMapping)Type; + var returnType = typeMapping.GetType().GetGenericArguments()[0]; + return System.Linq.Expressions.Expression.Call( + typeof(DeserializeFromYaml), + "Process", + new System.Type[] { returnType }, + System.Linq.Enumerable.Single(arguments)); + } + + private static System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.DeserializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => + { + var reader = new System.IO.StringReader(value); + var parser = new YamlDotNet.Core.MergingParser(new YamlDotNet.Core.Parser(reader)); + return serializer.Deserialize(parser); + }); + }); + } + } +} \ No newline at end of file diff --git a/docs/workflows/person-and-pet-enum.bonsai b/docs/workflows/person-and-pet-enum.bonsai new file mode 100644 index 0000000..eb14530 --- /dev/null +++ b/docs/workflows/person-and-pet-enum.bonsai @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/docs/workflows/person-example-bonsai-sgen.bonsai b/docs/workflows/person-example-bonsai-sgen.bonsai new file mode 100644 index 0000000..631776e --- /dev/null +++ b/docs/workflows/person-example-bonsai-sgen.bonsai @@ -0,0 +1,16 @@ + + + + + + + 0 + + + + + + \ No newline at end of file diff --git a/docs/workflows/person_and_dog.json b/docs/workflows/person_and_dog.json new file mode 100644 index 0000000..6f343e8 --- /dev/null +++ b/docs/workflows/person_and_dog.json @@ -0,0 +1,48 @@ +{ + "title": "PersonAndPet", + "definitions": { + "Person": { + "title": "Person", + "type": "object", + "properties": { + "Age": { + "type": "integer" + }, + "FirstName": { + "type": "string" + }, + "LastName": { + "type": "string" + }, + "DOB": { + "type": "string", + "format": "date-time" + } + } + }, + "Dog": { + "title": "Dog", + "type": "object", + "properties": { + "Name": { + "type": "string" + }, + "Breed": { + "type": "string" + }, + "Age": { + "type": "integer" + } + } + } + }, + "type": "object", + "properties": { + "owner": { + "$ref": "#/definitions/Person" + }, + "pet": { + "$ref": "#/definitions/Dog" + } + } +} diff --git a/docs/workflows/person_and_pet_enum.json b/docs/workflows/person_and_pet_enum.json new file mode 100644 index 0000000..78234c0 --- /dev/null +++ b/docs/workflows/person_and_pet_enum.json @@ -0,0 +1,38 @@ +{ + "title": "PersonAndPet", + "definitions": { + "Person": { + "title": "Person", + "type": "object", + "properties": { + "Age": { + "type": "integer" + }, + "FirstName": { + "type": "string" + }, + "LastName": { + "type": "string" + }, + "DOB": { + "type": "string", + "format": "date-time" + } + } + }, + "Pet": { + "title": "Pet", + "type": "string", + "enum": ["Dog", "Cat", "Fish", "Bird", "Reptile"] + } + }, + "type": "object", + "properties": { + "owner": { + "$ref": "#/definitions/Person" + }, + "pet": { + "$ref": "#/definitions/Pet" + } + } +} diff --git a/docs/workflows/personand-dog-example-bonsai-sgen.bonsai b/docs/workflows/personand-dog-example-bonsai-sgen.bonsai new file mode 100644 index 0000000..62d3bcc --- /dev/null +++ b/docs/workflows/personand-dog-example-bonsai-sgen.bonsai @@ -0,0 +1,21 @@ + + + + + + + 0 + + + + + 0 + + + + + + \ No newline at end of file diff --git a/docs/workflows/personand-dog-example-nested-building-bonsai-sgen.bonsai b/docs/workflows/personand-dog-example-nested-building-bonsai-sgen.bonsai new file mode 100644 index 0000000..43236bc --- /dev/null +++ b/docs/workflows/personand-dog-example-nested-building-bonsai-sgen.bonsai @@ -0,0 +1,55 @@ + + + + + + + 0 + + + + + + + + + + 0 + + + + + + + + + + + + Owner + + + Age + + + Pet + + + Breed + + + + + + + + + + + + + + \ No newline at end of file From fd9fe2261b6d16e004b575fe98cd3af8df0b6b28 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 12:08:31 +0000 Subject: [PATCH 41/63] Remove unecessary label --- docs/articles/basic-usage.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index 44fc347..3f9efff 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -127,7 +127,6 @@ We can replace the `Pet` object in the previous example with an [`enum`](https:/ > [!TIP] > In certain cases, it may be useful to use `x-enum-names` to specify the rendered names of the enum values. -> Example: > > ```json > { From d7a327f08537f82a349c3446a4b0f9d00a4d3d1f Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 12:14:48 +0000 Subject: [PATCH 42/63] Refactor file names --- docs/articles/basic-usage.md | 4 ++-- docs/workflows/{person_and_dog.json => person-and-dog.json} | 0 .../{person_and_pet_enum.json => person-and-pet-enum.json} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename docs/workflows/{person_and_dog.json => person-and-dog.json} (100%) rename docs/workflows/{person_and_pet_enum.json => person-and-pet-enum.json} (100%) diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index 3f9efff..a828107 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -44,7 +44,7 @@ Finally, if one considers the `json-schema` as the "source of truth" for the dat The previous example shows how a single record can be modelled. In practice, projects often require modelling different types of objects. This is where `Bonsai.Sgen` shines, as it allows you to generate multiple objects from a single schema file: -[person_and_dog](~/workflows/person_and_dog.json) +[person_and_dog](~/workflows/person-and-dog.json) ```json { @@ -107,7 +107,7 @@ The previous example highlights the simplicity of generating Bonsai code for sim `Bonsai.Sgen` also supports the generation of enums using the `enum` type in the `json-schema`: We can replace the `Pet` object in the previous example with an [`enum`](https://json-schema.org/understanding-json-schema/reference/enum): -[Pet as Enum](~/workflows/person_and_pet_enum.json). +[Pet as Enum](~/workflows/person-and-pet-enum.json). ```json (...) diff --git a/docs/workflows/person_and_dog.json b/docs/workflows/person-and-dog.json similarity index 100% rename from docs/workflows/person_and_dog.json rename to docs/workflows/person-and-dog.json diff --git a/docs/workflows/person_and_pet_enum.json b/docs/workflows/person-and-pet-enum.json similarity index 100% rename from docs/workflows/person_and_pet_enum.json rename to docs/workflows/person-and-pet-enum.json From 6e7d0dee540be61662efeb4759e719c57f65893c Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 12:20:49 +0000 Subject: [PATCH 43/63] Add array manipulation example --- docs/articles/basic-usage.md | 24 ++ .../workflows/Extensions/PersonAndPetsEnum.cs | 303 ++++++++++++++++++ docs/workflows/person-and-pets-enum.bonsai | 32 ++ docs/workflows/person-and-pets-enum.json | 41 +++ 4 files changed, 400 insertions(+) create mode 100644 docs/workflows/Extensions/PersonAndPetsEnum.cs create mode 100644 docs/workflows/person-and-pets-enum.bonsai create mode 100644 docs/workflows/person-and-pets-enum.json diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index a828107..7e4dffc 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -107,6 +107,7 @@ The previous example highlights the simplicity of generating Bonsai code for sim `Bonsai.Sgen` also supports the generation of enums using the `enum` type in the `json-schema`: We can replace the `Pet` object in the previous example with an [`enum`](https://json-schema.org/understanding-json-schema/reference/enum): + [Pet as Enum](~/workflows/person-and-pet-enum.json). ```json @@ -125,6 +126,12 @@ We can replace the `Pet` object in the previous example with an [`enum`](https:/ } ``` +In Bonsai, they can be manipulated as [`Enum`](https://learn.microsoft.com/en-us/dotnet/api/system.enum?view=net-9.0) types: + +:::workflow +![Person and Pets](~/workflows/person-and-pet-enum.bonsai) +::: + > [!TIP] > In certain cases, it may be useful to use `x-enum-names` to specify the rendered names of the enum values. > @@ -138,3 +145,20 @@ We can replace the `Pet` object in the previous example with an [`enum`](https:/ > } > } > ``` + +## Lists + +`Bonsai.Sgen` also supports the generation of lists using the `array` type in the `json-schema`: + +```json + "pets": { + "type": "array", + "items": {"$ref": "#/definitions/Pet"} + } +``` + +`json-schema` `array`s will be rendered as [`List`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1?view=net-9.0) in the generated code and can be manipulated (and created) as such. + +:::workflow +![Person and Pets](~/workflows/person-and-pets-enum.bonsai) +::: diff --git a/docs/workflows/Extensions/PersonAndPetsEnum.cs b/docs/workflows/Extensions/PersonAndPetsEnum.cs new file mode 100644 index 0000000..518625b --- /dev/null +++ b/docs/workflows/Extensions/PersonAndPetsEnum.cs @@ -0,0 +1,303 @@ +//---------------------- +// +// Generated using the NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.0.0) (http://NJsonSchema.org) +// +//---------------------- + + +namespace PersonAndPetsEnum +{ + #pragma warning disable // Disable all warnings + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class Person + { + + private int _age; + + private string _firstName; + + private string _lastName; + + private System.DateTimeOffset _dOB; + + public Person() + { + } + + protected Person(Person other) + { + _age = other._age; + _firstName = other._firstName; + _lastName = other._lastName; + _dOB = other._dOB; + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Age")] + public int Age + { + get + { + return _age; + } + set + { + _age = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="FirstName")] + public string FirstName + { + get + { + return _firstName; + } + set + { + _firstName = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="LastName")] + public string LastName + { + get + { + return _lastName; + } + set + { + _lastName = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="DOB")] + public System.DateTimeOffset DOB + { + get + { + return _dOB; + } + set + { + _dOB = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Person(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new Person(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("Age = " + _age + ", "); + stringBuilder.Append("FirstName = " + _firstName + ", "); + stringBuilder.Append("LastName = " + _lastName + ", "); + stringBuilder.Append("DOB = " + _dOB); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + public enum Pet + { + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Dog")] + Dog = 0, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Cat")] + Cat = 1, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Fish")] + Fish = 2, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Bird")] + Bird = 3, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Reptile")] + Reptile = 4, + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class PersonAndPets + { + + private Person _owner; + + private System.Collections.Generic.List _pets = new System.Collections.Generic.List(); + + public PersonAndPets() + { + } + + protected PersonAndPets(PersonAndPets other) + { + _owner = other._owner; + _pets = other._pets; + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="owner")] + public Person Owner + { + get + { + return _owner; + } + set + { + _owner = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="pets")] + public System.Collections.Generic.List Pets + { + get + { + return _pets; + } + set + { + _pets = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new PersonAndPets(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new PersonAndPets(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("owner = " + _owner + ", "); + stringBuilder.Append("pets = " + _pets); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + /// + /// Serializes a sequence of data model objects into YAML strings. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Serializes a sequence of data model objects into YAML strings.")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + public partial class SerializeToYaml + { + + private System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.SerializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => serializer.Serialize(value)); + }); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + } + + + /// + /// Deserializes a sequence of YAML strings into data model objects. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Deserializes a sequence of YAML strings into data model objects.")] + [System.ComponentModel.DefaultPropertyAttribute("Type")] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + public partial class DeserializeFromYaml : Bonsai.Expressions.SingleArgumentExpressionBuilder + { + + public DeserializeFromYaml() + { + Type = new Bonsai.Expressions.TypeMapping(); + } + + public Bonsai.Expressions.TypeMapping Type { get; set; } + + public override System.Linq.Expressions.Expression Build(System.Collections.Generic.IEnumerable arguments) + { + var typeMapping = (Bonsai.Expressions.TypeMapping)Type; + var returnType = typeMapping.GetType().GetGenericArguments()[0]; + return System.Linq.Expressions.Expression.Call( + typeof(DeserializeFromYaml), + "Process", + new System.Type[] { returnType }, + System.Linq.Enumerable.Single(arguments)); + } + + private static System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.DeserializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => + { + var reader = new System.IO.StringReader(value); + var parser = new YamlDotNet.Core.MergingParser(new YamlDotNet.Core.Parser(reader)); + return serializer.Deserialize(parser); + }); + }); + } + } +} \ No newline at end of file diff --git a/docs/workflows/person-and-pets-enum.bonsai b/docs/workflows/person-and-pets-enum.bonsai new file mode 100644 index 0000000..7b9df4f --- /dev/null +++ b/docs/workflows/person-and-pets-enum.bonsai @@ -0,0 +1,32 @@ + + + + + + + + + Owner + + + Pets + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/workflows/person-and-pets-enum.json b/docs/workflows/person-and-pets-enum.json new file mode 100644 index 0000000..0f0f950 --- /dev/null +++ b/docs/workflows/person-and-pets-enum.json @@ -0,0 +1,41 @@ +{ + "title": "PersonAndPets", + "definitions": { + "Person": { + "title": "Person", + "type": "object", + "properties": { + "Age": { + "type": "integer" + }, + "FirstName": { + "type": "string" + }, + "LastName": { + "type": "string" + }, + "DOB": { + "type": "string", + "format": "date-time" + } + } + }, + "Pet": { + "title": "Pet", + "type": "string", + "enum": ["Dog", "Cat", "Fish", "Bird", "Reptile"] + } + }, + "type": "object", + "properties": { + "owner": { + "$ref": "#/definitions/Person" + }, + "pets": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + } +} From d1e6bd5eea25172ed64a4c0822e3984aff62a402 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 12:53:13 +0000 Subject: [PATCH 44/63] Add nullable example --- docs/articles/basic-usage.md | 22 ++ .../Extensions/PersonAndPetEnumNullable.cs | 303 ++++++++++++++++++ .../person-and-pet-enum-nullable.bonsai | 66 ++++ .../person-and-pet-enum-nullable.json | 41 +++ 4 files changed, 432 insertions(+) create mode 100644 docs/workflows/Extensions/PersonAndPetEnumNullable.cs create mode 100644 docs/workflows/person-and-pet-enum-nullable.bonsai create mode 100644 docs/workflows/person-and-pet-enum-nullable.json diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index 7e4dffc..82ac423 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -162,3 +162,25 @@ In Bonsai, they can be manipulated as [`Enum`](https://learn.microsoft.com/en-us :::workflow ![Person and Pets](~/workflows/person-and-pets-enum.bonsai) ::: + +## Nullable types + +`json-schema` supports the `null` type, which can be used to represent nullable types. The standard is a bit loose in this regard, but `Bonsai.Sgen` will generate a nullable-T if the json-schema represents it using the `oneOf` keyword: + +```json + "pet": { + "oneOf": [ + {"$ref": "#/definitions/Pet"}, + {"type": "null"} + ] + } +``` + +For value types, the generated code will render a [Nullable value type](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/nullable-value-types) type. This type will expose two properties: `HasValue` and `Value`, that can be used to test and manipulate the type, respectively. + +For reference types, the generated code will not render a nullable type since reference types are already nullable in C#. A consumer can test for `null` to determine if the value is present by simply using an `ExpressionTransform` operator with `it == null`.: + +:::workflow +![Nullable pet](~/workflows/person-and-pets-enum-nullable.bonsai) +::: + diff --git a/docs/workflows/Extensions/PersonAndPetEnumNullable.cs b/docs/workflows/Extensions/PersonAndPetEnumNullable.cs new file mode 100644 index 0000000..4d8f22c --- /dev/null +++ b/docs/workflows/Extensions/PersonAndPetEnumNullable.cs @@ -0,0 +1,303 @@ +//---------------------- +// +// Generated using the NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.0.0) (http://NJsonSchema.org) +// +//---------------------- + + +namespace PersonAndPetEnumNullable +{ + #pragma warning disable // Disable all warnings + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class Person + { + + private int _age; + + private string _firstName; + + private string _lastName; + + private System.DateTimeOffset _dOB; + + public Person() + { + } + + protected Person(Person other) + { + _age = other._age; + _firstName = other._firstName; + _lastName = other._lastName; + _dOB = other._dOB; + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Age")] + public int Age + { + get + { + return _age; + } + set + { + _age = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="FirstName")] + public string FirstName + { + get + { + return _firstName; + } + set + { + _firstName = value; + } + } + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="LastName")] + public string LastName + { + get + { + return _lastName; + } + set + { + _lastName = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="DOB")] + public System.DateTimeOffset DOB + { + get + { + return _dOB; + } + set + { + _dOB = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Person(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new Person(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("Age = " + _age + ", "); + stringBuilder.Append("FirstName = " + _firstName + ", "); + stringBuilder.Append("LastName = " + _lastName + ", "); + stringBuilder.Append("DOB = " + _dOB); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + public enum Pet + { + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Dog")] + Dog = 0, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Cat")] + Cat = 1, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Fish")] + Fish = 2, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Bird")] + Bird = 3, + + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="Reptile")] + Reptile = 4, + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class PersonAndPet + { + + private Person _owner; + + private Pet? _pet; + + public PersonAndPet() + { + } + + protected PersonAndPet(PersonAndPet other) + { + _owner = other._owner; + _pet = other._pet; + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="owner")] + public Person Owner + { + get + { + return _owner; + } + set + { + _owner = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [YamlDotNet.Serialization.YamlMemberAttribute(Alias="pet")] + public Pet? Pet + { + get + { + return _pet; + } + set + { + _pet = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new PersonAndPet(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new PersonAndPet(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("owner = " + _owner + ", "); + stringBuilder.Append("pet = " + _pet); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + /// + /// Serializes a sequence of data model objects into YAML strings. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Serializes a sequence of data model objects into YAML strings.")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + public partial class SerializeToYaml + { + + private System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.SerializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => serializer.Serialize(value)); + }); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + } + + + /// + /// Deserializes a sequence of YAML strings into data model objects. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.4.0.0 (YamlDotNet v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Deserializes a sequence of YAML strings into data model objects.")] + [System.ComponentModel.DefaultPropertyAttribute("Type")] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + public partial class DeserializeFromYaml : Bonsai.Expressions.SingleArgumentExpressionBuilder + { + + public DeserializeFromYaml() + { + Type = new Bonsai.Expressions.TypeMapping(); + } + + public Bonsai.Expressions.TypeMapping Type { get; set; } + + public override System.Linq.Expressions.Expression Build(System.Collections.Generic.IEnumerable arguments) + { + var typeMapping = (Bonsai.Expressions.TypeMapping)Type; + var returnType = typeMapping.GetType().GetGenericArguments()[0]; + return System.Linq.Expressions.Expression.Call( + typeof(DeserializeFromYaml), + "Process", + new System.Type[] { returnType }, + System.Linq.Enumerable.Single(arguments)); + } + + private static System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Defer(() => + { + var serializer = new YamlDotNet.Serialization.DeserializerBuilder() + .Build(); + return System.Reactive.Linq.Observable.Select(source, value => + { + var reader = new System.IO.StringReader(value); + var parser = new YamlDotNet.Core.MergingParser(new YamlDotNet.Core.Parser(reader)); + return serializer.Deserialize(parser); + }); + }); + } + } +} \ No newline at end of file diff --git a/docs/workflows/person-and-pet-enum-nullable.bonsai b/docs/workflows/person-and-pet-enum-nullable.bonsai new file mode 100644 index 0000000..dda837d --- /dev/null +++ b/docs/workflows/person-and-pet-enum-nullable.bonsai @@ -0,0 +1,66 @@ + + + + + + + + + Value Type + + + + Pet + + + HasValue + + + + Source1 + + + HasValue + + + + + + + + + + + Value + + + Reference Type + + + + + + + Owner + + + IsNull + it == null + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/workflows/person-and-pet-enum-nullable.json b/docs/workflows/person-and-pet-enum-nullable.json new file mode 100644 index 0000000..f8e7342 --- /dev/null +++ b/docs/workflows/person-and-pet-enum-nullable.json @@ -0,0 +1,41 @@ +{ + "title": "PersonAndPet", + "definitions": { + "Person": { + "title": "Person", + "type": "object", + "properties": { + "Age": { + "type": "integer" + }, + "FirstName": { + "type": "string" + }, + "LastName": { + "type": "string" + }, + "DOB": { + "type": "string", + "format": "date-time" + } + } + }, + "Pet": { + "title": "Pet", + "type": "string", + "enum": ["Dog", "Cat", "Fish", "Bird", "Reptile"] + } + }, + "type": "object", + "properties": { + "owner": { + "$ref": "#/definitions/Person" + }, + "pet": { + "oneOf": [ + {"$ref": "#/definitions/Pet"}, + {"type": "null"} + ] + } + } +} From ebcb4f40ed267dbe71015b0040e4ed0454b0a2c5 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 27 Dec 2024 13:00:55 +0000 Subject: [PATCH 45/63] Add required field section --- docs/articles/basic-usage.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index 82ac423..575cf16 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -184,3 +184,15 @@ For reference types, the generated code will not render a nullable type since re ![Nullable pet](~/workflows/person-and-pets-enum-nullable.bonsai) ::: +## Required fields + +`json-schema` supports the [`required`](https://json-schema.org/learn/getting-started-step-by-step#define-required-properties) keyword to specify which fields are required. By default, all fields are optional. This can be useful to enforce the presence of certain fields in the object at deserialization time. However, `Bonsai.Sgen` will not generate any code to enforce this requirement during object construction, only at deserialization. It is up to the user to ensure that the object is correctly populated before using it. + +> [!Note] +> Some confusion maybe arise about the distinction between `null` and `required`. This is all the more confusing since different languages and libraries may refer to these concepts differently. For the sake of this tool (and honestly in general) the following definitions are used: + +> - `nullable` means that the field can be `null` or type `T` +> - `required` means that the field must be present in the object at deserialization time +> - An object can be `nullable` and `required` at the same time. This means it MUST be defined in the object, but it can be defined as `null`. +> - An object can be `not required` and `nullable`. This does NOT mean that the object is, by default, `null`. It means that the object should have a default value, which can in theory be `null`. +> - An object can be `not required` and `not nullable`. This means that the object must have a default value, which cannot be `null`. \ No newline at end of file From 6d59f898d2a8785923ff7dca2b35489a32f18d00 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 28 Dec 2024 18:11:41 +0000 Subject: [PATCH 46/63] Add documentation on Unions --- docs/articles/advanced-usage.md | 73 +++ .../Extensions/PersonAndDiscriminatedPets.cs | 530 ++++++++++++++++++ .../person-and-discriminated-pets.json | 84 +++ .../person-pet-discriminated-union.bonsai | 73 +++ 4 files changed, 760 insertions(+) create mode 100644 docs/workflows/Extensions/PersonAndDiscriminatedPets.cs create mode 100644 docs/workflows/person-and-discriminated-pets.json create mode 100644 docs/workflows/person-pet-discriminated-union.bonsai diff --git a/docs/articles/advanced-usage.md b/docs/articles/advanced-usage.md index e69de29..3263724 100644 --- a/docs/articles/advanced-usage.md +++ b/docs/articles/advanced-usage.md @@ -0,0 +1,73 @@ +--- +uid: advanced-usage +--- + + +## Unions + +In the previous examples, we have seen how create object properties of a single type. However, in practice, data structures' fields can often be represented by one of several types. We have actually seen a special case of this behavior in the previous nullable example, where a field can be either a value of a given type or `null` (or an Union between type `T` and `null`). + +Similarly, `json-schema` allows union types to be defined using the `oneOf` keyword. For example, consider the following schema: + +```json +{ + "title": "MyPet", + "type": "object", + "properties": { + "FooProperty": { + "oneOf": [ + { "type": "string" }, + { "type": "number" } + ] + } + } +} +``` + +If we run `Bonsai.Sgen` on this schema, we will get the following signature for the `FooProperty` property: + +```csharp +public object FooProperty +``` + +This is because while the `oneOf` keyword is supported by the `Bonsai.Sgen` tool, for statically typed languages like `C#` and `Bonsai`, we need to know the exact type of the property at compile time. As a result, we opt to "up-cast" the property to the most general type that can represent all the possible types in the union (`object`). It is up to the user to down-cast the property to the correct type at runtime. + + +## Tagged Unions + +At this point, you might be wondering if there is a way to represent union types in a more type-safe way in json-schema. The answer is yes, and the way to do it is by using [`discriminated unions`](https://en.wikipedia.org/wiki/Tagged_union) (or `tagged union`). The syntax for discriminated unions is not supported by vanilla `json-schema`, but it is supported by the [`OpenAPI` standard](https://swagger.io/docs/specification/v3_0/data-models/inheritance-and-polymorphism/#discriminator), which is a superset of `json-schema`. The key idea behind discriminated unions is to add a `discriminator` field to the schema that specifies the property that will be used to determine the type of the object at runtime. + +For example, a `Pet` object that can be either a `Dog` or a `Cat` can be represented as follows: + +[person](~/workflows/person-and-discriminated-pets.json) + +```json +"Pet": { + "discriminator": { + "mapping": { + "cat": "#/definitions/Cat", + "dog": "#/definitions/Dog" + }, + "propertyName": "pet_type" + }, + "oneOf": [ + { + "$ref": "#/definitions/Dog" + }, + { + "$ref": "#/definitions/Cat" + } + ] + } +``` + +In `C#`, `Bonsai.Sgen` will generate a root type `Pet` that will be inherited by the `Dog` and `Cat` types (since in the worst case scenario, the discriminated property must be shared). The `Pet` type will have a `pet_type` property that will be used to downcast to the proper type at runtime. At this point we can open our example in `Bonsai` and see how the `Pet` type is represented in the workflow. + +As you can see below, we still get a `Pet` type. Better than `object` but still not a `Dog` or `Cat` type. Fortunately, `Bonsai.Sgen` will generate an operator that can be used to filter and downcast the `Pet` objects to the correct type at runtime. These are called `Match` operators. After adding a `MatchPet` to our workflow we can select the desired target type which will allow us access to the properties of the `Dog` or `Cat` type. Conversely, we can also upcast a `Dog` or `Cat` to a `Pet` leaving the `MatchPet` operator's `Type` property empty. + +:::workflow +![Discriminated Unions](~/workflows/person-pet-discriminated-union.bonsai) +::: + +> [!Important] +> In is general advisable to use references in the `oneOf` syntax. Not only does this decision make your `json-schema` significantly smaller, it will also help `Bonsai.Sgen` generate the correct class hierarchy if multiple unions are present in the schema. If you use inline objects, `Bonsai.Sgen` will likely have to generate a new root class for each union, which can lead to a lot of duplicated code and a more complex object hierarchy. \ No newline at end of file diff --git a/docs/workflows/Extensions/PersonAndDiscriminatedPets.cs b/docs/workflows/Extensions/PersonAndDiscriminatedPets.cs new file mode 100644 index 0000000..a0bcd35 --- /dev/null +++ b/docs/workflows/Extensions/PersonAndDiscriminatedPets.cs @@ -0,0 +1,530 @@ +//---------------------- +// +// Generated using the NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.0.0) (http://NJsonSchema.org) +// +//---------------------- + + +namespace PersonAndDiscriminatedPets +{ + #pragma warning disable // Disable all warnings + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.3.0.0 (Newtonsoft.Json v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class Cat : Pet + { + + private int _age; + + private bool _canMeow = true; + + public Cat() + { + } + + protected Cat(Cat other) : + base(other) + { + _age = other._age; + _canMeow = other._canMeow; + } + + [Newtonsoft.Json.JsonPropertyAttribute("age", Required=Newtonsoft.Json.Required.Always)] + public int Age + { + get + { + return _age; + } + set + { + _age = value; + } + } + + [Newtonsoft.Json.JsonPropertyAttribute("can_meow")] + public bool CanMeow + { + get + { + return _canMeow; + } + set + { + _canMeow = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Cat(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new Cat(this)); + } + + protected override bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + if (base.PrintMembers(stringBuilder)) + { + stringBuilder.Append(", "); + } + stringBuilder.Append("age = " + _age + ", "); + stringBuilder.Append("can_meow = " + _canMeow); + return true; + } + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.3.0.0 (Newtonsoft.Json v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class Dog : Pet + { + + private int _age; + + private bool _canBark = true; + + public Dog() + { + } + + protected Dog(Dog other) : + base(other) + { + _age = other._age; + _canBark = other._canBark; + } + + [Newtonsoft.Json.JsonPropertyAttribute("age", Required=Newtonsoft.Json.Required.Always)] + public int Age + { + get + { + return _age; + } + set + { + _age = value; + } + } + + [Newtonsoft.Json.JsonPropertyAttribute("can_bark")] + public bool CanBark + { + get + { + return _canBark; + } + set + { + _canBark = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Dog(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new Dog(this)); + } + + protected override bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + if (base.PrintMembers(stringBuilder)) + { + stringBuilder.Append(", "); + } + stringBuilder.Append("age = " + _age + ", "); + stringBuilder.Append("can_bark = " + _canBark); + return true; + } + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.3.0.0 (Newtonsoft.Json v13.0.0.0)")] + [Newtonsoft.Json.JsonConverter(typeof(JsonInheritanceConverter), "pet_type")] + [JsonInheritanceAttribute("dog", typeof(Dog))] + [JsonInheritanceAttribute("cat", typeof(Cat))] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class Pet + { + + public Pet() + { + } + + protected Pet(Pet other) + { + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new Pet(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new Pet(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + return false; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.3.0.0 (Newtonsoft.Json v13.0.0.0)")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Source)] + public partial class PersonAndPet + { + + private string _owner; + + private Pet _pet; + + public PersonAndPet() + { + } + + protected PersonAndPet(PersonAndPet other) + { + _owner = other._owner; + _pet = other._pet; + } + + [Newtonsoft.Json.JsonPropertyAttribute("owner", Required=Newtonsoft.Json.Required.Always)] + public string Owner + { + get + { + return _owner; + } + set + { + _owner = value; + } + } + + [System.Xml.Serialization.XmlIgnoreAttribute()] + [Newtonsoft.Json.JsonPropertyAttribute("pet", Required=Newtonsoft.Json.Required.Always)] + public Pet Pet + { + get + { + return _pet; + } + set + { + _pet = value; + } + } + + public System.IObservable Process() + { + return System.Reactive.Linq.Observable.Defer(() => System.Reactive.Linq.Observable.Return(new PersonAndPet(this))); + } + + public System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, _ => new PersonAndPet(this)); + } + + protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) + { + stringBuilder.Append("owner = " + _owner + ", "); + stringBuilder.Append("pet = " + _pet); + return true; + } + + public override string ToString() + { + System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(); + stringBuilder.Append(GetType().Name); + stringBuilder.Append(" { "); + if (PrintMembers(stringBuilder)) + { + stringBuilder.Append(" "); + } + stringBuilder.Append("}"); + return stringBuilder.ToString(); + } + } + + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.9.0.0 (Newtonsoft.Json v13.0.0.0)")] + [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Interface, AllowMultiple = true)] + internal class JsonInheritanceAttribute : System.Attribute + { + public JsonInheritanceAttribute(string key, System.Type type) + { + Key = key; + Type = type; + } + + public string Key { get; private set; } + + public System.Type Type { get; private set; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.9.0.0 (Newtonsoft.Json v13.0.0.0)")] + public class JsonInheritanceConverter : Newtonsoft.Json.JsonConverter + { + internal static readonly string DefaultDiscriminatorName = "discriminator"; + + private readonly string _discriminatorName; + + [System.ThreadStatic] + private static bool _isReading; + + [System.ThreadStatic] + private static bool _isWriting; + + public JsonInheritanceConverter() + { + _discriminatorName = DefaultDiscriminatorName; + } + + public JsonInheritanceConverter(string discriminatorName) + { + _discriminatorName = discriminatorName; + } + + public string DiscriminatorName { get { return _discriminatorName; } } + + public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) + { + try + { + _isWriting = true; + + var jObject = Newtonsoft.Json.Linq.JObject.FromObject(value, serializer); + jObject.AddFirst(new Newtonsoft.Json.Linq.JProperty(_discriminatorName, GetSubtypeDiscriminator(value.GetType()))); + writer.WriteToken(jObject.CreateReader()); + } + finally + { + _isWriting = false; + } + } + + public override bool CanWrite + { + get + { + if (_isWriting) + { + _isWriting = false; + return false; + } + return true; + } + } + + public override bool CanRead + { + get + { + if (_isReading) + { + _isReading = false; + return false; + } + return true; + } + } + + public override bool CanConvert(System.Type objectType) + { + return true; + } + + public override object ReadJson(Newtonsoft.Json.JsonReader reader, System.Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer) + { + var jObject = serializer.Deserialize(reader); + if (jObject == null) + return null; + + var discriminatorValue = jObject.GetValue(_discriminatorName); + var discriminator = discriminatorValue != null ? Newtonsoft.Json.Linq.Extensions.Value(discriminatorValue) : null; + var subtype = GetObjectSubtype(objectType, discriminator); + + var objectContract = serializer.ContractResolver.ResolveContract(subtype) as Newtonsoft.Json.Serialization.JsonObjectContract; + if (objectContract == null || System.Linq.Enumerable.All(objectContract.Properties, p => p.PropertyName != _discriminatorName)) + { + jObject.Remove(_discriminatorName); + } + + try + { + _isReading = true; + return serializer.Deserialize(jObject.CreateReader(), subtype); + } + finally + { + _isReading = false; + } + } + + private System.Type GetObjectSubtype(System.Type objectType, string discriminator) + { + foreach (var attribute in System.Reflection.CustomAttributeExtensions.GetCustomAttributes(System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType), true)) + { + if (attribute.Key == discriminator) + return attribute.Type; + } + + return objectType; + } + + private string GetSubtypeDiscriminator(System.Type objectType) + { + foreach (var attribute in System.Reflection.CustomAttributeExtensions.GetCustomAttributes(System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType), true)) + { + if (attribute.Type == objectType) + return attribute.Key; + } + + return objectType.Name; + } + } + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.3.0.0 (Newtonsoft.Json v13.0.0.0)")] + [System.ComponentModel.DefaultPropertyAttribute("Type")] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Combinator)] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + public partial class MatchPet : Bonsai.Expressions.SingleArgumentExpressionBuilder + { + + public Bonsai.Expressions.TypeMapping Type { get; set; } + + public override System.Linq.Expressions.Expression Build(System.Collections.Generic.IEnumerable arguments) + { + var typeMapping = Type; + var returnType = typeMapping != null ? typeMapping.GetType().GetGenericArguments()[0] : typeof(Pet); + return System.Linq.Expressions.Expression.Call( + typeof(MatchPet), + "Process", + new System.Type[] { returnType }, + System.Linq.Enumerable.Single(arguments)); + } + + + private static System.IObservable Process(System.IObservable source) + where TResult : Pet + { + return System.Reactive.Linq.Observable.Create(observer => + { + var sourceObserver = System.Reactive.Observer.Create( + value => + { + var match = value as TResult; + if (match != null) observer.OnNext(match); + }, + observer.OnError, + observer.OnCompleted); + return System.ObservableExtensions.SubscribeSafe(source, sourceObserver); + }); + } + } + + + /// + /// Serializes a sequence of data model objects into JSON strings. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.3.0.0 (Newtonsoft.Json v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Serializes a sequence of data model objects into JSON strings.")] + [Bonsai.CombinatorAttribute()] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + public partial class SerializeToJson + { + + private System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, value => Newtonsoft.Json.JsonConvert.SerializeObject(value)); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + + public System.IObservable Process(System.IObservable source) + { + return Process(source); + } + } + + + /// + /// Deserializes a sequence of JSON strings into data model objects. + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("Bonsai.Sgen", "0.3.0.0 (Newtonsoft.Json v13.0.0.0)")] + [System.ComponentModel.DescriptionAttribute("Deserializes a sequence of JSON strings into data model objects.")] + [System.ComponentModel.DefaultPropertyAttribute("Type")] + [Bonsai.WorkflowElementCategoryAttribute(Bonsai.ElementCategory.Transform)] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + [System.Xml.Serialization.XmlIncludeAttribute(typeof(Bonsai.Expressions.TypeMapping))] + public partial class DeserializeFromJson : Bonsai.Expressions.SingleArgumentExpressionBuilder + { + + public DeserializeFromJson() + { + Type = new Bonsai.Expressions.TypeMapping(); + } + + public Bonsai.Expressions.TypeMapping Type { get; set; } + + public override System.Linq.Expressions.Expression Build(System.Collections.Generic.IEnumerable arguments) + { + var typeMapping = (Bonsai.Expressions.TypeMapping)Type; + var returnType = typeMapping.GetType().GetGenericArguments()[0]; + return System.Linq.Expressions.Expression.Call( + typeof(DeserializeFromJson), + "Process", + new System.Type[] { returnType }, + System.Linq.Enumerable.Single(arguments)); + } + + private static System.IObservable Process(System.IObservable source) + { + return System.Reactive.Linq.Observable.Select(source, value => Newtonsoft.Json.JsonConvert.DeserializeObject(value)); + } + } +} diff --git a/docs/workflows/person-and-discriminated-pets.json b/docs/workflows/person-and-discriminated-pets.json new file mode 100644 index 0000000..73af29c --- /dev/null +++ b/docs/workflows/person-and-discriminated-pets.json @@ -0,0 +1,84 @@ +{ + "definitions": { + "Cat": { + "properties": { + "pet_type": { + "const": "cat", + "default": "cat", + "title": "Pet Type", + "type": "string" + }, + "age": { + "title": "Age", + "type": "integer" + }, + "can_meow": { + "default": true, + "title": "Can Meow", + "type": "boolean" + } + }, + "required": [ + "age" + ], + "title": "Cat", + "type": "object" + }, + "Dog": { + "properties": { + "pet_type": { + "const": "dog", + "default": "dog", + "title": "Pet Type", + "type": "string" + }, + "age": { + "title": "Age", + "type": "integer" + }, + "can_bark": { + "default": true, + "title": "Can Bark", + "type": "boolean" + } + }, + "required": [ + "age" + ], + "title": "Dog", + "type": "object" + }, + "Pet": { + "discriminator": { + "mapping": { + "cat": "#/definitions/Cat", + "dog": "#/definitions/Dog" + }, + "propertyName": "pet_type" + }, + "oneOf": [ + { + "$ref": "#/definitions/Dog" + }, + { + "$ref": "#/definitions/Cat" + } + ] + } + }, + "properties": { + "owner": { + "title": "Owner", + "type": "string" + }, + "pet": { + "$ref": "#/definitions/Pet" + } + }, + "required": [ + "owner", + "pet" + ], + "title": "PersonAndPet", + "type": "object" +} \ No newline at end of file diff --git a/docs/workflows/person-pet-discriminated-union.bonsai b/docs/workflows/person-pet-discriminated-union.bonsai new file mode 100644 index 0000000..453acb7 --- /dev/null +++ b/docs/workflows/person-pet-discriminated-union.bonsai @@ -0,0 +1,73 @@ + + + + + + + + + Pet + + + + + + CanBark + + + + + + CanMeow + + + + 0 + true + + + + + + true + 0 + + + + + + + + + + + CanBark + + + + + + CanMeow + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 75d3476c8ffe7c91fbf758577042b4a48bd072af Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 28 Dec 2024 18:31:00 +0000 Subject: [PATCH 47/63] Add section on partials --- docs/articles/advanced-usage.md | 35 ++++++++++++++++++- .../Extensions/CatExtensionMethods.cs | 14 ++++++++ docs/workflows/sum-cats.bonsai | 32 +++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 docs/workflows/Extensions/CatExtensionMethods.cs create mode 100644 docs/workflows/sum-cats.bonsai diff --git a/docs/articles/advanced-usage.md b/docs/articles/advanced-usage.md index 3263724..b040677 100644 --- a/docs/articles/advanced-usage.md +++ b/docs/articles/advanced-usage.md @@ -70,4 +70,37 @@ As you can see below, we still get a `Pet` type. Better than `object` but still ::: > [!Important] -> In is general advisable to use references in the `oneOf` syntax. Not only does this decision make your `json-schema` significantly smaller, it will also help `Bonsai.Sgen` generate the correct class hierarchy if multiple unions are present in the schema. If you use inline objects, `Bonsai.Sgen` will likely have to generate a new root class for each union, which can lead to a lot of duplicated code and a more complex object hierarchy. \ No newline at end of file +> In is general advisable to use references in the `oneOf` syntax. Not only does this decision make your `json-schema` significantly smaller, it will also help `Bonsai.Sgen` generate the correct class hierarchy if multiple unions are present in the schema. If you use inline objects, `Bonsai.Sgen` will likely have to generate a new root class for each union, which can lead to a lot of duplicated code and a more complex object hierarchy. + + + +## Extending generated code with `partial` classes + +Since `Bonsai.Sgen` will generate proper `class` for each object in the schema, it is possible to use these types to create custom operators and methods using the `Scriping Extensions` feature of `Bonsai`. However, sometimes we may want to extend the features of the generated classes directly... + +For those that inspected the general `C#` code, you will notice that all classes are marked as [`partial`](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods). This is a feature of `C#` that allows a class to be split. This was a deliberate design choice to allow users to extend the generated code. However, because it is usually always a bad idea to modify generated code directly (e.g. we may want to regenerate it in the future), `partial` classes allows modification to be made in a separate file. + +Suppose we want to sum `Cats`, we can overload the operator with a small method in a separate file:: + +```csharp +namespace PersonAndDiscriminatedPets +{ + partial class Cat{ + public static Cat operator +(Cat c1, Cat c2) + { + return new Cat + { + CanMeow = c1.CanMeow || c2.CanMeow, + Age = c1.Age + c2.Age + }; + } + } +} +``` + +In `Bonsai`, we can now use the `Add` operator to sum `Cats`: + + +:::workflow +![Discriminated Unions](~/workflows/sum-cats.bonsai) +::: diff --git a/docs/workflows/Extensions/CatExtensionMethods.cs b/docs/workflows/Extensions/CatExtensionMethods.cs new file mode 100644 index 0000000..4fd051d --- /dev/null +++ b/docs/workflows/Extensions/CatExtensionMethods.cs @@ -0,0 +1,14 @@ + +namespace PersonAndDiscriminatedPets +{ + partial class Cat{ + public static Cat operator +(Cat c1, Cat c2) + { + return new Cat + { + CanMeow = c1.CanMeow || c2.CanMeow, + Age = c1.Age + c2.Age + }; + } + } +} diff --git a/docs/workflows/sum-cats.bonsai b/docs/workflows/sum-cats.bonsai new file mode 100644 index 0000000..0e3df9e --- /dev/null +++ b/docs/workflows/sum-cats.bonsai @@ -0,0 +1,32 @@ + + + + + + + 0 + true + + + + + 0 + true + + + + + + + + + + + + + + \ No newline at end of file From 6c9e67825e94c2854ed114619e3b978cae4fd6f7 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 28 Dec 2024 18:34:47 +0000 Subject: [PATCH 48/63] Add x-abstract documentation --- docs/articles/advanced-usage.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/articles/advanced-usage.md b/docs/articles/advanced-usage.md index b040677..ceb7fce 100644 --- a/docs/articles/advanced-usage.md +++ b/docs/articles/advanced-usage.md @@ -104,3 +104,8 @@ In `Bonsai`, we can now use the `Add` operator to sum `Cats`: :::workflow ![Discriminated Unions](~/workflows/sum-cats.bonsai) ::: + + +## Other supported tags + +- `x-abstract`: This tag is used to mark a class as abstract. An abstract class will not be generated as an operator in Bonsai. This may be useful for root classes of unions that may never need to be manipulated in Bonsai. \ No newline at end of file From f16b4befa91873c23065e9a58498a21a36b24115 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sat, 28 Dec 2024 18:49:06 +0000 Subject: [PATCH 49/63] Add (de)serialization examples --- .bonsai/Bonsai.config | 3 ++ docs/articles/basic-usage.md | 25 ++++++++++- docs/articles/experimental.md | 0 docs/articles/intro.md | 2 +- docs/workflows/serialization-example.bonsai | 48 +++++++++++++++++++++ 5 files changed, 76 insertions(+), 2 deletions(-) delete mode 100644 docs/articles/experimental.md create mode 100644 docs/workflows/serialization-example.bonsai diff --git a/.bonsai/Bonsai.config b/.bonsai/Bonsai.config index 3db0977..1fa28c4 100644 --- a/.bonsai/Bonsai.config +++ b/.bonsai/Bonsai.config @@ -7,6 +7,7 @@ + @@ -31,6 +32,7 @@ + @@ -39,6 +41,7 @@ + diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index 575cf16..d782e6c 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -195,4 +195,27 @@ For reference types, the generated code will not render a nullable type since re > - `required` means that the field must be present in the object at deserialization time > - An object can be `nullable` and `required` at the same time. This means it MUST be defined in the object, but it can be defined as `null`. > - An object can be `not required` and `nullable`. This does NOT mean that the object is, by default, `null`. It means that the object should have a default value, which can in theory be `null`. -> - An object can be `not required` and `not nullable`. This means that the object must have a default value, which cannot be `null`. \ No newline at end of file +> - An object can be `not required` and `not nullable`. This means that the object must have a default value, which cannot be `null`. + + +## Serialization and Deserialization + +One of the biggest perks of using json-schema to represent our objects is the guaranteed that all records are (de)serializable. This means that we can go from a text-based format (great specification and logging) to a `C#` type seamlessly, and vice-versa. `Bonsai.Sgen` will optionally generate (de)serialization operators for all objects in the schema if the `--serializer` property is not `None`. Currently, two formats are supported out of the box: `Json` (via [`NewtonsoftJson`](https://github.com/JamesNK/Newtonsoft.Json)) and `yaml` (via [`YamlDotNet`](https://github.com/aaubry/YamlDotNet)). + +The two operations are afforded via the `SerializeToYaml` (or `SerializeToJson`) and `DeserializeFromYaml` (or `DeserializeFromJson`) operators, respectively. + +`SerializeToYaml` will take a `T` object (known to the namespace) and return a `string` representation of the object. +`DeserializeFromYaml` will take a `string` and return a `T` object. If validation fails, the operator will throw an exception. + +:::workflow +![(de)serialization](~/workflows/serialization-example.bonsai) +::: + +> [!Tip] +> Remember to add the necessary package references to your `Extensions.csproj` file depending on the serializer you want to use! +> ```xml +> +> +> +> +> ``` \ No newline at end of file diff --git a/docs/articles/experimental.md b/docs/articles/experimental.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/articles/intro.md b/docs/articles/intro.md index 85d8603..1ee7386 100644 --- a/docs/articles/intro.md +++ b/docs/articles/intro.md @@ -4,7 +4,7 @@ uid: intro ## What is Bonsai.Sgen? -`Bonsai.Sgen` is a code generator tool for the [Bonsai programming language](https://bonsai-rx.org/). It leverages [`json-schema`](https://json-schema.org/) as a standard to represent [record-like](https://en.wikipedia.org/wiki/Record_(computer_science)) structures, and automatically generates Bonsai-compatible isomorphic operators to create and manipulate these objects. +`Bonsai.Sgen` is a code generator tool for the [Bonsai programming language](https://bonsai-rx.org/). It leverages [`json-schema`](https://json-schema.org/) as a standard to represent [record-like](https://en.wikipedia.org/wiki/Record_(computer_science)) structures, and automatically generates Bonsai-compatible isomorphic operators to create and manipulate these objects. It builds on top of the [`NJsonSchema` library](https://github.com/RicoSuter/NJsonSchema) by providing further customization of the generated code as well as bonsai-specific features. ## Getting started diff --git a/docs/workflows/serialization-example.bonsai b/docs/workflows/serialization-example.bonsai new file mode 100644 index 0000000..a6285d5 --- /dev/null +++ b/docs/workflows/serialization-example.bonsai @@ -0,0 +1,48 @@ + + + + + + Serialization + + + + + + + + + + + false + false + + + + Deserialization + + + + + + + + + + Pet.HasValue + + + + + + + + + + + + \ No newline at end of file From 0d47ee8e46f2a37701d9674671dc1b41ac44fa1c Mon Sep 17 00:00:00 2001 From: glopesdev Date: Sun, 16 Mar 2025 20:13:13 +0000 Subject: [PATCH 50/63] Include all articles in toc and add missing files --- docs/README.md | 2 +- docs/articles/advanced-usage.md | 5 +-- docs/articles/basic-usage.md | 15 +++---- docs/articles/intro.md | 45 ------------------- docs/articles/toc.yml | 5 ++- docs/articles/why-bonsai-sgen.md | 10 ++--- ... => person-and-dog-nested-building.bonsai} | 0 ...nsai-sgen.bonsai => person-and-dog.bonsai} | 0 8 files changed, 15 insertions(+), 67 deletions(-) delete mode 100644 docs/articles/intro.md rename docs/workflows/{personand-dog-example-nested-building-bonsai-sgen.bonsai => person-and-dog-nested-building.bonsai} (100%) rename docs/workflows/{personand-dog-example-bonsai-sgen.bonsai => person-and-dog.bonsai} (100%) diff --git a/docs/README.md b/docs/README.md index eba8df6..3ddfa3a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Serializer Generator Tool -Tool for automatically generating YAML / JSON serialization classes and constructor operators from schema files. +`Bonsai.Sgen` is a code generator tool for the [Bonsai](https://bonsai-rx.org/) programming language. It leverages [JSON Schema](https://json-schema.org/) as a standard to specify [record data types](https://en.wikipedia.org/wiki/Record_(computer_science)), and automatically generates operators to create and manipulate these objects. It builds on top of [NJsonSchema](https://github.com/RicoSuter/NJsonSchema) by providing further customization of the generated code as well as Bonsai-specific features. ## Getting Started diff --git a/docs/articles/advanced-usage.md b/docs/articles/advanced-usage.md index ceb7fce..653ffae 100644 --- a/docs/articles/advanced-usage.md +++ b/docs/articles/advanced-usage.md @@ -1,7 +1,4 @@ ---- -uid: advanced-usage ---- - +# Advanced Usage ## Unions diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index d782e6c..6cdff37 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -1,7 +1,4 @@ ---- -uid: basic-usage ---- - +# Basic Usage ## Automatic generation of Bonsai code using Bonsai.Sgen @@ -83,7 +80,7 @@ dotnet bonsai.sgen --schema docs/workflows/person_and_dog.json --output docs/wor ``` :::workflow -![PersonAndDog as BonsaiSgen](~/workflows/person-and-dog-example-bonsai-sgen.bonsai) +![Person And Dog](~/workflows/person-and-dog.bonsai) ::: A few things worth noting in this example: @@ -99,7 +96,7 @@ The previous example highlights the simplicity of generating Bonsai code for sim :::workflow -![PersonAndDog building as BonsaiSgen](~/workflows/person-and-dog-nested-building-example-bonsai-sgen.bonsai) +![Person And Dog Nested Building](~/workflows/person-and-dog-nested-building.bonsai) ::: ## Enums @@ -181,7 +178,7 @@ For value types, the generated code will render a [Nullable value type](https:// For reference types, the generated code will not render a nullable type since reference types are already nullable in C#. A consumer can test for `null` to determine if the value is present by simply using an `ExpressionTransform` operator with `it == null`.: :::workflow -![Nullable pet](~/workflows/person-and-pets-enum-nullable.bonsai) +![Nullable pet](~/workflows/person-and-pet-enum-nullable.bonsai) ::: ## Required fields @@ -215,7 +212,7 @@ The two operations are afforded via the `SerializeToYaml` (or `SerializeToJson`) > Remember to add the necessary package references to your `Extensions.csproj` file depending on the serializer you want to use! > ```xml > -> +> > -> +> > ``` \ No newline at end of file diff --git a/docs/articles/intro.md b/docs/articles/intro.md deleted file mode 100644 index 1ee7386..0000000 --- a/docs/articles/intro.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -uid: intro ---- - -## What is Bonsai.Sgen? - -`Bonsai.Sgen` is a code generator tool for the [Bonsai programming language](https://bonsai-rx.org/). It leverages [`json-schema`](https://json-schema.org/) as a standard to represent [record-like](https://en.wikipedia.org/wiki/Record_(computer_science)) structures, and automatically generates Bonsai-compatible isomorphic operators to create and manipulate these objects. It builds on top of the [`NJsonSchema` library](https://github.com/RicoSuter/NJsonSchema) by providing further customization of the generated code as well as bonsai-specific features. - -## Getting started - -1. Navigate to the [Bonsai.Sgen NuGet tool package](https://www.nuget.org/packages/Bonsai.Sgen/) -2. Click `.NET CLI (Local)` and copy the two suggested commands. E.g.: - - ```cmd - dotnet new tool-manifest # if you are setting up this repo - dotnet tool install --local Bonsai.Sgen - ``` - -3. To view the tool help reference documentation, run: - - ```cmd - dotnet bonsai.sgen --help - ``` - -4. To generate YAML serialization classes from a schema file: - - ```cmd - dotnet bonsai.sgen --schema schema.json --serializer YamlDotNet - ``` - -5. To generate JSON serialization classes from a schema file: - - ```cmd - dotnet bonsai.sgen --schema schema.json --serializer NewtonsoftJson - ``` - -6. Copy the generated class file to your project `Extensions` folder. - -7. Add the necessary package references to your `Extensions.csproj` file. For example: - - ```xml - - - - \ No newline at end of file diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml index ef45d5b..982cfb5 100644 --- a/docs/articles/toc.yml +++ b/docs/articles/toc.yml @@ -1 +1,4 @@ -- href: ../index.md \ No newline at end of file +- href: ../index.md +- href: why-bonsai-sgen.md +- href: basic-usage.md +- href: advanced-usage.md \ No newline at end of file diff --git a/docs/articles/why-bonsai-sgen.md b/docs/articles/why-bonsai-sgen.md index 1fb2d7a..29ac5e3 100644 --- a/docs/articles/why-bonsai-sgen.md +++ b/docs/articles/why-bonsai-sgen.md @@ -1,10 +1,6 @@ ---- -uid: why-bonsai-sgen ---- +# Why Bonsai Sgen? -## Why should I care? - -`Bonsai.Sgen` attempts to solve the problem of writing boilerplate code to create represent data structures in Bonsai. Let's try to convince you by looking at a simple example. +`Bonsai.Sgen` attempts to solve the problem of writing boilerplate code to model domain-specific data in Bonsai. Let's try to convince you by looking at a simple example. Let's we have a simple record-like object that represents a ´Person´: @@ -19,7 +15,7 @@ Let's we have a simple record-like object that represents a ´Person´: If we want to represent this object in Bonsai, we have a few alternatives: -1. Using a `DynamicClass` object: +1. Using an [`ExpressionTransform`](xref:Bonsai.Scripting.Expressions.ExpressionTransform) with a [Data Object Initializer](https://bonsai-rx.org/docs/api/Bonsai.Scripting.Expressions.ExpressionTransform.html#data-object-initializers): :::workflow ![Person as DynamicClass](~/workflows/person-example-dynamic-class.bonsai) diff --git a/docs/workflows/personand-dog-example-nested-building-bonsai-sgen.bonsai b/docs/workflows/person-and-dog-nested-building.bonsai similarity index 100% rename from docs/workflows/personand-dog-example-nested-building-bonsai-sgen.bonsai rename to docs/workflows/person-and-dog-nested-building.bonsai diff --git a/docs/workflows/personand-dog-example-bonsai-sgen.bonsai b/docs/workflows/person-and-dog.bonsai similarity index 100% rename from docs/workflows/personand-dog-example-bonsai-sgen.bonsai rename to docs/workflows/person-and-dog.bonsai From 438e7b6a98dd7cae4e649293225bffa7f6178f6a Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Sun, 16 Mar 2025 17:17:10 -0700 Subject: [PATCH 51/63] Improve clarity and consistency --- docs/articles/advanced-usage.md | 64 +++++++++++++--------------- docs/articles/basic-usage.md | 73 ++++++++++++++++---------------- docs/articles/why-bonsai-sgen.md | 22 +++++----- 3 files changed, 75 insertions(+), 84 deletions(-) diff --git a/docs/articles/advanced-usage.md b/docs/articles/advanced-usage.md index 653ffae..ade2af2 100644 --- a/docs/articles/advanced-usage.md +++ b/docs/articles/advanced-usage.md @@ -1,10 +1,10 @@ -# Advanced Usage +# Advanced usage ## Unions -In the previous examples, we have seen how create object properties of a single type. However, in practice, data structures' fields can often be represented by one of several types. We have actually seen a special case of this behavior in the previous nullable example, where a field can be either a value of a given type or `null` (or an Union between type `T` and `null`). +In the previous examples, we have seen how create object properties of a single type. However, in practice, data structures' fields can often be represented by one of several types. We have actually seen a special case of this behavior in the previous nullable example, where a field can be either a value of a given type `T` or `null` (or an Union between type `T` and `null`). -Similarly, `json-schema` allows union types to be defined using the `oneOf` keyword. For example, consider the following schema: +`json-schema` allows union types using the `oneOf` keyword. For example: ```json { @@ -21,18 +21,19 @@ Similarly, `json-schema` allows union types to be defined using the `oneOf` keyw } ``` -If we run `Bonsai.Sgen` on this schema, we will get the following signature for the `FooProperty` property: +Running `Bonsai.Sgen` on this schema generates the following type signature for `FooProperty`: ```csharp public object FooProperty ``` -This is because while the `oneOf` keyword is supported by the `Bonsai.Sgen` tool, for statically typed languages like `C#` and `Bonsai`, we need to know the exact type of the property at compile time. As a result, we opt to "up-cast" the property to the most general type that can represent all the possible types in the union (`object`). It is up to the user to down-cast the property to the correct type at runtime. +While `oneOf` is supported, statically typed languages like `C#` require the exact type at compile time. Thus, the property is "up-cast" to `object`, and users must down-cast it to the correct type at runtime. +## Tagged-Unions ## Tagged Unions -At this point, you might be wondering if there is a way to represent union types in a more type-safe way in json-schema. The answer is yes, and the way to do it is by using [`discriminated unions`](https://en.wikipedia.org/wiki/Tagged_union) (or `tagged union`). The syntax for discriminated unions is not supported by vanilla `json-schema`, but it is supported by the [`OpenAPI` standard](https://swagger.io/docs/specification/v3_0/data-models/inheritance-and-polymorphism/#discriminator), which is a superset of `json-schema`. The key idea behind discriminated unions is to add a `discriminator` field to the schema that specifies the property that will be used to determine the type of the object at runtime. +Unions types can be made type-aware by using[`tagged unions`](https://en.wikipedia.org/wiki/Tagged_union) (or `discriminated unions`). The syntax for tagged unions is not part of the `json-schema` specification, but it is supported by the [`OpenAPI` standard](https://swagger.io/docs/specification/v3_0/data-models/inheritance-and-polymorphism/#discriminator), which is a superset of `json-schema`. The key idea behind tagged unions is to add a `discriminator` field to the schema that specifies the property that will be used to determine the type of the object at runtime. For example, a `Pet` object that can be either a `Dog` or a `Cat` can be represented as follows: @@ -40,69 +41,62 @@ For example, a `Pet` object that can be either a `Dog` or a `Cat` can be represe ```json "Pet": { - "discriminator": { - "mapping": { - "cat": "#/definitions/Cat", - "dog": "#/definitions/Dog" - }, - "propertyName": "pet_type" - }, - "oneOf": [ - { - "$ref": "#/definitions/Dog" - }, - { - "$ref": "#/definitions/Cat" - } - ] - } + "discriminator": { + "mapping": { + "cat": "#/definitions/Cat", + "dog": "#/definitions/Dog" + }, + "propertyName": "pet_type" + }, + "oneOf": [ + { "$ref": "#/definitions/Dog" }, + { "$ref": "#/definitions/Cat" } + ] +} ``` In `C#`, `Bonsai.Sgen` will generate a root type `Pet` that will be inherited by the `Dog` and `Cat` types (since in the worst case scenario, the discriminated property must be shared). The `Pet` type will have a `pet_type` property that will be used to downcast to the proper type at runtime. At this point we can open our example in `Bonsai` and see how the `Pet` type is represented in the workflow. -As you can see below, we still get a `Pet` type. Better than `object` but still not a `Dog` or `Cat` type. Fortunately, `Bonsai.Sgen` will generate an operator that can be used to filter and downcast the `Pet` objects to the correct type at runtime. These are called `Match` operators. After adding a `MatchPet` to our workflow we can select the desired target type which will allow us access to the properties of the `Dog` or `Cat` type. Conversely, we can also upcast a `Dog` or `Cat` to a `Pet` leaving the `MatchPet` operator's `Type` property empty. +As you can see below, we still get a `Pet` type. Better than `object` but still not a `Dog` or `Cat` type. Fortunately, `Bonsai.Sgen` will generate an operator that can be used to filter and downcast the `Pet` objects to the correct type at runtime. These are called `Match` operators. `MatchPet` can be used to select the desired target type which will allow us access to the properties of the `Dog` or `Cat` subtypes. Conversely, we can also upcast a `Dog` or `Cat` to a `Pet` by leaving the `MatchPet` operator's `Type` property empty. :::workflow ![Discriminated Unions](~/workflows/person-pet-discriminated-union.bonsai) ::: > [!Important] -> In is general advisable to use references in the `oneOf` syntax. Not only does this decision make your `json-schema` significantly smaller, it will also help `Bonsai.Sgen` generate the correct class hierarchy if multiple unions are present in the schema. If you use inline objects, `Bonsai.Sgen` will likely have to generate a new root class for each union, which can lead to a lot of duplicated code and a more complex object hierarchy. +> In is advisable to use references in the `oneOf` syntax. Not only does this decision make your `json-schema` significantly smaller, it will also help `Bonsai.Sgen` generate the correct class hierarchy if multiple unions are present in the schema. If you use inline objects, `Bonsai.Sgen` will likely have to generate a new root class for each union, which can lead to a lot of duplicated code and a more complex object hierarchy. ## Extending generated code with `partial` classes -Since `Bonsai.Sgen` will generate proper `class` for each object in the schema, it is possible to use these types to create custom operators and methods using the `Scriping Extensions` feature of `Bonsai`. However, sometimes we may want to extend the features of the generated classes directly... - -For those that inspected the general `C#` code, you will notice that all classes are marked as [`partial`](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods). This is a feature of `C#` that allows a class to be split. This was a deliberate design choice to allow users to extend the generated code. However, because it is usually always a bad idea to modify generated code directly (e.g. we may want to regenerate it in the future), `partial` classes allows modification to be made in a separate file. +Generated classes are marked as [`partial`](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods), allowing you to extend them without modifying the generated code directly. -Suppose we want to sum `Cats`, we can overload the operator with a small method in a separate file:: +For example, to add an operator for summing `Cat` objects: ```csharp namespace PersonAndDiscriminatedPets { - partial class Cat{ + partial class Cat + { public static Cat operator +(Cat c1, Cat c2) { return new Cat { - CanMeow = c1.CanMeow || c2.CanMeow, - Age = c1.Age + c2.Age + CanMeow = c1.CanMeow || c2.CanMeow, + Age = c1.Age + c2.Age }; } } } ``` -In `Bonsai`, we can now use the `Add` operator to sum `Cats`: - +In Bonsai, use the `Add` operator to sum `Cat` objects: :::workflow ![Discriminated Unions](~/workflows/sum-cats.bonsai) ::: - ## Other supported tags -- `x-abstract`: This tag is used to mark a class as abstract. An abstract class will not be generated as an operator in Bonsai. This may be useful for root classes of unions that may never need to be manipulated in Bonsai. \ No newline at end of file +- `x-abstract`: Marks a class as abstract, preventing it from being generated as an operator in Bonsai. diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index 6cdff37..f3ecc13 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -1,10 +1,10 @@ -# Basic Usage +# Basic usage ## Automatic generation of Bonsai code using Bonsai.Sgen -We will expand this example later on, but for now, let's see how we can use `Bonsai.Sgen` to automatically generate the Bonsai code for the `Person` object. +We will expand this example later, but for now, let's see how to use `Bonsai.Sgen` to automatically generate Bonsai code for the `Person` object. -First, we need to define the schema of the object in a JSON file: +First, define the schema of the object in a JSON file: [person](~/workflows/person.json) @@ -21,25 +21,25 @@ First, we need to define the schema of the object in a JSON file: } ``` -Second, we need to run the `Bonsai.Sgen` tool to generate the Bonsai code: +Next, run the `Bonsai.Sgen` tool to generate the Bonsai code: ```cmd dotnet bonsai.sgen --schema docs/workflows/person.json --output docs/workflows/Extensions/PersonSgen.cs ``` -Finally, we can use the generated code in our Bonsai workflow: +Finally, use the generated code in your Bonsai workflow: :::workflow ![Person as BonsaiSgen](~/workflows/person-example-bonsai-sgen.bonsai) ::: -As you can probably tell, the `Bonsai.Sgen` approach is much more concise and less error-prone than the previous ones. It allows you to focus on the data structure itself and not on the boilerplate code required to create it in Bonsai. Moreover, as we will see later, the tool also automatically generates serialization and deserialization boilerplate code for the object, which can be very useful when working with external data sources. +The `Bonsai.Sgen` approach is concise and less error-prone, allowing you to focus on the data structure itself rather than boilerplate code. Additionally, the tool automatically generates, type-aware, serialization and deserialization operators, which are useful when working with external data sources. -Finally, if one considers the `json-schema` as the "source of truth" for the data structure representation, it is possible to generate multiple representations of the object in different languages, ensuring interoperability. This can be very useful when working in a multi-language environment (e.g. running experiment in Bonsai and analysis in Python) and when sharing data structures across different projects. +By treating the `json-schema` as the "source of truth," you can generate multiple representations of the object in different languages, ensuring interoperability. This is particularly useful in multi-language environments (e.g., running experiments in Bonsai and analysis in Python) and when sharing data structures across projects. ## Multiple objects -The previous example shows how a single record can be modelled. In practice, projects often require modelling different types of objects. This is where `Bonsai.Sgen` shines, as it allows you to generate multiple objects from a single schema file: +The previous example demonstrates modeling a single record. In practice, projects often require modeling multiple object types. This is where `Bonsai.Sgen` excels, allowing you to generate multiple objects from a single schema file: [person_and_dog](~/workflows/person-and-dog.json) @@ -88,13 +88,12 @@ A few things worth noting in this example: - The schema file contains two definitions: `Person` and `Dog` that give rise to two operators (`Person` and `Dog`) in the generated code. - A third definition `PersonAndPet` is used to combine the two objects into a single record. This can be omitted as we will see later by using the `x-abstract` property. - The `--namespace` flag is used to specify the namespace of the generated code. This is useful to prevent name clashes between different schemas (e.g. `PersonAndDog.Person` and `Person` from the previous example). -- Both `Person` and `Dog` `json-schema` objects are passed as references. This is extremely important as it allows the reuse of the same definition in multiple places. This is particularly useful when working with complex data structures that share common fields. If definitions are passed in-line (i.e. redefined each time), Bonsai.Sgen may not be able to correctly identify them as the same object. +- Both `Person` and `Dog` are passed as references. If definitions are instead passed in-line (i.e. redefined each time), Bonsai.Sgen may not be able to correctly identify them as the same object, and may thus generate multiple classes of the same object. ## Nested objects The previous example highlights the simplicity of generating Bonsai code for simple objects. However, the real power of `Bonsai.Sgen` comes when dealing with more complex data structures, such as nested objects. Moreover, Bonsai syntax lends itself quite nicely to represent these nested structures as well as build them: - :::workflow ![Person And Dog Nested Building](~/workflows/person-and-dog-nested-building.bonsai) ::: @@ -109,18 +108,19 @@ We can replace the `Pet` object in the previous example with an [`enum`](https:/ ```json (...) - { - "Pet": { - "title": "Pet", - "type": "string", - "enum": ["Dog", "Cat", "Fish", "Bird", "Reptile"] - } - }, - "type": "object", - "properties": { - "owner": {"$ref": "#/definitions/Person"}, - "pet": {"$ref": "#/definitions/Pet"} + +{ + "Pet": { + "title": "Pet", + "type": "string", + "enum": ["Dog", "Cat", "Fish", "Bird", "Reptile"] } +}, +"type": "object", +"properties": { + "owner": {"$ref": "#/definitions/Person"}, + "pet": {"$ref": "#/definitions/Pet"} +} ``` In Bonsai, they can be manipulated as [`Enum`](https://learn.microsoft.com/en-us/dotnet/api/system.enum?view=net-9.0) types: @@ -148,10 +148,10 @@ In Bonsai, they can be manipulated as [`Enum`](https://learn.microsoft.com/en-us `Bonsai.Sgen` also supports the generation of lists using the `array` type in the `json-schema`: ```json - "pets": { - "type": "array", - "items": {"$ref": "#/definitions/Pet"} - } +"pets": { + "type": "array", + "items": {"$ref": "#/definitions/Pet"} +} ``` `json-schema` `array`s will be rendered as [`List`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1?view=net-9.0) in the generated code and can be manipulated (and created) as such. @@ -165,17 +165,17 @@ In Bonsai, they can be manipulated as [`Enum`](https://learn.microsoft.com/en-us `json-schema` supports the `null` type, which can be used to represent nullable types. The standard is a bit loose in this regard, but `Bonsai.Sgen` will generate a nullable-T if the json-schema represents it using the `oneOf` keyword: ```json - "pet": { - "oneOf": [ - {"$ref": "#/definitions/Pet"}, - {"type": "null"} - ] - } +"pet": { + "oneOf": [ + {"$ref": "#/definitions/Pet"}, + {"type": "null"} + ] +} ``` For value types, the generated code will render a [Nullable value type](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/nullable-value-types) type. This type will expose two properties: `HasValue` and `Value`, that can be used to test and manipulate the type, respectively. -For reference types, the generated code will not render a nullable type since reference types are already nullable in C#. A consumer can test for `null` to determine if the value is present by simply using an `ExpressionTransform` operator with `it == null`.: +For reference types, the generated code will not render a nullable type since reference types are already nullable in C#. A consumer can test for `null` to determine if the value is present by simply using an `ExpressionTransform` operator with `it == null`: :::workflow ![Nullable pet](~/workflows/person-and-pet-enum-nullable.bonsai) @@ -186,20 +186,19 @@ For reference types, the generated code will not render a nullable type since re `json-schema` supports the [`required`](https://json-schema.org/learn/getting-started-step-by-step#define-required-properties) keyword to specify which fields are required. By default, all fields are optional. This can be useful to enforce the presence of certain fields in the object at deserialization time. However, `Bonsai.Sgen` will not generate any code to enforce this requirement during object construction, only at deserialization. It is up to the user to ensure that the object is correctly populated before using it. > [!Note] -> Some confusion maybe arise about the distinction between `null` and `required`. This is all the more confusing since different languages and libraries may refer to these concepts differently. For the sake of this tool (and honestly in general) the following definitions are used: - +> Some confusion may arise about the distinction between `null` and `required`. This is all the more confusing since different languages and libraries may refer to these concepts differently. For the sake of this tool (and honestly in general) the following definitions are used: +> > - `nullable` means that the field can be `null` or type `T` > - `required` means that the field must be present in the object at deserialization time > - An object can be `nullable` and `required` at the same time. This means it MUST be defined in the object, but it can be defined as `null`. > - An object can be `not required` and `nullable`. This does NOT mean that the object is, by default, `null`. It means that the object should have a default value, which can in theory be `null`. > - An object can be `not required` and `not nullable`. This means that the object must have a default value, which cannot be `null`. - ## Serialization and Deserialization -One of the biggest perks of using json-schema to represent our objects is the guaranteed that all records are (de)serializable. This means that we can go from a text-based format (great specification and logging) to a `C#` type seamlessly, and vice-versa. `Bonsai.Sgen` will optionally generate (de)serialization operators for all objects in the schema if the `--serializer` property is not `None`. Currently, two formats are supported out of the box: `Json` (via [`NewtonsoftJson`](https://github.com/JamesNK/Newtonsoft.Json)) and `yaml` (via [`YamlDotNet`](https://github.com/aaubry/YamlDotNet)). +One of the biggest perks of using json-schema to represent our objects is the guarantee that all records are (de)serializable. This means that we can go from a text-based format (great for specification and logging) to a `C#` type seamlessly, and vice-versa. `Bonsai.Sgen` will optionally generate (de)serialization operators for all objects in the schema if the `--serializer` property is not `None`. Currently, two formats are supported out of the box: `Json` (via [`NewtonsoftJson`](https://github.com/JamesNK/Newtonsoft.Json)) and `yaml` (via [`YamlDotNet`](https://github.com/aaubry/YamlDotNet)). -The two operations are afforded via the `SerializeToYaml` (or `SerializeToJson`) and `DeserializeFromYaml` (or `DeserializeFromJson`) operators, respectively. +The two operations are afforded via the `SerializeToYaml` (or `SerializeToJson`) and `DeserializeFromYaml` (or `DeserializeFromJson`) operators, respectively. `SerializeToYaml` will take a `T` object (known to the namespace) and return a `string` representation of the object. `DeserializeFromYaml` will take a `string` and return a `T` object. If validation fails, the operator will throw an exception. diff --git a/docs/articles/why-bonsai-sgen.md b/docs/articles/why-bonsai-sgen.md index 29ac5e3..7933339 100644 --- a/docs/articles/why-bonsai-sgen.md +++ b/docs/articles/why-bonsai-sgen.md @@ -1,19 +1,17 @@ # Why Bonsai Sgen? -`Bonsai.Sgen` attempts to solve the problem of writing boilerplate code to model domain-specific data in Bonsai. Let's try to convince you by looking at a simple example. +`Bonsai.Sgen` addresses the challenge of writing boilerplate code to model domain-specific data in Bonsai. Let's explore this with a simple example. -Let's we have a simple record-like object that represents a ´Person´: +Suppose we have a record-like object that represents a `Person`: - -| Field Name | Type | Description | -|------------|----------|---------------------------| -| age | int | The age of a person | +| Field Name | Type | Description | +|------------|----------|------------------------------| +| age | int | The age of a person | | first_name | string | The first name of the person | | last_name | string | The last name of the person | -| dob | datetime | Date of birth | - +| dob | datetime | Date of birth | -If we want to represent this object in Bonsai, we have a few alternatives: +To represent this object in Bonsai, we have a few options: 1. Using an [`ExpressionTransform`](xref:Bonsai.Scripting.Expressions.ExpressionTransform) with a [Data Object Initializer](https://bonsai-rx.org/docs/api/Bonsai.Scripting.Expressions.ExpressionTransform.html#data-object-initializers): @@ -21,7 +19,7 @@ If we want to represent this object in Bonsai, we have a few alternatives: ![Person as DynamicClass](~/workflows/person-example-dynamic-class.bonsai) ::: -This approach is rather brittle as the representation of the record does not exist as a "first class citizen" and only at compile-time. This has a few implications one of which is the inability to create [Subject Sources](https://bonsai-rx.org/docs/articles/subjects.html#source-subjects) from the type. +This approach is brittle because the record representation exists only at compile-time and not as a "first-class citizen." This limitation prevents the creation of [Subject Sources](https://bonsai-rx.org/docs/articles/subjects.html#source-subjects) from the type. 2. Modeling the object as a C# class using [Scripting Extensions](https://bonsai-rx.org/docs/articles/scripting-extensions.html): @@ -35,7 +33,7 @@ public class Person } ``` -This approach is more robust than the previous one, but it requires writing additional, boilerplate code to allow the creation of the object in Bonsai: +While more robust, this approach requires additional boilerplate code to enable object creation in Bonsai: ```Csharp using Bonsai; @@ -63,4 +61,4 @@ public class CreatePerson : Source } ``` -As you can probably tell, neither of these approaches is ideal when it comes to scale large projects. This is where `Bonsai.Sgen` comes in. \ No newline at end of file +As you can see, neither approach scales well for large projects. This is where `Bonsai.Sgen` comes in. \ No newline at end of file From d31811e399181bad8d95508104e2564d0db9a634 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Mon, 17 Mar 2025 08:52:06 -0700 Subject: [PATCH 52/63] Change title --- docs/articles/why-bonsai-sgen.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/articles/why-bonsai-sgen.md b/docs/articles/why-bonsai-sgen.md index 7933339..2ae723c 100644 --- a/docs/articles/why-bonsai-sgen.md +++ b/docs/articles/why-bonsai-sgen.md @@ -1,4 +1,6 @@ -# Why Bonsai Sgen? +# Why use Bonsai.Sgen? + +## Data schemas `Bonsai.Sgen` addresses the challenge of writing boilerplate code to model domain-specific data in Bonsai. Let's explore this with a simple example. From 6ba1dc235d54cb99a9d9a1af8569b7e35867d0a6 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Mon, 17 Mar 2025 09:07:05 -0700 Subject: [PATCH 53/63] Improve clarity and formatting --- docs/articles/advanced-usage.md | 8 ++-- docs/articles/basic-usage.md | 8 ++-- docs/articles/why-bonsai-sgen.md | 68 ++++++++++++++++---------------- 3 files changed, 41 insertions(+), 43 deletions(-) diff --git a/docs/articles/advanced-usage.md b/docs/articles/advanced-usage.md index ade2af2..26281d6 100644 --- a/docs/articles/advanced-usage.md +++ b/docs/articles/advanced-usage.md @@ -31,9 +31,7 @@ While `oneOf` is supported, statically typed languages like `C#` require the exa ## Tagged-Unions -## Tagged Unions - -Unions types can be made type-aware by using[`tagged unions`](https://en.wikipedia.org/wiki/Tagged_union) (or `discriminated unions`). The syntax for tagged unions is not part of the `json-schema` specification, but it is supported by the [`OpenAPI` standard](https://swagger.io/docs/specification/v3_0/data-models/inheritance-and-polymorphism/#discriminator), which is a superset of `json-schema`. The key idea behind tagged unions is to add a `discriminator` field to the schema that specifies the property that will be used to determine the type of the object at runtime. +Unions types can be made type-aware by using [`tagged unions`](https://en.wikipedia.org/wiki/Tagged_union) (or `discriminated unions`). The syntax for tagged unions is not part of the `json-schema` specification, but it is supported by the [`OpenAPI` standard](https://swagger.io/docs/specification/v3_0/data-models/inheritance-and-polymorphism/#discriminator), which is a superset of `json-schema`. The key idea behind tagged unions is to add a `discriminator` field to the schema that specifies the property that will be used to determine the type of the object at runtime. For example, a `Pet` object that can be either a `Dog` or a `Cat` can be represented as follows: @@ -64,7 +62,7 @@ As you can see below, we still get a `Pet` type. Better than `object` but still ::: > [!Important] -> In is advisable to use references in the `oneOf` syntax. Not only does this decision make your `json-schema` significantly smaller, it will also help `Bonsai.Sgen` generate the correct class hierarchy if multiple unions are present in the schema. If you use inline objects, `Bonsai.Sgen` will likely have to generate a new root class for each union, which can lead to a lot of duplicated code and a more complex object hierarchy. +> In is strongly recommended to use references with the `oneOf` syntax. Not only does this decision make your `json-schema` significantly smaller, it will also help `Bonsai.Sgen` generate the correct class hierarchy if multiple unions are present in the schema. If you use inline objects, `Bonsai.Sgen` will likely have to generate a new root class for each union, which can lead to a lot of duplicated code and a more complex object hierarchy. @@ -97,6 +95,6 @@ In Bonsai, use the `Add` operator to sum `Cat` objects: ![Discriminated Unions](~/workflows/sum-cats.bonsai) ::: -## Other supported tags +## Supported tags - `x-abstract`: Marks a class as abstract, preventing it from being generated as an operator in Bonsai. diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index f3ecc13..dde4765 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -23,7 +23,7 @@ First, define the schema of the object in a JSON file: Next, run the `Bonsai.Sgen` tool to generate the Bonsai code: -```cmd +```shell dotnet bonsai.sgen --schema docs/workflows/person.json --output docs/workflows/Extensions/PersonSgen.cs ``` @@ -33,7 +33,7 @@ Finally, use the generated code in your Bonsai workflow: ![Person as BonsaiSgen](~/workflows/person-example-bonsai-sgen.bonsai) ::: -The `Bonsai.Sgen` approach is concise and less error-prone, allowing you to focus on the data structure itself rather than boilerplate code. Additionally, the tool automatically generates, type-aware, serialization and deserialization operators, which are useful when working with external data sources. +The `Bonsai.Sgen` approach is concise and less error-prone, allowing you to focus on the data structure itself rather than boilerplate code. Additionally, the tool automatically generates, type-aware, [serialization and deserialization operators](#serialization-and-deserialization), which are useful when working with external data sources. By treating the `json-schema` as the "source of truth," you can generate multiple representations of the object in different languages, ensuring interoperability. This is particularly useful in multi-language environments (e.g., running experiments in Bonsai and analysis in Python) and when sharing data structures across projects. @@ -75,7 +75,7 @@ The previous example demonstrates modeling a single record. In practice, project } ``` -```cmd +```shell dotnet bonsai.sgen --schema docs/workflows/person_and_dog.json --output docs/workflows/Extensions/PersonAndDogSgen.cs --namespace PersonAndDog ``` @@ -92,7 +92,7 @@ A few things worth noting in this example: ## Nested objects -The previous example highlights the simplicity of generating Bonsai code for simple objects. However, the real power of `Bonsai.Sgen` comes when dealing with more complex data structures, such as nested objects. Moreover, Bonsai syntax lends itself quite nicely to represent these nested structures as well as build them: +The real power of `Bonsai.Sgen` comes when dealing with more complex data structures, such as nested objects. Bonsai syntax lends itself quite nicely to represent, as well as compose and manipulate them: :::workflow ![Person And Dog Nested Building](~/workflows/person-and-dog-nested-building.bonsai) diff --git a/docs/articles/why-bonsai-sgen.md b/docs/articles/why-bonsai-sgen.md index 2ae723c..6c86cd2 100644 --- a/docs/articles/why-bonsai-sgen.md +++ b/docs/articles/why-bonsai-sgen.md @@ -17,50 +17,50 @@ To represent this object in Bonsai, we have a few options: 1. Using an [`ExpressionTransform`](xref:Bonsai.Scripting.Expressions.ExpressionTransform) with a [Data Object Initializer](https://bonsai-rx.org/docs/api/Bonsai.Scripting.Expressions.ExpressionTransform.html#data-object-initializers): -:::workflow -![Person as DynamicClass](~/workflows/person-example-dynamic-class.bonsai) -::: + :::workflow + ![Person as DynamicClass](~/workflows/person-example-dynamic-class.bonsai) + ::: -This approach is brittle because the record representation exists only at compile-time and not as a "first-class citizen." This limitation prevents the creation of [Subject Sources](https://bonsai-rx.org/docs/articles/subjects.html#source-subjects) from the type. + This approach is brittle because the record representation exists only at compile-time and not as a "first-class citizen". For instance, this limitation prevents the creation of [Subject Sources](https://bonsai-rx.org/docs/articles/subjects.html#source-subjects) from the type. 2. Modeling the object as a C# class using [Scripting Extensions](https://bonsai-rx.org/docs/articles/scripting-extensions.html): -```Csharp -public class Person -{ - public int Age; - public string FirstName; - public string LastName; - public DateTime DOB; -} -``` + ```Csharp + public class Person + { + public int Age; + public string FirstName; + public string LastName; + public DateTime DOB; + } + ``` -While more robust, this approach requires additional boilerplate code to enable object creation in Bonsai: + While more robust, this approach requires additional boilerplate code to enable object creation in Bonsai: -```Csharp -using Bonsai; -using System; -using System.Reactive.Linq; + ```Csharp + using Bonsai; + using System; + using System.Reactive.Linq; -public class CreatePerson : Source -{ + public class CreatePerson : Source + { - public int Age { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - public DateTime DOB { get; set; } + public int Age { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public DateTime DOB { get; set; } - public override IObservable Generate() - { - return Observable.Return(new Person + public override IObservable Generate() { - Age = Age, - FirstName = FirstName, - LastName = LastName, - DOB = DOB - }); + return Observable.Return(new Person + { + Age = Age, + FirstName = FirstName, + LastName = LastName, + DOB = DOB + }); + } } -} -``` + ``` As you can see, neither approach scales well for large projects. This is where `Bonsai.Sgen` comes in. \ No newline at end of file From b4370234ba1cf1b2e1b1014185a3265ea17b93c0 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Mon, 17 Mar 2025 09:15:05 -0700 Subject: [PATCH 54/63] Add missing workflow --- docs/workflows/person-and-pet-enum.bonsai | 28 +++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/workflows/person-and-pet-enum.bonsai b/docs/workflows/person-and-pet-enum.bonsai index eb14530..a4b6fc4 100644 --- a/docs/workflows/person-and-pet-enum.bonsai +++ b/docs/workflows/person-and-pet-enum.bonsai @@ -1,9 +1,33 @@  - - + + + + + + Pet + + + + Dog + + + + + + + Value + Dog + + + + + + + \ No newline at end of file From 992daf535657dc681a874fbb27851d4a0f762263 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 20 Mar 2025 09:38:53 -0700 Subject: [PATCH 55/63] Refactor from @banchan86 comments --- docs/articles/advanced-usage.md | 2 +- docs/articles/basic-usage.md | 12 ++++++++---- docs/articles/why-bonsai-sgen.md | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/articles/advanced-usage.md b/docs/articles/advanced-usage.md index 26281d6..30c8479 100644 --- a/docs/articles/advanced-usage.md +++ b/docs/articles/advanced-usage.md @@ -68,7 +68,7 @@ As you can see below, we still get a `Pet` type. Better than `object` but still ## Extending generated code with `partial` classes -Generated classes are marked as [`partial`](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods), allowing you to extend them without modifying the generated code directly. +Generated classes are marked as [`partial`](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods), allowing you to extend them without modifying the generated code directly. This can be done by placing the new `.cs` file in the [`Extensions`](https://bonsai-rx.org/docs/articles/scripting-extensions.html) folder of your project. For example, to add an operator for summing `Cat` objects: diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index dde4765..226038e 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -1,5 +1,9 @@ # Basic usage +> [!TIP] +> It is strongly recommend to be familiar with [Bonsai Scripting Extensions](https://bonsai-rx.org/docs/articles/scripting-extensions.html) before using this tool. + + ## Automatic generation of Bonsai code using Bonsai.Sgen We will expand this example later, but for now, let's see how to use `Bonsai.Sgen` to automatically generate Bonsai code for the `Person` object. @@ -24,7 +28,7 @@ First, define the schema of the object in a JSON file: Next, run the `Bonsai.Sgen` tool to generate the Bonsai code: ```shell -dotnet bonsai.sgen --schema docs/workflows/person.json --output docs/workflows/Extensions/PersonSgen.cs +dotnet bonsai.sgen --schema person.json --output Extensions/PersonSgen.cs ``` Finally, use the generated code in your Bonsai workflow: @@ -76,7 +80,7 @@ The previous example demonstrates modeling a single record. In practice, project ``` ```shell -dotnet bonsai.sgen --schema docs/workflows/person_and_dog.json --output docs/workflows/Extensions/PersonAndDogSgen.cs --namespace PersonAndDog +dotnet bonsai.sgen --schema person-and-dog.json --output Extensions/PersonAndDogSgen.cs --namespace PersonAndDog ``` :::workflow @@ -86,7 +90,7 @@ dotnet bonsai.sgen --schema docs/workflows/person_and_dog.json --output docs/wor A few things worth noting in this example: - The schema file contains two definitions: `Person` and `Dog` that give rise to two operators (`Person` and `Dog`) in the generated code. -- A third definition `PersonAndPet` is used to combine the two objects into a single record. This can be omitted as we will see later by using the `x-abstract` property. +- A third definition `PersonAndPet` is used to combine the two objects into a single record. - The `--namespace` flag is used to specify the namespace of the generated code. This is useful to prevent name clashes between different schemas (e.g. `PersonAndDog.Person` and `Person` from the previous example). - Both `Person` and `Dog` are passed as references. If definitions are instead passed in-line (i.e. redefined each time), Bonsai.Sgen may not be able to correctly identify them as the same object, and may thus generate multiple classes of the same object. @@ -175,7 +179,7 @@ In Bonsai, they can be manipulated as [`Enum`](https://learn.microsoft.com/en-us For value types, the generated code will render a [Nullable value type](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/nullable-value-types) type. This type will expose two properties: `HasValue` and `Value`, that can be used to test and manipulate the type, respectively. -For reference types, the generated code will not render a nullable type since reference types are already nullable in C#. A consumer can test for `null` to determine if the value is present by simply using an `ExpressionTransform` operator with `it == null`: +For reference types, the generated code will not render a nullable type since reference types are already nullable in C#. A data consumer application can test for `null` to determine if the value is present by simply using an `ExpressionTransform` operator with `it == null`: :::workflow ![Nullable pet](~/workflows/person-and-pet-enum-nullable.bonsai) diff --git a/docs/articles/why-bonsai-sgen.md b/docs/articles/why-bonsai-sgen.md index 6c86cd2..116702c 100644 --- a/docs/articles/why-bonsai-sgen.md +++ b/docs/articles/why-bonsai-sgen.md @@ -15,7 +15,7 @@ Suppose we have a record-like object that represents a `Person`: To represent this object in Bonsai, we have a few options: -1. Using an [`ExpressionTransform`](xref:Bonsai.Scripting.Expressions.ExpressionTransform) with a [Data Object Initializer](https://bonsai-rx.org/docs/api/Bonsai.Scripting.Expressions.ExpressionTransform.html#data-object-initializers): +1. Using an [`ExpressionTransform`](xref:Bonsai.Scripting.Expressions.ExpressionTransform) with a [Data Object Initializer](xref:Bonsai.Scripting.Expressions.ExpressionTransform#data-object-initializers): :::workflow ![Person as DynamicClass](~/workflows/person-example-dynamic-class.bonsai) From 6efe3657ba6adc58e87f76d244a00c59f46e046c Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:20:15 -0700 Subject: [PATCH 56/63] Minor clarity improvements --- docs/articles/basic-usage.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index 226038e..b3734b7 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -158,7 +158,7 @@ In Bonsai, they can be manipulated as [`Enum`](https://learn.microsoft.com/en-us } ``` -`json-schema` `array`s will be rendered as [`List`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1?view=net-9.0) in the generated code and can be manipulated (and created) as such. +`json-schema` `array` will be rendered as [`List`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1?view=net-9.0) in the generated code and can be manipulated (and created) as such. :::workflow ![Person and Pets](~/workflows/person-and-pets-enum.bonsai) @@ -179,7 +179,7 @@ In Bonsai, they can be manipulated as [`Enum`](https://learn.microsoft.com/en-us For value types, the generated code will render a [Nullable value type](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/nullable-value-types) type. This type will expose two properties: `HasValue` and `Value`, that can be used to test and manipulate the type, respectively. -For reference types, the generated code will not render a nullable type since reference types are already nullable in C#. A data consumer application can test for `null` to determine if the value is present by simply using an `ExpressionTransform` operator with `it == null`: +For reference types, the generated code will not render a nullable type since reference types are already nullable in C#. An application can test for `null` to determine if the value is present by simply using an `ExpressionTransform` operator with `it == null`: :::workflow ![Nullable pet](~/workflows/person-and-pet-enum-nullable.bonsai) From d973a2706f4bea41451ef3fe2491e39beec383ae Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 3 Jul 2025 08:36:20 -0700 Subject: [PATCH 57/63] Fix landing page --- docs/articles/toc.yml | 3 ++- docs/index.md | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml index 982cfb5..98b79c8 100644 --- a/docs/articles/toc.yml +++ b/docs/articles/toc.yml @@ -1,4 +1,5 @@ -- href: ../index.md +- name: Getting Started + href: ../index.md - href: why-bonsai-sgen.md - href: basic-usage.md - href: advanced-usage.md \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index ba991fe..19f2082 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1 @@ ---- -_layout: landing ---- - [!INCLUDE [](README.md)] From f9509eff0416e96942abb6ce313981635f1c35ce Mon Sep 17 00:00:00 2001 From: brunocruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Mon, 7 Jul 2025 10:16:47 -0700 Subject: [PATCH 58/63] Apply suggestions from code review Improve clarity Be consistent with naming json references Co-authored-by: Shawn Tan --- docs/articles/advanced-usage.md | 2 +- docs/articles/basic-usage.md | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/articles/advanced-usage.md b/docs/articles/advanced-usage.md index 30c8479..24e23a9 100644 --- a/docs/articles/advanced-usage.md +++ b/docs/articles/advanced-usage.md @@ -35,7 +35,7 @@ Unions types can be made type-aware by using [`tagged unions`](https://en.wikipe For example, a `Pet` object that can be either a `Dog` or a `Cat` can be represented as follows: -[person](~/workflows/person-and-discriminated-pets.json) +[person-and-discriminated-pets.json](~/workflows/person-and-discriminated-pets.json) ```json "Pet": { diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index b3734b7..2992625 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -1,7 +1,7 @@ # Basic usage > [!TIP] -> It is strongly recommend to be familiar with [Bonsai Scripting Extensions](https://bonsai-rx.org/docs/articles/scripting-extensions.html) before using this tool. +It is strongly recommended to be familiar with [Bonsai Scripting Extensions](https://bonsai-rx.org/docs/articles/scripting-extensions.html) before using this tool. ## Automatic generation of Bonsai code using Bonsai.Sgen @@ -10,7 +10,7 @@ We will expand this example later, but for now, let's see how to use `Bonsai.Sge First, define the schema of the object in a JSON file: -[person](~/workflows/person.json) +[person.json](~/workflows/person.json) ```json { @@ -45,7 +45,7 @@ By treating the `json-schema` as the "source of truth," you can generate multipl The previous example demonstrates modeling a single record. In practice, projects often require modeling multiple object types. This is where `Bonsai.Sgen` excels, allowing you to generate multiple objects from a single schema file: -[person_and_dog](~/workflows/person-and-dog.json) +[person-and-dog.json](~/workflows/person-and-dog.json) ```json { @@ -108,7 +108,7 @@ The real power of `Bonsai.Sgen` comes when dealing with more complex data struct We can replace the `Pet` object in the previous example with an [`enum`](https://json-schema.org/understanding-json-schema/reference/enum): -[Pet as Enum](~/workflows/person-and-pet-enum.json). +[person-and-pet-enum.json](~/workflows/person-and-pet-enum.json). ```json (...) @@ -177,7 +177,7 @@ In Bonsai, they can be manipulated as [`Enum`](https://learn.microsoft.com/en-us } ``` -For value types, the generated code will render a [Nullable value type](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/nullable-value-types) type. This type will expose two properties: `HasValue` and `Value`, that can be used to test and manipulate the type, respectively. +For value types, the generated code will render a [Nullable value type](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/nullable-value-types). This type will expose two properties: `HasValue` and `Value`, that can be used to test and manipulate the type, respectively. For reference types, the generated code will not render a nullable type since reference types are already nullable in C#. An application can test for `null` to determine if the value is present by simply using an `ExpressionTransform` operator with `it == null`: @@ -190,7 +190,7 @@ For reference types, the generated code will not render a nullable type since re `json-schema` supports the [`required`](https://json-schema.org/learn/getting-started-step-by-step#define-required-properties) keyword to specify which fields are required. By default, all fields are optional. This can be useful to enforce the presence of certain fields in the object at deserialization time. However, `Bonsai.Sgen` will not generate any code to enforce this requirement during object construction, only at deserialization. It is up to the user to ensure that the object is correctly populated before using it. > [!Note] -> Some confusion may arise about the distinction between `null` and `required`. This is all the more confusing since different languages and libraries may refer to these concepts differently. For the sake of this tool (and honestly in general) the following definitions are used: +> Some confusion may arise about the distinction between `null` and `required`. This is all the more confusing since different languages and libraries may refer to these concepts in different ways. For the sake of this tool, the following definitions are used: > > - `nullable` means that the field can be `null` or type `T` > - `required` means that the field must be present in the object at deserialization time From 0f047c5bbba6aad07c44796f8b63798376635af6 Mon Sep 17 00:00:00 2001 From: Shawn Tan Date: Mon, 7 Jul 2025 10:37:59 -0700 Subject: [PATCH 59/63] Add json files to docfx.json resources and fix tip alert --- docs/articles/basic-usage.md | 2 +- docs/docfx.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index 2992625..4e33671 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -1,7 +1,7 @@ # Basic usage > [!TIP] -It is strongly recommended to be familiar with [Bonsai Scripting Extensions](https://bonsai-rx.org/docs/articles/scripting-extensions.html) before using this tool. +> It is strongly recommended to be familiar with [Bonsai Scripting Extensions](https://bonsai-rx.org/docs/articles/scripting-extensions.html) before using this tool. ## Automatic generation of Bonsai code using Bonsai.Sgen diff --git a/docs/docfx.json b/docs/docfx.json index f49ac87..c09e81c 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -36,7 +36,7 @@ { "files": [ "images/**", - "workflows/**/*.{bonsai,svg}", + "workflows/**/*.{bonsai,svg,json}", "{articles,tutorials,examples}/**/*.{bonsai,svg}" ] }, From 8a268bea3082d7a30b81a4d0c1b178a7671f8d4f Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 8 Jul 2025 16:44:18 +0100 Subject: [PATCH 60/63] Simplify getting started and add missing links --- docs/README.md | 37 +++++++++++++------------------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/docs/README.md b/docs/README.md index 3ddfa3a..5697da6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,45 +4,34 @@ ## Getting Started -1. Navigate to the [Bonsai.Sgen NuGet tool package](https://www.nuget.org/packages/Bonsai.Sgen/) -2. Click `.NET CLI (Local)` and copy the two suggested commands. E.g.: +1. Install `Bonsai.Sgen` as a local tool: ```cmd - dotnet new tool-manifest # if you are setting up this repo - dotnet tool install --local Bonsai.Sgen + dotnet new tool-manifest ``` -3. To view the tool help reference documentation, run: - ```cmd - dotnet bonsai.sgen --help + dotnet tool install --local Bonsai.Sgen ``` -4. To generate YAML serialization classes from a schema file: +2. Generate YAML serialization classes from a schema file: ```cmd dotnet bonsai.sgen --schema schema.json --serializer YamlDotNet ``` -5. To generate JSON serialization classes from a schema file: - - ```cmd - dotnet bonsai.sgen --schema schema.json --serializer NewtonsoftJson - ``` +3. Copy the generated class file to your project `Extensions` folder. -6. Copy the generated class file to your project `Extensions` folder. - -7. Add the necessary package references to your `Extensions.csproj` file. For example: +4. Add the necessary package references to your `Extensions.csproj` file: ```xml - - - - + ``` -8. To restore the tool at any point, run: +## Additional Documentation - ```cmd - dotnet tool restore - ``` +For additional documentation and examples, refer to the [Sgen documentation pages](https://bonsai-rx.org/sgen). + +## Feedback & Contributing + +`Bonsai.Sgen` is released as open source under the [MIT license](https://licenses.nuget.org/MIT). Bug reports and contributions are welcome at [the GitHub repository](https://github.com/bonsai-rx/sgen). \ No newline at end of file From d3799f6fe210bbc51040add3635207cc0e05c034 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 8 Jul 2025 16:52:22 +0100 Subject: [PATCH 61/63] Update environment version to 2.9.0 --- .bonsai/Bonsai.config | 80 +++++++++++++++++++++++-------------------- .bonsai/NuGet.config | 2 +- 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/.bonsai/Bonsai.config b/.bonsai/Bonsai.config index 1fa28c4..6426f6c 100644 --- a/.bonsai/Bonsai.config +++ b/.bonsai/Bonsai.config @@ -1,29 +1,31 @@  - - - - - - - + + + + + + + + + - - + + - - + + - - + + - - + + @@ -35,38 +37,40 @@ - - - - - - - - - - - + + + + + + + + + + + - - + + - - + + - - + + + + - - - - - - + + + + + + \ No newline at end of file diff --git a/.bonsai/NuGet.config b/.bonsai/NuGet.config index aa5beec..bdc4519 100644 --- a/.bonsai/NuGet.config +++ b/.bonsai/NuGet.config @@ -2,4 +2,4 @@ - + \ No newline at end of file From 3ca7597e6d010337ec3f62e0327a8faff83e8fb5 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 8 Jul 2025 17:31:11 +0100 Subject: [PATCH 62/63] Remove unused library path We do not need to inject any packages into the environment as this project does not generate library packages. This makes it easier to debug documentation locally without having to run dummy package builds. --- docs/build.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/build.ps1 b/docs/build.ps1 index 01ad8f5..230b2ea 100644 --- a/docs/build.ps1 +++ b/docs/build.ps1 @@ -9,7 +9,6 @@ Push-Location $PSScriptRoot try { $libPaths = @() $libPaths += Get-ChildItem "..\artifacts\bin\*\release_net4*" -Directory | Select-Object -Expand FullName - $libPaths += "..\artifacts\package\release" ./export-images.ps1 $libPaths dotnet docfx metadata From 9d17a0bcd1ec11f5f5b3508cce25368c0b1eddf1 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 8 Jul 2025 23:30:39 +0100 Subject: [PATCH 63/63] Refactor data definition language section The motivation was rewritten to emphasize the gains of generator, serialization, and data exchange. Style was standardized across all articles and made consistent with JSON Schema docs. --- docs/articles/advanced-usage.md | 17 ++- docs/articles/basic-usage.md | 53 ++------- docs/articles/data-definition.md | 111 ++++++++++++++++++ docs/articles/example-person.md | 28 +++++ docs/articles/toc.yml | 2 +- docs/articles/why-bonsai-sgen.md | 66 ----------- .../simple-serialization-example.bonsai | 48 ++++++++ 7 files changed, 208 insertions(+), 117 deletions(-) create mode 100644 docs/articles/data-definition.md create mode 100644 docs/articles/example-person.md delete mode 100644 docs/articles/why-bonsai-sgen.md create mode 100644 docs/workflows/simple-serialization-example.bonsai diff --git a/docs/articles/advanced-usage.md b/docs/articles/advanced-usage.md index 24e23a9..e0c629c 100644 --- a/docs/articles/advanced-usage.md +++ b/docs/articles/advanced-usage.md @@ -2,9 +2,9 @@ ## Unions -In the previous examples, we have seen how create object properties of a single type. However, in practice, data structures' fields can often be represented by one of several types. We have actually seen a special case of this behavior in the previous nullable example, where a field can be either a value of a given type `T` or `null` (or an Union between type `T` and `null`). +In the previous examples, we have seen how to create object properties of a single type. However, in many real-world applications, data structure fields can be represented by one of several types. We have actually seen a special case of this behavior in the previous nullable example, where a field can be either a value of a given type `T` or `null` (or a "union" between type `T` and `null`). -`json-schema` allows union types using the `oneOf` keyword. For example: +JSON Schema allows union types using the `oneOf` keyword. For example: ```json { @@ -27,11 +27,11 @@ Running `Bonsai.Sgen` on this schema generates the following type signature for public object FooProperty ``` -While `oneOf` is supported, statically typed languages like `C#` require the exact type at compile time. Thus, the property is "up-cast" to `object`, and users must down-cast it to the correct type at runtime. +While `oneOf` is supported, statically typed languages like C# require the exact type at compile time. Thus, the property is "up-cast" to `object`, and users must down-cast it to the correct type at runtime. ## Tagged-Unions -Unions types can be made type-aware by using [`tagged unions`](https://en.wikipedia.org/wiki/Tagged_union) (or `discriminated unions`). The syntax for tagged unions is not part of the `json-schema` specification, but it is supported by the [`OpenAPI` standard](https://swagger.io/docs/specification/v3_0/data-models/inheritance-and-polymorphism/#discriminator), which is a superset of `json-schema`. The key idea behind tagged unions is to add a `discriminator` field to the schema that specifies the property that will be used to determine the type of the object at runtime. +Union types can be made type-aware by using [`tagged unions`](https://en.wikipedia.org/wiki/Tagged_union) (or `discriminated unions`). The syntax for tagged unions is not part of the JSON Schema specification, however it is supported by the [OpenAPI standard](https://swagger.io/docs/specification/v3_0/data-models/inheritance-and-polymorphism/#discriminator), which is a superset of JSON Schema. The key idea behind tagged unions is to add a `discriminator` field to the schema that specifies the property that will be used to determine the type of the object at runtime. For example, a `Pet` object that can be either a `Dog` or a `Cat` can be represented as follows: @@ -53,17 +53,16 @@ For example, a `Pet` object that can be either a `Dog` or a `Cat` can be represe } ``` -In `C#`, `Bonsai.Sgen` will generate a root type `Pet` that will be inherited by the `Dog` and `Cat` types (since in the worst case scenario, the discriminated property must be shared). The `Pet` type will have a `pet_type` property that will be used to downcast to the proper type at runtime. At this point we can open our example in `Bonsai` and see how the `Pet` type is represented in the workflow. +Given this schema, `Bonsai.Sgen` will generate a root type `Pet` that will be specialised by the `Dog` and `Cat` types (since in the worst case scenario, the discriminated property must be shared). The `Pet` type will have a `pet_type` property that will be used to downcast to the proper type at runtime. At this point we can open our example in `Bonsai` and see how the `Pet` type is represented in the workflow. -As you can see below, we still get a `Pet` type. Better than `object` but still not a `Dog` or `Cat` type. Fortunately, `Bonsai.Sgen` will generate an operator that can be used to filter and downcast the `Pet` objects to the correct type at runtime. These are called `Match` operators. `MatchPet` can be used to select the desired target type which will allow us access to the properties of the `Dog` or `Cat` subtypes. Conversely, we can also upcast a `Dog` or `Cat` to a `Pet` by leaving the `MatchPet` operator's `Type` property empty. +As you can see below, we still get a `Pet` type. Better than `object`, but still not a `Dog` or `Cat` type. Fortunately, `Bonsai.Sgen` will generate an operator that can be used to filter and downcast the `Pet` objects to the correct type at runtime. These are called `Match` operators. `MatchPet` can be used to select the desired target type which will allow us access to the properties of the `Dog` or `Cat` subtypes. Conversely, we can also upcast a `Dog` or `Cat` to a `Pet` by leaving the `MatchPet` operator's `Type` property empty. :::workflow ![Discriminated Unions](~/workflows/person-pet-discriminated-union.bonsai) ::: -> [!Important] -> In is strongly recommended to use references with the `oneOf` syntax. Not only does this decision make your `json-schema` significantly smaller, it will also help `Bonsai.Sgen` generate the correct class hierarchy if multiple unions are present in the schema. If you use inline objects, `Bonsai.Sgen` will likely have to generate a new root class for each union, which can lead to a lot of duplicated code and a more complex object hierarchy. - +> [!IMPORTANT] +> It is strongly recommended to use references with the `oneOf` syntax. Not only does this decision make your JSON Schema significantly smaller, it will also help `Bonsai.Sgen` generate the correct class hierarchy if multiple unions are present in the schema. If you use inline objects, `Bonsai.Sgen` will likely have to generate a new root class for each union, which can lead to a lot of duplicated code and a more complex object hierarchy. ## Extending generated code with `partial` classes diff --git a/docs/articles/basic-usage.md b/docs/articles/basic-usage.md index 4e33671..4bf1531 100644 --- a/docs/articles/basic-usage.md +++ b/docs/articles/basic-usage.md @@ -1,45 +1,16 @@ # Basic usage +`Bonsai.Sgen` can be used to generate many different kinds of models with different relationships between types. + > [!TIP] > It is strongly recommended to be familiar with [Bonsai Scripting Extensions](https://bonsai-rx.org/docs/articles/scripting-extensions.html) before using this tool. -## Automatic generation of Bonsai code using Bonsai.Sgen - -We will expand this example later, but for now, let's see how to use `Bonsai.Sgen` to automatically generate Bonsai code for the `Person` object. - -First, define the schema of the object in a JSON file: - -[person.json](~/workflows/person.json) - -```json -{ - "title": "Person", - "type": "object", - "properties": { - "Age": { "type": "integer" }, - "FirstName": { "type": "string" }, - "LastName": { "type": "string" }, - "DOB": { "type": "string", "format": "date-time" } - } -} -``` - -Next, run the `Bonsai.Sgen` tool to generate the Bonsai code: - -```shell -dotnet bonsai.sgen --schema person.json --output Extensions/PersonSgen.cs -``` - -Finally, use the generated code in your Bonsai workflow: - -:::workflow -![Person as BonsaiSgen](~/workflows/person-example-bonsai-sgen.bonsai) -::: +## Single object -The `Bonsai.Sgen` approach is concise and less error-prone, allowing you to focus on the data structure itself rather than boilerplate code. Additionally, the tool automatically generates, type-aware, [serialization and deserialization operators](#serialization-and-deserialization), which are useful when working with external data sources. +In this first example we recall how to model the single record type `Person` defined in the [Data Definition](data-definition.md) section. -By treating the `json-schema` as the "source of truth," you can generate multiple representations of the object in different languages, ensuring interoperability. This is particularly useful in multi-language environments (e.g., running experiments in Bonsai and analysis in Python) and when sharing data structures across projects. +[!INCLUDE [](example-person.md)] ## Multiple objects @@ -79,7 +50,7 @@ The previous example demonstrates modeling a single record. In practice, project } ``` -```shell +```powershell dotnet bonsai.sgen --schema person-and-dog.json --output Extensions/PersonAndDogSgen.cs --namespace PersonAndDog ``` @@ -104,7 +75,7 @@ The real power of `Bonsai.Sgen` comes when dealing with more complex data struct ## Enums -`Bonsai.Sgen` also supports the generation of enums using the `enum` type in the `json-schema`: +`Bonsai.Sgen` also supports the generation of enums using the `enum` type in the JSON Schema: We can replace the `Pet` object in the previous example with an [`enum`](https://json-schema.org/understanding-json-schema/reference/enum): @@ -149,7 +120,7 @@ In Bonsai, they can be manipulated as [`Enum`](https://learn.microsoft.com/en-us ## Lists -`Bonsai.Sgen` also supports the generation of lists using the `array` type in the `json-schema`: +`Bonsai.Sgen` also supports the generation of lists using the `array` type in the JSON Schema: ```json "pets": { @@ -158,7 +129,7 @@ In Bonsai, they can be manipulated as [`Enum`](https://learn.microsoft.com/en-us } ``` -`json-schema` `array` will be rendered as [`List`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1?view=net-9.0) in the generated code and can be manipulated (and created) as such. +JSON Schema `array` will be rendered as [`List`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1?view=net-9.0) in the generated code and can be manipulated (and created) as such. :::workflow ![Person and Pets](~/workflows/person-and-pets-enum.bonsai) @@ -166,7 +137,7 @@ In Bonsai, they can be manipulated as [`Enum`](https://learn.microsoft.com/en-us ## Nullable types -`json-schema` supports the `null` type, which can be used to represent nullable types. The standard is a bit loose in this regard, but `Bonsai.Sgen` will generate a nullable-T if the json-schema represents it using the `oneOf` keyword: +JSON Schema supports the `null` type, which can be used to represent nullable types. The standard is a bit loose in this regard, but `Bonsai.Sgen` will generate a nullable-T if the JSON Schema represents it using the `oneOf` keyword: ```json "pet": { @@ -187,7 +158,7 @@ For reference types, the generated code will not render a nullable type since re ## Required fields -`json-schema` supports the [`required`](https://json-schema.org/learn/getting-started-step-by-step#define-required-properties) keyword to specify which fields are required. By default, all fields are optional. This can be useful to enforce the presence of certain fields in the object at deserialization time. However, `Bonsai.Sgen` will not generate any code to enforce this requirement during object construction, only at deserialization. It is up to the user to ensure that the object is correctly populated before using it. +JSON Schema supports the [`required`](https://json-schema.org/learn/getting-started-step-by-step#define-required-properties) keyword to specify which fields are required. By default, all fields are optional. This can be useful to enforce the presence of certain fields in the object at deserialization time. However, `Bonsai.Sgen` will not generate any code to enforce this requirement during object construction, only at deserialization. It is up to the user to ensure that the object is correctly populated before using it. > [!Note] > Some confusion may arise about the distinction between `null` and `required`. This is all the more confusing since different languages and libraries may refer to these concepts in different ways. For the sake of this tool, the following definitions are used: @@ -200,7 +171,7 @@ For reference types, the generated code will not render a nullable type since re ## Serialization and Deserialization -One of the biggest perks of using json-schema to represent our objects is the guarantee that all records are (de)serializable. This means that we can go from a text-based format (great for specification and logging) to a `C#` type seamlessly, and vice-versa. `Bonsai.Sgen` will optionally generate (de)serialization operators for all objects in the schema if the `--serializer` property is not `None`. Currently, two formats are supported out of the box: `Json` (via [`NewtonsoftJson`](https://github.com/JamesNK/Newtonsoft.Json)) and `yaml` (via [`YamlDotNet`](https://github.com/aaubry/YamlDotNet)). +One of the biggest perks of using JSON Schema to represent our objects is the guarantee that all records are (de)serializable. This means that we can go from a text-based format (great for specification and logging) to a C# type seamlessly, and vice-versa. `Bonsai.Sgen` will optionally generate (de)serialization operators for all objects in the schema if the `--serializer` property is not `None`. Currently, two formats are supported out of the box: `Json` (via [`NewtonsoftJson`](https://github.com/JamesNK/Newtonsoft.Json)) and `yaml` (via [`YamlDotNet`](https://github.com/aaubry/YamlDotNet)). The two operations are afforded via the `SerializeToYaml` (or `SerializeToJson`) and `DeserializeFromYaml` (or `DeserializeFromJson`) operators, respectively. diff --git a/docs/articles/data-definition.md b/docs/articles/data-definition.md new file mode 100644 index 0000000..db55795 --- /dev/null +++ b/docs/articles/data-definition.md @@ -0,0 +1,111 @@ +# Data Definition + +`Bonsai.Sgen` addresses the problem of defining and implementing custom data types in the Bonsai programming language. Let's explore this with a simple example. + +## Introduction + +Suppose we want to create a new record-like object type that represents a `Person`: + +| Field Name | Type | Description | +|------------|----------|------------------------------| +| age | int | The age of a person | +| first_name | string | The first name of the person | +| last_name | string | The last name of the person | +| dob | datetime | Date of birth | + +Since there is currently no special syntax to declare object types directly in Bonsai, we need to leverage indirect approaches to define our new record type. We start by exploring the previously available options below, along with their limitations, and finally introduce a third, more powerful, alternative. + +## Data Object Initializers + +One powerful feature of [`ExpressionTransform`](xref:Bonsai.Scripting.Expressions.ExpressionTransform) operators is support for writing [Data Object Initializers](xref:Bonsai.Scripting.Expressions.ExpressionTransform#data-object-initializers): + +:::workflow +![Person as DynamicClass](~/workflows/person-example-dynamic-class.bonsai) +::: + +**ExpressionTransform:** +``` +new( + Item1 as Age, + Item2 as FirstName, + Item3 as LastName, + Item4 as DOB +) +``` + +This approach works, but is somewhat brittle, as the `Person` record exists only as an anonymous type in the current workflow compiler context. This limitation prevents the creation of named references to the type, required for instance to create [Subject Sources](https://bonsai-rx.org/docs/articles/subjects.html#source-subjects). It also requires scripting to be used anywhere we need to create new objects. + +## Custom Scripting Extension + +A more powerful alternative is to leverage C# directly to define our type class, by using custom [Scripting Extensions](https://bonsai-rx.org/docs/articles/scripting-extensions.html): + +```csharp +public class Person +{ + public int Age; + public string FirstName; + public string LastName; + public DateTime DOB; +} +``` + +This approach is much more flexible, as it allows composing our record using arbitrary C# types. It also supports nesting and even defining custom operators and functions on the new type. However, even for simple types we will need to write additional code to allow this type to be directly created and manipulated inside a Bonsai workflow: + +```csharp +using Bonsai; +using System; +using System.Reactive.Linq; + +public class CreatePerson : Source +{ + public int Age { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public DateTime DOB { get; set; } + + public override IObservable Generate() + { + return Observable.Return(new Person + { + Age = Age, + FirstName = FirstName, + LastName = LastName, + DOB = DOB + }); + } +} +``` + +Essentially we are augmenting the class with a source operator that creates a new instance of the record type with the parameters specified in the class properties. Because the record class is now a regular Bonsai operator, it will show up in the editor toolbox and can be placed and configured in the workflow as usual. + +While this might be enough to work around the need for the single odd type in our project, it doesn't scale well to model other common requirements for domain-specific record types, such as support for type hierarchies, serialization, or polymorphism, all of which would require additional boilerplate code on top of our simple type. + +As projects increase in complexity, writing such boilerplate code can quickly become cumbersome and error prone. + +## JSON Schema + +`Bonsai.Sgen` provides a new, and much more flexible, solution to this problem by leveraging [JSON Schema](https://json-schema.org/) directly as a data definition language in Bonsai. + +### How to Use + +[!INCLUDE [](example-person.md)] + +### Advantages + +Although initially this form may seem less direct and more complicated than even the C# type definition, there are a number of advantages immediately falling out from using JSON Schema as our data definition language: + +1. Custom Bonsai operator code can be automatically generated from the JSON Schema. +2. Data objects backed by JSON Schemas can be used to read and write JSON files with validation guarantees. +3. Both JSON files and JSON Schemas are interoperable with any other language. + +With `Bonsai.Sgen` you can focus on the specification of the data structure itself, rather than on the details of boilerplate code. Furthermore, you don't even need to write the schema by hand directly in JSON, since you can use any language supporting JSON Schemas. For example, you can easily write a full data model in Python and use those classes directly to generate a JSON Schema for Bonsai. + +### Saving and Loading + +`Bonsai.Sgen` automatically generates [serialization and deserialization operators](basic-usage.md#serialization-and-deserialization): + +:::workflow +![(de)serialization](~/workflows/simple-serialization-example.bonsai) +::: + +This means you immediately gain the ability to use data objects as configuration files you load into your workflow, or as data records that you save with your experiment. Because all data will be backed by a schema, all these records can be immediately accessed by Python or any other language, saving you even more time setting up data processing pipelines. \ No newline at end of file diff --git a/docs/articles/example-person.md b/docs/articles/example-person.md new file mode 100644 index 0000000..a4bf1b3 --- /dev/null +++ b/docs/articles/example-person.md @@ -0,0 +1,28 @@ +First, define the JSON Schema for our `Person` data type: + +[person.json](~/workflows/person.json) + +```json +{ + "title": "Person", + "type": "object", + "properties": { + "Age": { "type": "integer" }, + "FirstName": { "type": "string" }, + "LastName": { "type": "string" }, + "DOB": { "type": "string", "format": "date-time" } + } +} +``` + +Generate custom Bonsai extension code using `Bonsai.Sgen`: + +```powershell +dotnet bonsai.sgen --schema person.json --output Extensions/PersonSgen.Generated.cs +``` + +Use the generated operators directly in your Bonsai workflow: + +:::workflow +![Person as BonsaiSgen](~/workflows/person-example-bonsai-sgen.bonsai) +::: \ No newline at end of file diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml index 98b79c8..222f8a7 100644 --- a/docs/articles/toc.yml +++ b/docs/articles/toc.yml @@ -1,5 +1,5 @@ - name: Getting Started href: ../index.md -- href: why-bonsai-sgen.md +- href: data-definition.md - href: basic-usage.md - href: advanced-usage.md \ No newline at end of file diff --git a/docs/articles/why-bonsai-sgen.md b/docs/articles/why-bonsai-sgen.md deleted file mode 100644 index 116702c..0000000 --- a/docs/articles/why-bonsai-sgen.md +++ /dev/null @@ -1,66 +0,0 @@ -# Why use Bonsai.Sgen? - -## Data schemas - -`Bonsai.Sgen` addresses the challenge of writing boilerplate code to model domain-specific data in Bonsai. Let's explore this with a simple example. - -Suppose we have a record-like object that represents a `Person`: - -| Field Name | Type | Description | -|------------|----------|------------------------------| -| age | int | The age of a person | -| first_name | string | The first name of the person | -| last_name | string | The last name of the person | -| dob | datetime | Date of birth | - -To represent this object in Bonsai, we have a few options: - -1. Using an [`ExpressionTransform`](xref:Bonsai.Scripting.Expressions.ExpressionTransform) with a [Data Object Initializer](xref:Bonsai.Scripting.Expressions.ExpressionTransform#data-object-initializers): - - :::workflow - ![Person as DynamicClass](~/workflows/person-example-dynamic-class.bonsai) - ::: - - This approach is brittle because the record representation exists only at compile-time and not as a "first-class citizen". For instance, this limitation prevents the creation of [Subject Sources](https://bonsai-rx.org/docs/articles/subjects.html#source-subjects) from the type. - -2. Modeling the object as a C# class using [Scripting Extensions](https://bonsai-rx.org/docs/articles/scripting-extensions.html): - - ```Csharp - public class Person - { - public int Age; - public string FirstName; - public string LastName; - public DateTime DOB; - } - ``` - - While more robust, this approach requires additional boilerplate code to enable object creation in Bonsai: - - ```Csharp - using Bonsai; - using System; - using System.Reactive.Linq; - - public class CreatePerson : Source - { - - public int Age { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - public DateTime DOB { get; set; } - - public override IObservable Generate() - { - return Observable.Return(new Person - { - Age = Age, - FirstName = FirstName, - LastName = LastName, - DOB = DOB - }); - } - } - ``` - -As you can see, neither approach scales well for large projects. This is where `Bonsai.Sgen` comes in. \ No newline at end of file diff --git a/docs/workflows/simple-serialization-example.bonsai b/docs/workflows/simple-serialization-example.bonsai new file mode 100644 index 0000000..d771fc1 --- /dev/null +++ b/docs/workflows/simple-serialization-example.bonsai @@ -0,0 +1,48 @@ + + + + + + Serialization + + + + + + + + + + + false + false + + + + Deserialization + + + + + + + + + + Age + + + + + + + + + + + + \ No newline at end of file