diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 881c709..2f005d9 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -1,25 +1,65 @@
-name: .NET
+name: Build & Release (.NET)
on:
push:
branches: [ main ]
+ tags:
+ - "v*.*.*" # Release tags like v1.2.3
pull_request:
branches: [ main ]
jobs:
build:
-
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - name: Setup .NET
- uses: actions/setup-dotnet@v1
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET 10 (preview)
+ uses: actions/setup-dotnet@v4
with:
- dotnet-version: '5.0.x'
- - name: Restore dependencies
+ dotnet-version: "10.0.x"
+
+ - name: Restore
run: dotnet restore
+
- name: Build
- run: dotnet build --no-restore
+ run: dotnet build --configuration Release --no-restore
+
- name: Test
- run: dotnet test --no-build --verbosity normal
+ run: dotnet test --configuration Release --no-build --verbosity normal
+
+ release:
+ needs: build
+ if: startsWith(github.ref, 'refs/tags/v')
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET 10 (preview)
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: "10.0.x"
+
+ - name: Extract version from tag
+ id: version
+ run: |
+ VERSION="${GITHUB_REF#refs/tags/v}"
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+
+ - name: Pack NuGet package
+ run: dotnet pack \
+ --configuration Release \
+ -p:PackageVersion=${{ steps.version.outputs.version }} \
+ -o ./artifacts
+
+ - name: Publish GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ files: artifacts/*.nupkg
+ env:
+ GITHUB_TOKEN: ${{ secrets.FSHARP_DATA_MUTATOR }}
+
diff --git a/README.md b/README.md
index 17b22fc..3cd87dc 100644
--- a/README.md
+++ b/README.md
@@ -3,14 +3,14 @@
Enables to create copies (similar to lenses) to generated FSharp.Data types (json only for now),
-The library now depends both on FSharp.Data and Newtonsoft.Json as dependencies, but can be improved.
+The library now depends both on FSharp.Data and System.Text.Json as dependencies, but can be improved.
A [medium article](https://jkone27-3876.medium.com/fsharp-data-mutator-66550bb6a2cc) about it.
## Usage
```fsharp
-#r "nuget:FSharp.Data.Mutator,0.1.0-beta"
+#r "nuget:FSharp.Data.Mutator,0.2.0"
open FSharp.Data
open FSharp.Data.Mutator
@@ -62,3 +62,12 @@ val it : JsonProvider<...>.Root =
```
Have fun!
+
+## Mantainers
+
+to create a release, just create a new tag with the version number and push it to the repository, the release will be automatically created and published on nuget.org.
+
+```cli
+git tag v0.2.0
+git push origin v0.2.0
+```
diff --git a/dotnet-tools.json b/dotnet-tools.json
new file mode 100644
index 0000000..0ee28ae
--- /dev/null
+++ b/dotnet-tools.json
@@ -0,0 +1,13 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-outdated-tool": {
+ "version": "4.7.0",
+ "commands": [
+ "dotnet-outdated"
+ ],
+ "rollForward": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/FSharp.Data.Mutator.fsproj b/src/FSharp.Data.Mutator.fsproj
index ecc18a3..5f8841d 100644
--- a/src/FSharp.Data.Mutator.fsproj
+++ b/src/FSharp.Data.Mutator.fsproj
@@ -1,7 +1,7 @@
- netstandard2.1;net5.0
+ netstandard2.1;net8.0;net10.0
true
3390;$(WarnOn)
jkone27
@@ -29,8 +29,8 @@
-
-
+
+
diff --git a/src/JsonMutator.fs b/src/JsonMutator.fs
index 368dc35..757adf0 100644
--- a/src/JsonMutator.fs
+++ b/src/JsonMutator.fs
@@ -1,138 +1,254 @@
namespace FSharp.Data.Mutator
-open Newtonsoft.Json.Linq
-open Microsoft.FSharp.Quotations
-open FSharp.Data
+open System
+open System.Text.Json
+open System.Text.Json.Nodes
open System.Text.RegularExpressions
open System.Linq.Expressions
open Microsoft.FSharp.Linq.RuntimeHelpers
+open Microsoft.FSharp.Quotations
+open Microsoft.FSharp.Reflection
open System.Collections.Generic
-open System
-open FSharp.Data.Runtime.BaseTypes
open System.Runtime.CompilerServices
-
+open FSharp.Data
+open FSharp.Data.Runtime.BaseTypes
[]
module JsonMutator =
-
- type JToken with
+
+ // -----------------------------
+ // JsonNode <-> FSharp.Data.JsonValue
+ // -----------------------------
+
+ type JsonNode with
member this.JsonValue() =
- this.ToString() |> JsonValue.Parse
-
- type JToken with
- member this.With (mutatorFunc: JToken -> 'a) =
- this |> fun y -> mutatorFunc(y) |> ignore; y
-
- type JsonValue with
- member this.JToken() =
+ this.ToJsonString() |> JsonValue.Parse
+
+ type JsonValue with
+ member this.JsonNode() =
this.ToString()
- |> JToken.Parse
-
- type JsonValue with
- member this.With (mutatorFunc: JToken -> 'a) =
- this.JToken().With(mutatorFunc).JsonValue()
+ |> JsonNode.Parse
+
+ // -----------------------------
+ // "With" helpers (mutation)
+ // -----------------------------
+
+ type JsonNode with
+ member this.With (mutatorFunc: JsonNode -> 'a) =
+ this |> fun n -> mutatorFunc n |> ignore; n
+
+ type JsonValue with
+ member this.With (mutatorFunc: JsonNode -> 'a) =
+ this.JsonNode().With(mutatorFunc).JsonValue()
+
+ // -----------------------------
+ // Expression helpers
+ // -----------------------------
let getLR expr =
- let rec getLeftRight (expr : Expression) r =
+ let rec getLeftRight (expr: Expression) r =
match expr with
- | :? MethodCallExpression as mc when mc.Arguments.Count > 0 ->
+ | :? MethodCallExpression as mc when mc.Arguments.Count > 0 ->
getLeftRight (mc.Arguments.[0]) r
- | :? LambdaExpression as l ->
+ | :? LambdaExpression as l ->
getLeftRight l.Body r
- | :? BinaryExpression as be ->
- getLeftRight be.Right [be.Left; be.Right]
- |_ -> r
+ | :? BinaryExpression as be ->
+ getLeftRight be.Right [ be.Left; be.Right ]
+ | _ -> r
getLeftRight expr []
-
- let inline UpdateLeaf<'a when 'a :> IJsonDocument>(updateAction: Expr<('a -> bool)>) (jsonValue: JsonValue) =
-
- let expression =
- updateAction
- |> LeafExpressionConverter.QuotationToExpression
-
- let binomialResult =
- expression
- |> getLR
-
- // todo if not primitive, turn to JToken
- let jtoken =
- match binomialResult with
- [l;r] ->
- let t = r.Type.Name.ToLower()
- match t with
- | "jsonvalue" ->
+
+ // -----------------------------
+ // JsonPath-like selection for JsonNode
+ // -----------------------------
+
+ let private trySelectPath (root: JsonNode) (path: string) : JsonNode option =
+ if String.IsNullOrWhiteSpace path then
+ Some root
+ else
+ let parts = path.Split('.', StringSplitOptions.RemoveEmptyEntries)
+
+ let parsePart (p: string) =
+ let idxStart = p.IndexOf('[')
+ if idxStart >= 0 then
+ let idxEnd = p.IndexOf(']', idxStart + 1)
+ let name = p.Substring(0, idxStart)
+ let idx = p.Substring(idxStart + 1, idxEnd - idxStart - 1) |> int
+ name, Some idx
+ else
+ p, None
+
+ let rec loop (node: JsonNode) i =
+ if i = parts.Length then
+ Some node
+ else
+ let name, idxOpt = parsePart parts.[i]
+
+ match node : JsonNode with
+ | :? JsonObject as o ->
+ let mutable outNode: JsonNode = null
+ match o.TryGetPropertyValue(name, &outNode) with
+ | true ->
+ match idxOpt, outNode with
+ | Some idx, (:? JsonArray as arr) when idx >= 0 && idx < arr.Count ->
+ loop arr.[idx] (i + 1)
+ | None, _ ->
+ loop outNode (i + 1)
+ | _ ->
+ None
+ | false ->
+ None
+
+ | :? JsonArray as arr ->
+ match idxOpt with
+ | Some idx when idx >= 0 && idx < arr.Count ->
+ loop arr.[idx] (i + 1)
+ | _ ->
+ None
+
+ | _ ->
+ None
+
+ loop root 0
+
+ // -----------------------------
+ // Option detection and conversion
+ // -----------------------------
+
+ let private isOptionType (t: Type) =
+ t.IsGenericType && t.GetGenericTypeDefinition() = typedefof