From eff6946eec924324c7e8a2c90a7e33eb34285baa Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Wed, 6 Nov 2024 15:45:26 -0800 Subject: [PATCH 01/26] Ensure generated attribute honors nullable & required properties --- Bonsai.Sgen/CSharpClassTemplate.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bonsai.Sgen/CSharpClassTemplate.cs b/Bonsai.Sgen/CSharpClassTemplate.cs index 001596f..38a9b30 100644 --- a/Bonsai.Sgen/CSharpClassTemplate.cs +++ b/Bonsai.Sgen/CSharpClassTemplate.cs @@ -121,7 +121,7 @@ public override void BuildType(CodeTypeDeclaration type) nameof(JsonPropertyAttribute.Required), new CodeFieldReferenceExpression( new CodeTypeReferenceExpression(typeof(Required)), - nameof(Required.Always)))); + property.IsNullable ? nameof(Required.AllowNull) : nameof(Required.Always)))); } propertyDeclaration.CustomAttributes.Add(jsonProperty); } From c001718d5d6ccbd49d173125cd9f5f7c2dd400b4 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:53:43 +0000 Subject: [PATCH 02/26] Add bonsai environment --- .bonsai/NuGet.config | 8 ++++++++ .bonsai/setup.cmd | 1 + .bonsai/setup.ps1 | 21 +++++++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 .bonsai/NuGet.config create mode 100644 .bonsai/setup.cmd create mode 100644 .bonsai/setup.ps1 diff --git a/.bonsai/NuGet.config b/.bonsai/NuGet.config new file mode 100644 index 0000000..fa620e4 --- /dev/null +++ b/.bonsai/NuGet.config @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/.bonsai/setup.cmd b/.bonsai/setup.cmd new file mode 100644 index 0000000..3c53f3a --- /dev/null +++ b/.bonsai/setup.cmd @@ -0,0 +1 @@ +powershell -ExecutionPolicy Bypass -File .\setup.ps1 \ No newline at end of file diff --git a/.bonsai/setup.ps1 b/.bonsai/setup.ps1 new file mode 100644 index 0000000..01cfba6 --- /dev/null +++ b/.bonsai/setup.ps1 @@ -0,0 +1,21 @@ +Push-Location $PSScriptRoot +if (!(Test-Path "./Bonsai.exe")) { + $release = "https://github.com/bonsai-rx/bonsai/releases/latest/download/Bonsai.zip" + $configPath = "./Bonsai.config" + if (Test-Path $configPath) { + [xml]$config = Get-Content $configPath + $bootstrapper = $config.PackageConfiguration.Packages.Package.where{$_.id -eq 'Bonsai'} + if ($bootstrapper) { + $version = $bootstrapper.version + $release = "https://github.com/bonsai-rx/bonsai/releases/download/$version/Bonsai.zip" + } + } + Invoke-WebRequest $release -OutFile "temp.zip" + Move-Item -Path "NuGet.config" "temp.config" -ErrorAction SilentlyContinue + Expand-Archive "temp.zip" -DestinationPath "." -Force + Move-Item -Path "temp.config" "NuGet.config" -Force -ErrorAction SilentlyContinue + Remove-Item -Path "temp.zip" + Remove-Item -Path "Bonsai32.exe" +} +& .\Bonsai.exe --no-editor +Pop-Location \ No newline at end of file From 39727d420473897d9219541fa76d0850e7bd9519 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 03/26] 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 e5a12cb2d1c67b8c63fec3428f399c42e8a037ec 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 04/26] 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 7021f473cde1f9b6efbceae178353b0a682e0473 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 05/26] 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 9784f082b3298b05d7aefa0e1893e6e1a0d79d06 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 06/26] 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 070f8fc10125556d4f550aa63c8716e71c1b9dac 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 07/26] 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 56f11bb28153f1fc5ecb17a0a9513f38612f3fc0 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:45 +0000 Subject: [PATCH 08/26] Add local dotnet tool manifest and register Bonsai.Sgen --- .config/dotnet-tools.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .config/dotnet-tools.json diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..8b3ab7e --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "bonsai.sgen": { + "version": "0.4.0", + "commands": [ + "bonsai.sgen" + ], + "rollForward": false + } + } +} \ No newline at end of file From 44cf8853cf4251fb298baec0adf2ef349bd985eb 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 09/26] 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 9fb6072418974805a9a385422463764848736b01 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 10/26] 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 6f3cf6b49b643452b3fa67f9a90bb99fce31cf04 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 11/26] 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 f96b9ac4ab9e5fa2df69f1f0c288d11aa51c735a 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:50 +0000 Subject: [PATCH 12/26] Ignore environment temp files --- .bonsai/.gitignore | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .bonsai/.gitignore diff --git a/.bonsai/.gitignore b/.bonsai/.gitignore new file mode 100644 index 0000000..cceac95 --- /dev/null +++ b/.bonsai/.gitignore @@ -0,0 +1,13 @@ +# Bonsai scripting files +.vs +.vscode +bin +obj +Packages +Bonsai.exe.WebView2 +*.bin +*.avi +*.dll +*.exe +*.exe.settings +*.bonsai.layout \ No newline at end of file From 7331a2ca6e3efe9d9f502179658b844f68012d4d 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 13/26] Add environment package requirements --- .bonsai/Bonsai.config | 69 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .bonsai/Bonsai.config diff --git a/.bonsai/Bonsai.config b/.bonsai/Bonsai.config new file mode 100644 index 0000000..3db0977 --- /dev/null +++ b/.bonsai/Bonsai.config @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 859772755922986a1faf097e3b8b7008d58e9c52 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 14/26] 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 adea528c859a5e764c1c40ceb771334a49acefb9 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 15/26] 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 64a0540c5bb6729ac917cf88eafdd1901529523a 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 16/26] 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 a4ad4ffde1569116e687196a91fd435b9f0379bc 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 17/26] 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 457343e54cad9b396cac5c68d4270ee8a5faf7c2 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 18/26] 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 d3207dd463ce465e3bc57e93491b190d5a20ff93 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 19/26] 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 99d973f83fc09d0168c190de8f7f15ee0ea9edcb 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 20/26] 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 2e37736be6d54e6798e681ac2932e006c81dab4d 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 21/26] 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 7561403ff5451f3db67f4452d6cc7b45352c51a7 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 22/26] 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 e1fab344d3d64719e64cd176d3de4c906e6e58be 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 23/26] 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 e98f70c1003a0456b951aa53b6a5cefe296595fe 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 24/26] 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 5f0262e0edc33719c5df95f0d1a5c5a74b1ab7b9 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 25/26] 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 fece6dc6a29f81ef6028c44c1585b762f784ce8c 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 26/26] 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