diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..c9c342f7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,47 @@ +version: 2 +updates: +- package-ecosystem: nuget + labels: [dependency] + groups: + microsoft: + patterns: [Microsoft.*, System.*] + xunit: + patterns: [xunit.*] + directory: / + schedule: + interval: monthly + target-branch: main + +- package-ecosystem: nuget + labels: [dependency, support] + groups: + microsoft: + patterns: [Microsoft.*, System.*] + xunit: + patterns: [xunit.*] + directory: / + schedule: + interval: monthly + target-branch: support/1.x + ignore: + - dependency-name: Newtonsoft.Json + +- package-ecosystem: github-actions + labels: [dependency] + groups: + actions: + patterns: [actions/*] + directory: / + schedule: + interval: monthly + target-branch: main + +- package-ecosystem: github-actions + labels: [dependency, support] + groups: + actions: + patterns: [actions/*] + directory: / + schedule: + interval: monthly + target-branch: support/1.x diff --git a/.github/mergify.yml b/.github/mergify.yml new file mode 100644 index 00000000..9e668864 --- /dev/null +++ b/.github/mergify.yml @@ -0,0 +1,26 @@ +pull_request_rules: + - name: Automatic approve on dependabot PR + conditions: + - author~=^dependabot(|-preview)\[bot\]$ + - base=main + actions: + review: + type: APPROVE + + - name: Automatic merge on approval + conditions: + - author~=^dependabot(|-preview)\[bot\]$ + - '#commits-behind=0' # Only merge up to date pull requests + - check-success=build + - check-success=build & run tests + - base=main + actions: + merge: + + - name: Thank contributor + conditions: + - merged + - -author~=^.*\[bot\]$ + actions: + comment: + message: "Thank you for your contribution, @{{author}}!" diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 00000000..5daeec95 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,30 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 90 + +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 30 + +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security + - bug + - planning + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: true + +# Set to true to ignore issues with an assignee (defaults to false) +exemptAssignees: true + +# Label to use when marking an issue as stale +staleLabel: stale + +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. After 30 days from now, it will be closed if no further + activity occurs. Thank you for your contributions. + +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..ced83a5e --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,21 @@ +name: docker + +on: + push: + branches: [main, support/*] + tags: ["*"] + pull_request: + branches: [main, support/*] + +jobs: + docker: + name: build & run tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: docker build + run: docker build -t json-ld.net . + - name: docker test + run: docker run --rm json-ld.net dotnet test diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 00000000..2507c88a --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,133 @@ +name: dotnet + +on: + push: + branches: [main, support/*] + tags: ["*"] + pull_request: + branches: [main, support/*] + +jobs: + build: + runs-on: ubuntu-latest + + outputs: + fullSemVer: ${{ steps.gitversion.outputs.fullSemVer }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: gittools/actions/gitversion/setup@v3.1.11 + with: + versionSpec: 5.x + + - id: gitversion + uses: gittools/actions/gitversion/execute@v3.1.11 + + - uses: actions/setup-dotnet@v4.3.1 + with: + dotnet-version: 6.0.x + + - uses: actions/cache@v4 + env: + NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: ${{ runner.os }}-nuget- + + - run: dotnet restore + + - run: dotnet build --configuration Release --no-restore + + - run: | + dotnet test \ + --configuration Release \ + --no-build \ + --no-restore \ + -p:CollectCoverage=true \ + -p:CoverletOutputFormat=opencover \ + -p:Exclude="[JsonLD.Test*]*" + + - name: Codecov + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + run: bash <(curl -s https://codecov.io/bash) + + - run: | + dotnet pack \ + --include-source \ + --configuration Release \ + --no-build \ + --no-restore \ + -p:PackageVersion="${{ steps.gitversion.outputs.fullSemVer }}" \ + src/json-ld.net/json-ld.net.csproj \ + --output ${{ github.workspace }}/nugets/ + + - uses: actions/upload-artifact@v4 + with: + name: nugets + path: nugets + + nuget-push-dev: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + needs: build + + steps: + - name: download artifact + uses: actions/download-artifact@v4 + with: + name: nugets + + - name: setup dotnet + uses: actions/setup-dotnet@v4.3.1 + with: + dotnet-version: 6.0.x + source-url: https://nuget.pkg.github.com/linked-data-dotnet/index.json + env: + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: nuget push + run: dotnet nuget push "*.nupkg" --skip-duplicate --api-key ${{ secrets.GITHUB_TOKEN }} + + nuget-push-prod: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + needs: build + + steps: + - uses: actions/download-artifact@v4 + with: + name: nugets + + - uses: actions/setup-dotnet@v4.3.1 + with: + dotnet-version: 6.0.x + source-url: https://api.nuget.org/v3/index.json + env: + NUGET_AUTH_TOKEN: ${{ secrets.NUGET_API_KEY }} + + - name: nuget push + run: dotnet nuget push "*.nupkg" --skip-duplicate --api-key ${{ secrets.NUGET_API_KEY }} + + release-artifacts: + runs-on: ubuntu-latest + needs: build + if: startsWith(github.ref, 'refs/tags/') + + steps: + - uses: actions/download-artifact@v4 + with: + name: nugets + + - name: Upload to stable release + uses: svenstaro/upload-release-action@2.9.0 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: nugets + asset_name: json-ld.net + tag: ${{ github.ref }} + overwrite: true diff --git a/.gitignore b/.gitignore index 764c8aa4..3f27ed22 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ build/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* +*opencover.xml *_i.c *_p.c @@ -166,4 +167,7 @@ $RECYCLE.BIN/ .DS_Store # NuGet package store -packages/ \ No newline at end of file +packages/ + +# Jetbrains Rider things. +.idea/** \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..5629666f --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @asbjornu @goofballlogic @tpluscode diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 00000000..eca789cf --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..5fcb9c66 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +# If you want to build and test under Linux using a docker container, here's how: +# +#> docker build -t json-ld.net . +#> docker run --rm json-ld.net dotnet test -v normal + +# .NET Core 6.0 on Ubuntu 20.04 LTS +FROM mcr.microsoft.com/dotnet/sdk:6.0-focal + +WORKDIR /App + +# First we ONLY copy sln and csproj files so that we don't have to re-cache +# dotnet restore every time a .cs file changes +COPY src/json-ld.net/json-ld.net.csproj src/json-ld.net/json-ld.net.csproj +COPY test/json-ld.net.tests/json-ld.net.tests.csproj test/json-ld.net.tests/json-ld.net.tests.csproj +COPY JsonLD.sln JsonLD.sln +RUN dotnet restore + +# Then we copy everything and run dotnet build +COPY . . +RUN dotnet build diff --git a/JsonLD.sln b/JsonLD.sln index 05284e85..cdef61d5 100644 --- a/JsonLD.sln +++ b/JsonLD.sln @@ -1,11 +1,20 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +# Visual Studio 15 +VisualStudioVersion = 15.0.28010.2026 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "json-ld.net", "src\json-ld.net\json-ld.net.xproj", "{E1AB2A29-D1E4-45A1-9076-8255916F5693}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "json-ld.net", "src\json-ld.net\json-ld.net.csproj", "{E1AB2A29-D1E4-45A1-9076-8255916F5693}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "json-ld.net.tests", "test\json-ld.net.tests\json-ld.net.tests.xproj", "{05CBE0E2-FBD2-40D1-BD9A-D30BD7ACF219}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "json-ld.net.tests", "test\json-ld.net.tests\json-ld.net.tests.csproj", "{05CBE0E2-FBD2-40D1-BD9A-D30BD7ACF219}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8D83EC18-10BB-4C6E-A34A-AC183AD6D814}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8010CF28-BCB3-4DC2-901F-3118B2AAD142}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "root", "root", "{4B8ED350-355A-4D30-9F63-B13FBFDBD9E8}" +ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore +EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -25,4 +34,11 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F10834B6-ACA3-4C86-892B-368D0B68ED83} + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {E1AB2A29-D1E4-45A1-9076-8255916F5693} = {8D83EC18-10BB-4C6E-A34A-AC183AD6D814} + {05CBE0E2-FBD2-40D1-BD9A-D30BD7ACF219} = {8010CF28-BCB3-4DC2-901F-3118B2AAD142} + EndGlobalSection EndGlobal diff --git a/README.md b/README.md index a7b214db..05a63fb2 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,712 @@ -json-ld.net -========== +# json-ld.net -A JSON-LD processor for .NET. +[![NuGet][nuget-badge]][nuget] +[![Build Status][gha-badge]][build] +[![codecov][codecov-badge]][codecov] -v1.0.5 is available on NuGet.org at https://www.nuget.org/packages/json-ld.net/ +## Introduction +This library is an implementation of the JSON-LD specification in C#. -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +JSON, as specified in RFC7159, is a simple language for representing objects on +the Web. Linked Data is a way of describing content across different documents +or Web sites. Web resources are described using IRIs, and typically are +dereferencable entities that may be used to find more information, creating a +"Web of Knowledge". JSON-LD is intended to be a simple publishing method for +expressing not only Linked Data in JSON, but for adding semantics to existing +JSON. -#### Supported frameworks +JSON-LD is designed as a light-weight syntax that can be used to express Linked +Data. It is primarily intended to be a way to express Linked Data in JavaScript +and other Web-based programming environments. It is also useful when building +interoperable Web Services and when storing Linked Data in JSON-based document +storage engines. It is practical and designed to be as simple as possible, +utilizing the large number of JSON parsers and existing code that is in use +today. It is designed to be able to express key-value pairs, RDF data, RDFa +data, Microformats data, and Microdata. That is, it supports every major +Web-based structured data model in use today. -* NET 4.0 -* NETStandard 1.3 -* Portable NET 4.5, Windows 8 +The syntax does not require many applications to change their JSON, but easily +add meaning by adding context in a way that is either in-band or out-of-band. +The syntax is designed to not disturb already deployed systems running on JSON, +but provide a smooth migration path from plain JSON to semantically enhanced +JSON. Finally, the format is intended to be fast to parse, fast to generate, +stream-based and document-based processing compatible, and require a very small +memory footprint in order to operate. -Contributing -===== +You can read more about JSON-LD on the [JSON-LD website][jsonld]. -Pull requests for json-ld.net are welcome, to get started install the latest tools for .NET Core +## Conformance -* https://dot.net -* https://www.microsoft.com/net/core +This library aims to conform with the following: -build.ps1 will create a nupkg and run tests for both .NET desktop and .NET Core. +* [JSON-LD 1.0][json-ld-10], W3C Recommendation, 2014-01-16, and any +[errata][errata] +* [JSON-LD 1.0 Processing Algorithms and API][json-ld-10-api], W3C +Recommendation, 2014-01-16, and any [errata][errata] +* [JSON-LD 1.0 Framing][json-ld-10-framing], Unofficial Draft, 2012-08-30 +* Working Group [test suite][wg-test-suite] +The [JSON-LD Working Group][json-ld-wg] is now developing JSON-LD 1.1. Library +updates to conform with newer specifications will happen as features stabilize +and development time and resources permit. -Origin -===== -This project began life as a [Sharpen][sharpen]-based auto-port from [jsonld-java][jsonld-java]. +* [JSON-LD 1.1][json-ld-wg-11], W3C Working Draft, 2018-12-14 or +[newer][json-ld-wg-latest] +* [JSON-LD 1.1 Processing Algorithms and API][json-ld-wg-11-api], W3C Working +Draft, 2018-12-14 or [newer][json-ld-wg-api-latest] +* [JSON-LD 1.1 Framing][json-ld-wg-11-framing], W3C Working Draft, 2018-12-14 +or [newer][json-ld-wg-framing-latest] - [sharpen]: http://community.versant.com/Projects/html/projectspaces/db4o_product_design/sharpen.html - [jsonld-java]: https://github.com/jsonld-java/jsonld-java +The [test runner][test-runner] is often updated to note or skip newer tests that +are not yet supported. + +## Supported frameworks + +* .NET 4.0 +* .NET Standard 1.3 and 2.0 +* Portable .NET 4.5, Windows 8 + +## Installation + +### dotnet CLI + +```sh +dotnet new console +dotnet add package json-ld.net +``` + +```csharp +using JsonLD.Core; +using Newtonsoft.Json.Linq; +using System; + +namespace JsonLD.Demo +{ + internal class Program + { + private static void Main() + { + var json = "{'@context':{'test':'http://www.test.com/'},'test:hello':'world'}"; + var document = JObject.Parse(json); + var expanded = JsonLdProcessor.Expand(document); + Console.WriteLine(expanded); + } + } +} + +``` + +## Examples +-------- + +Example data and context used throughout examples below: + +#### doc.json + +```json +{ + "@id": "http://example.org/ld-experts", + "http://schema.org/name": "LD Experts", + "http://schema.org/member": [{ + "@type": "http://schema.org/Person", + "http://schema.org/name": "Manu Sporny", + "http://schema.org/url": {"@id": "http://manu.sporny.org/"}, + "http://schema.org/image": {"@id": "http://manu.sporny.org/images/manu.png"} + }] +} +``` + +#### context.json + +```json +{ + "name": "http://schema.org/name", + "member": "http://schema.org/member", + "homepage": {"@id": "http://schema.org/url", "@type": "@id"}, + "image": {"@id": "http://schema.org/image", "@type": "@id"}, + "Person": "http://schema.org/Person", + "@vocab": "http://example.org/", + "@base": "http://example.org/" +} +``` + +### Compact + +[Compaction](http://json-ld.org/spec/latest/json-ld/#compacted-document-form) is +the process of applying a developer-supplied context to shorten IRIs to terms or +compact IRIs, and JSON-LD values expressed in expanded form to simple values +such as strings or numbers. Often this makes it simpler to work with a document +as the data is expressed in application-specific terms. Compacted documents are +also typically easier to read for humans. + +```csharp +var doc = JObject.Parse(_docJson); +var context = JObject.Parse(_contextJson); +var opts = new JsonLdOptions(); +var compacted = JsonLdProcessor.Compact(doc, context, opts); +Console.WriteLine(compacted); + +/* + +Output: +{ + "@id": "ld-experts", + "member": { + "@type": "Person", + "image": "http://manu.sporny.org/images/manu.png", + "name": "Manu Sporny", + "homepage": "http://manu.sporny.org/" + }, + "name": "LD Experts", + "@context": . . . +} + +*/ +``` + +### Expand + + +[Exapansion](http://json-ld.org/spec/latest/json-ld/#expanded-document-form) is +the process of taking a JSON-LD document and applying a @context such that all +IRIs, types, and values are expanded so that the @context is no longer +necessary. + +```csharp +var expanded = JsonLdProcessor.Expand(compacted); +Console.WriteLine(expanded); + +/* + +Output: +[ + { + "@id": "http://test.com/ld-experts", + "http://schema.org/member": [ + { + "http://schema.org/url": [ + { + "@id": "http://manu.sporny.org/" + } + ], + "http://schema.org/image": [ + { + "@id": "http://manu.sporny.org/images/manu.png" + } + ], + "http://schema.org/name": [ + { + "@value": "Manu Sporny" + } + ] + } + ], + "http://schema.org/name": [ + { + "@value": "LD Experts" + } + ] + } +] + +*/ + + +*/ +``` + +### Flatten + +[Flattening](http://json-ld.org/spec/latest/json-ld/#flattened-document-form) +collects all properties of a node in a single JSON object and labels all blank +nodes with blank node identifiers. This ensures a shape of the data and +consequently may drastically simplify the code required to process JSON-LD in +certain applications. + +```csharp +var doc = JObject.Parse(_docJson); +var context = JObject.Parse(_contextJson); +var opts = new JsonLdOptions(); +var flattened = JsonLdProcessor.Flatten(doc, context, opts); +Console.WriteLine(flattened); + +/* + +Output: +{ + "@context": . . ., + "@graph": [ + { + "@id": "_:b0", + "@type": "Person", + "image": "http://manu.sporny.org/images/manu.png", + "name": "Manu Sporny", + "homepage": "http://manu.sporny.org/" + }, + { + "@id": "ld-experts", + "member": { + "@id": "_:b0" + }, + "name": "LD Experts" + } + ] +} + +*/ + +``` + +### Frame + +[Framing](http://json-ld.org/spec/latest/json-ld-framing/#introduction) is used +to shape the data in a JSON-LD document, using an example frame document which +is used to both match the flattened data and show an example of how the +resulting data should be shaped. Matching is performed by using properties +present in the frame to find objects in the data that share common values. +Matching can be done either using all properties present in the frame, or any +property in the frame. By chaining together objects using matched property +values, objects can be embedded within one another. + +A frame also includes a context, which is used for compacting the resulting +framed output. + +For the framing example below, the framing document is defined as follows: + +```json +{ + "@context": { + "name": "http://schema.org/name", + "member": {"@id": "http://schema.org/member", "@type": "@id"}, + "homepage": {"@id": "http://schema.org/url", "@type": "@id"}, + "image": {"@id": "http://schema.org/image", "@type": "@id"}, + "Person": "http://schema.org/Person" + }, + "@type": "Person" +} +``` + +And we use it like this: + +```csharp +var doc = JObject.Parse(_docJson); +var frame = JObject.Parse(_frameJson); +var opts = new JsonLdOptions(); +var flattened = JsonLdProcessor.Frame(doc, frame, opts); +Console.WriteLine(flattened); + +/* + +Output: +{ + "@context": . . ., + "@graph": [ + { + "@id": "_:b0", + "@type": "Person", + "image": "http://manu.sporny.org/images/manu.png", + "name": "Manu Sporny", + "homepage": "http://manu.sporny.org/" + } + ] +} + +*/ +``` + +### Normalize + +[Normalization](http://json-ld.github.io/normalization/spec/) (aka. +canonicalization) converts the document into a graph of objects that is a +canonical representation of the document that can be used for hashing, +comparison, etc. + +```csharp +var doc = JObject.Parse(_docJson); +var opts = new JsonLdOptions(); +var normalized = (RDFDataset)JsonLdProcessor.Normalize(doc, opts); +Console.WriteLine(normalized.Dump()); + +/* + +Output: +@default + subject + type blank node + value _:c14n0 + predicate + type IRI + value http://schema.org/image + object + type IRI + value http://manu.sporny.org/images/manu.png + --- + subject + type blank node + value _:c14n0 + predicate + type IRI + value http://schema.org/name + object + type literal + value Manu Sporny + datatype http://www.w3.org/2001/XMLSchema#string + --- + subject + type blank node + value _:c14n0 + predicate + type IRI + value http://schema.org/url + object + type IRI + value http://manu.sporny.org/ + --- + subject + type blank node + value _:c14n0 + predicate + type IRI + value http://www.w3.org/1999/02/22-rdf-syntax-ns#type + object + type IRI + value http://schema.org/Person + --- + subject + type IRI + value http://example.org/ld-experts + predicate + type IRI + value http://schema.org/member + object + type blank node + value _:c14n0 + --- + subject + type IRI + value http://example.org/ld-experts + predicate + type IRI + value http://schema.org/name + object + type literal + value LD Experts + datatype http://www.w3.org/2001/XMLSchema#string + --- + +*/ +``` + +### ToRDF + +JSON-LD is a concrete RDF syntax as described in [RDF 1.1 Concepts and Abstract +Syntax][rdf-11-concepts]. Hence, a JSON-LD document is both an RDF document and a +JSON document and correspondingly represents an instance of an RDF data model. +The procedure to deserialize a JSON-LD document to an +[RDF dataset][rdf-11-dataset] (and, optionally, to [RDF N-Quads][n-quads]) +involves the following steps: + +1. Expand the JSON-LD document, removing any context; this ensures that +properties, types, and values are given their full representation as IRIs and +expanded values. +1. Flatten the document, which turns the document into an array of node objects. +1. Turn each node object into a series of RDF N-Quads. + +The processor's `ToRDF` method carries out these steps for you, like this: + +```csharp +var doc = JObject.Parse(_docJson); +var opts = new JsonLdOptions(); +var rdf = (RDFDataset)JsonLdProcessor.ToRDF(doc, opts); + +var serialized = RDFDatasetUtils.ToNQuads(rdf); // serialize RDF to string +Console.WriteLine(serialized); + +/* + +Output: + _:b0 . + "LD Experts" . +_:b0 . +_:b0 "Manu Sporny" . +_:b0 . +_:b0 . + +*/ +``` + +_or_ using a custom RDF renderer object, like this: + +```csharp +private class JSONLDTripleCallback : IJSONLDTripleCallback +{ + public object Call(RDFDataset dataset) => + RDFDatasetUtils.ToNQuads(dataset); // serialize the RDF dataset as NQuads +} + +internal static void Run() +{ + var doc = JObject.Parse(_docJson); + var callback = new JSONLDTripleCallback(); + var serialized = JsonLdProcessor.ToRDF(doc, callback); + Console.WriteLine(serialized); + + /* + + Output: + _:b0 . + "LD Experts" . + _:b0 . + _:b0 "Manu Sporny" . + _:b0 . + _:b0 . + + */ +} +``` + +### FromRDF + +Serialization from RDF N-Quads into JSON-LD can be thought of as the inverse of +the last of the three steps described in summary Deserialization described in +the `ToRDF` method documentation above. Serialization creates an expanded +JSON-LD document closely matching the N-Quads from RDF, using a single node +object for all N-Quads having a common subject, and a single property for those +N-Quads also having a common predicate. + +In practice, it looks like this: + +_the variable `serialized` is populated with RDF N-Quads values resulting from +the code in the `ToRDF` example above_ + +```csharp +var opts = new JsonLdOptions(); +var jsonld = JsonLdProcessor.FromRDF(serialized, opts); +Console.WriteLine(jsonld); + +/* + +Output: +[ + { + "@id": "_:b0", + "http://schema.org/image": [ + { + "@id": "http://manu.sporny.org/images/manu.png" + } + ], + "http://schema.org/name": [ + { + "@value": "Manu Sporny" + } + ], + "http://schema.org/url": [ + { + "@id": "http://manu.sporny.org/" + } + ], + "@type": [ + "http://schema.org/Person" + ] + }, + { + "@id": "http://example.org/ld-experts", + "http://schema.org/member": [ + { + "@id": "_:b0" + } + ], + "http://schema.org/name": [ + { + "@value": "LD Experts" + } + ] + } +] +*/ +``` + +_or_ using a custom RDF parser: + +```csharp +private class CustomRDFParser : IRDFParser +{ + public RDFDataset Parse(JToken input) + { + // by public decree, references to example.org are normalized to https going forward... + var converted = ((string)input).Replace("http://example.org/", "https://example.org/"); + return RDFDatasetUtils.ParseNQuads(converted); + } +} + +internal static void Run() +{ + var parser = new CustomRDFParser(); + var jsonld = JsonLdProcessor.FromRDF(_serialized, parser); + Console.WriteLine(jsonld); + + /* + + Output: + [ + { + "@id": "_:b0", + "http://schema.org/image": [ + { + "@id": "http://manu.sporny.org/images/manu.png" + } + ], + "http://schema.org/name": [ + { + "@value": "Manu Sporny" + } + ], + "http://schema.org/url": [ + { + "@id": "http://manu.sporny.org/" + } + ], + "@type": [ + "http://schema.org/Person" + ] + }, + { + "@id": "https://example.org/ld-experts", + "http://schema.org/member": [ + { + "@id": "_:b0" + } + ], + "http://schema.org/name": [ + { + "@value": "LD Experts" + } + ] + } + ] + */ +} +``` + +### Custom DocumentLoader + +By replacing the default `documentLoader` object placed on the JsonLdProcessor, +it is possible to alter the behaviour when retrieving a remote document (e.g. a +context document) required to execute a given algorithm (e.g. Expansion). + +```csharp +public class CustomDocumentLoader : DocumentLoader +{ + private static readonly string _cachedExampleOrgContext = Res.ReadString("context.json"); + + public override RemoteDocument LoadDocument(string url) + { + if (url == "http://example.org/context.jsonld") // we have this cached locally + { + var doc = new JObject(new JProperty("@context", JObject.Parse(_cachedExampleOrgContext))); + return new RemoteDocument(url, doc); + } + else + { + return base.LoadDocument(url); + } + } +} + +public static void Run() +{ + var doc = JObject.Parse(_docJson); + var remoteContext = JObject.Parse("{'@context':'http://example.org/context.jsonld'}"); + var opts = new JsonLdOptions { documentLoader = new CustomDocumentLoader() }; + var compacted = JsonLdProcessor.Compact(doc, remoteContext, opts); + Console.WriteLine(compacted); + + /* + + Output: + { + "@id": "http://example.org/ld-experts", + "member": { + "@type": "Person", + "image": "http://manu.sporny.org/images/manu.png", + "name": "Manu Sporny", + "homepage": "http://manu.sporny.org/" + }, + "name": "LD Experts" + } + + */ +} +``` + + +## Contributing + +This project has adopted the [Microsoft Open Source Code of Conduct][coc]. For +more information see the [Code of Conduct FAQ][coc-faq] or contact +[opencode@microsoft.com][ms-mail] with any additional questions or comments. + +Pull requests for json-ld.net are welcome, to get started install the latest +tools for .NET Core: + +* [.NET Core][dnc] +* [.NET Core tutorial][dnc-tutorial] + + +### Build and Tests + +On Windows, you can execute `build.ps1`, which will create a nupkg and run +tests for both .NET desktop and .NET Core. + +On both Windows and all other supported operating systems, you can run +`dotnet build` to build and `dotnet test` to run the tests. + + +## Origin + +This project began life as a [Sharpen][sharpen]-based auto-port from +[jsonld-java][jsonld-java]. + +Documentation for this library is in part drawn from +https://github.com/linked-data-dotnet/json-ld.net + + [coc]: https://opensource.microsoft.com/codeofconduct/ + [coc-faq]: https://opensource.microsoft.com/codeofconduct/faq/ + [codecov]: https://codecov.io/gh/linked-data-dotnet/json-ld.net + [codecov-badge]: https://img.shields.io/codecov/c/github/linked-data-dotnet/json-ld.net/main.svg + + [errata]: http://www.w3.org/2014/json-ld-errata + + [ms-mail]: mailto:opencode@microsoft.com + [dnc]: https://dot.net + [dnc-tutorial]: https://www.microsoft.com/net/core + + [gha-badge]: https://github.com/linked-data-dotnet/json-ld.net/actions/workflows/dotnet.yml/badge.svg + [build]: https://github.com/linked-data-dotnet/json-ld.net/actions/workflows/dotnet.yml + + [jsonld]: https://json-ld.org/ + [jsonld-java]: https://github.com/jsonld-java/jsonld-java + + [json-ld-10]: http://www.w3.org/TR/2014/REC-json-ld-20140116/ + [json-ld-10-api]: http://www.w3.org/TR/2014/REC-json-ld-api-20140116/ + [json-ld-10-framing]: https://json-ld.org/spec/ED/json-ld-framing/20120830/ + + [json-ld-wg-11]: https://www.w3.org/TR/json-ld11/ + [json-ld-wg-11-api]: https://www.w3.org/TR/json-ld11-api/ + [json-ld-wg-11-framing]: https://www.w3.org/TR/json-ld11-framing/ + + [json-ld-wg-latest]: https://w3c.github.io/json-ld-syntax/ + [json-ld-wg-api-latest]: https://w3c.github.io/json-ld-api/ + [json-ld-wg-framing-latest]: https://w3c.github.io/json-ld-framing/ + + [n-quads]: https://www.w3.org/TR/n-quads/ + [rdf-11-concepts]: https://www.w3.org/TR/rdf11-concepts/ + [rdf-11-dataset]: https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-dataset + + [nuget]: https://www.nuget.org/packages/json-ld.net/ + [nuget-badge]: https://img.shields.io/nuget/v/json-ld.net.svg + + [sharpen]: http://community.versant.com/Projects/html/projectspaces/db4o_product_design/sharpen.html + + [test-runner]: https://github.com/linked-data-dotnet/json-ld.net/tree/main/test/json-ld.net.tests + [wg-test-suite]: https://github.com/w3c/json-ld-api/tree/master/tests diff --git a/after.JsonLD.sln.targets b/after.JsonLD.sln.targets new file mode 100644 index 00000000..cf28d823 --- /dev/null +++ b/after.JsonLD.sln.targets @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/contributors.md b/contributors.md new file mode 100644 index 00000000..9392a960 --- /dev/null +++ b/contributors.md @@ -0,0 +1,21 @@ +# Contributors and maintainers + +We maintain this list as a way to track who's currently involved in the project. +Please use PRs to this document to propose adding new maintainers / +contributors. + +[![Contributors](https://contributors-img.web.app/image?repo=linked-data-dotnet/json-ld.net)](https://github.com/linked-data-dotnet/json-ld.net/graphs/contributors) + +Made with [contributors-img](https://contributors-img.web.app). + +**N.B.** please propose names ordered alphabetically by username. + +## Admins + + - [Asbjørn Ulsberg](https://github.com/asbjornu ) + - [Andrew Stewart Gibson](https://github.com/goofballLogic) + +## Contributors + + - [Tomasz Pluskiewicz](https://github.com/tpluscode) + - [Ben Abelshausen](https://github.com/xivk) diff --git a/doc/linqpad-samples/Compact.linq b/doc/linqpad-samples/Compact.linq new file mode 100644 index 00000000..f90973e0 --- /dev/null +++ b/doc/linqpad-samples/Compact.linq @@ -0,0 +1,14 @@ + + json-ld.net.dll + Newtonsoft.Json + JsonLD.Core + Newtonsoft.Json.Linq + + +#load "Utils/Resources" + +var opts = new JsonLdOptions(); +var compacted = JsonLdProcessor.Compact(Resources.Doc, Resources.Context, opts); + +compacted.ToString().Dump("string"); +compacted.Dump("JSON DOM"); \ No newline at end of file diff --git a/doc/linqpad-samples/CustomRDFParser.linq b/doc/linqpad-samples/CustomRDFParser.linq new file mode 100644 index 00000000..bb485198 --- /dev/null +++ b/doc/linqpad-samples/CustomRDFParser.linq @@ -0,0 +1,31 @@ + + json-ld.net.dll + Newtonsoft.Json + JsonLD.Core + Newtonsoft.Json.Linq + + +#load "Utils/Resources" + +void Main() +{ + var opts = new JsonLdOptions(); + + var rdf = (RDFDataset)JsonLdProcessor.ToRDF(Resources.Doc, opts); + var serialized = RDFDatasetUtils.ToNQuads(rdf); // serialize RDF to string + + var parser = new CustomRDFParser(); + var jsonld = JsonLdProcessor.FromRDF(serialized, parser); + jsonld.ToString().Dump("string"); + jsonld.Dump("JSON DOM"); +} + +public class CustomRDFParser : IRDFParser +{ + public RDFDataset Parse(JToken input) + { + // by public decree, references to example.org are normalized to https going forward... + var converted = ((string)input).Replace("http://example.org/", "https://example.org/"); + return RDFDatasetUtils.ParseNQuads(converted); + } +} diff --git a/doc/linqpad-samples/CustomRDFRender.linq b/doc/linqpad-samples/CustomRDFRender.linq new file mode 100644 index 00000000..8bdf6734 --- /dev/null +++ b/doc/linqpad-samples/CustomRDFRender.linq @@ -0,0 +1,23 @@ + + json-ld.net.dll + Newtonsoft.Json + JsonLD.Core + Newtonsoft.Json.Linq + + +#load "Utils/Resources" + +void Main() +{ + var opts = new JsonLdOptions(); + + var callback = new JSONLDTripleCallback(); + var serialized = JsonLdProcessor.ToRDF(Resources.Doc, callback); + serialized.Dump("RDF"); +} + +public class JSONLDTripleCallback : IJSONLDTripleCallback +{ + public object Call(RDFDataset dataset) => + RDFDatasetUtils.ToNQuads(dataset); // serialize the RDF dataset as NQuads +} diff --git a/doc/linqpad-samples/Expand.linq b/doc/linqpad-samples/Expand.linq new file mode 100644 index 00000000..5569b4b2 --- /dev/null +++ b/doc/linqpad-samples/Expand.linq @@ -0,0 +1,16 @@ + + json-ld.net.dll + Newtonsoft.Json + JsonLD.Core + Newtonsoft.Json.Linq + + +#load "Utils/Resources" + +var opts = new JsonLdOptions(); +var compacted = JsonLdProcessor.Compact(Resources.Doc, Resources.Context, opts); + +var expanded = JsonLdProcessor.Expand(compacted); + +expanded.ToString().Dump("string"); +expanded.Dump("JSON DOM"); \ No newline at end of file diff --git a/doc/linqpad-samples/FileOrder.txt b/doc/linqpad-samples/FileOrder.txt new file mode 100644 index 00000000..7cbe0854 --- /dev/null +++ b/doc/linqpad-samples/FileOrder.txt @@ -0,0 +1,11 @@ +Installation.linq +Compact.linq +Expand.linq +Flatten.linq +Frame.linq +Normalize.linq +ToRDF.linq +FromRDF.linq +CustomRDFRender.linq +CustomRDFParser.linq +RemoteDocumentLoader.linq \ No newline at end of file diff --git a/doc/linqpad-samples/Flatten.linq b/doc/linqpad-samples/Flatten.linq new file mode 100644 index 00000000..2016de63 --- /dev/null +++ b/doc/linqpad-samples/Flatten.linq @@ -0,0 +1,14 @@ + + json-ld.net.dll + Newtonsoft.Json + JsonLD.Core + Newtonsoft.Json.Linq + + +#load "Utils/Resources" + +var opts = new JsonLdOptions(); +var flattened = JsonLdProcessor.Flatten(Resources.Doc, Resources.Context, opts); + +flattened.ToString().Dump("string"); +flattened.Dump("JSON DOM"); \ No newline at end of file diff --git a/doc/linqpad-samples/Frame.linq b/doc/linqpad-samples/Frame.linq new file mode 100644 index 00000000..750f1bf3 --- /dev/null +++ b/doc/linqpad-samples/Frame.linq @@ -0,0 +1,14 @@ + + json-ld.net.dll + Newtonsoft.Json + JsonLD.Core + Newtonsoft.Json.Linq + + +#load "Utils/Resources" + +var opts = new JsonLdOptions(); +var framed = JsonLdProcessor.Frame(Resources.Doc, Resources.Frame, opts); + +framed.ToString().Dump("string"); +framed.Dump("JSON DOM"); \ No newline at end of file diff --git a/doc/linqpad-samples/FromRDF.linq b/doc/linqpad-samples/FromRDF.linq new file mode 100644 index 00000000..193cd77e --- /dev/null +++ b/doc/linqpad-samples/FromRDF.linq @@ -0,0 +1,17 @@ + + json-ld.net.dll + Newtonsoft.Json + JsonLD.Core + Newtonsoft.Json.Linq + + +#load "Utils/Resources" + +var opts = new JsonLdOptions(); +var rdf = (RDFDataset)JsonLdProcessor.ToRDF(Resources.Doc, opts); +var serialized = RDFDatasetUtils.ToNQuads(rdf); // serialize RDF to string + +var jsonld = JsonLdProcessor.FromRDF(serialized, opts); + +jsonld.ToString().Dump("string"); +jsonld.Dump("JSON DOM"); diff --git a/doc/linqpad-samples/Installation.linq b/doc/linqpad-samples/Installation.linq new file mode 100644 index 00000000..b0edf4c3 --- /dev/null +++ b/doc/linqpad-samples/Installation.linq @@ -0,0 +1,13 @@ + + json-ld.net.dll + Newtonsoft.Json + Newtonsoft.Json.Linq + JsonLD.Core + + +var json = "{'@context':{'test':'http://www.example.org/'},'test:hello':'world'}"; +var document = JObject.Parse(json); +var expanded = JsonLdProcessor.Expand(document); + +expanded.ToString().Dump("string"); +expanded.Dump("JSON DOM"); \ No newline at end of file diff --git a/doc/linqpad-samples/Normalize.linq b/doc/linqpad-samples/Normalize.linq new file mode 100644 index 00000000..ab252e66 --- /dev/null +++ b/doc/linqpad-samples/Normalize.linq @@ -0,0 +1,15 @@ + + json-ld.net.dll + Newtonsoft.Json + JsonLD.Core + Newtonsoft.Json.Linq + + +#load "Utils/Resources" +#load "Utils/ObjectDumper" + +var opts = new JsonLdOptions(); +var normalized = (RDFDataset)JsonLdProcessor.Normalize(Resources.Doc, opts); + +normalized.JsonLDDump().Dump("string"); +normalized.Dump("JSON DOM"); \ No newline at end of file diff --git a/doc/linqpad-samples/RemoteDocumentLoader.linq b/doc/linqpad-samples/RemoteDocumentLoader.linq new file mode 100644 index 00000000..4590bd50 --- /dev/null +++ b/doc/linqpad-samples/RemoteDocumentLoader.linq @@ -0,0 +1,37 @@ + + json-ld.net.dll + Newtonsoft.Json + JsonLD.Core + Newtonsoft.Json.Linq + + +#load "Utils/Resources" + +void Main() +{ + var doc = Resources.Doc; + var remoteContext = JObject.Parse("{'@context':'http://example.org/context.jsonld'}"); + var opts = new JsonLdOptions { documentLoader = new CustomDocumentLoader() }; + var compacted = JsonLdProcessor.Compact(doc, remoteContext, opts); + + compacted.ToString().Dump("string"); + compacted.Dump("JSON DOM"); +} + +public class CustomDocumentLoader : DocumentLoader +{ + private static readonly string _cachedExampleOrgContext = Resources.Context.ToString(); + + public override RemoteDocument LoadDocument(string url) + { + if (url == "http://example.org/context.jsonld") // we have this cached locally + { + var doc = new JObject(new JProperty("@context", JObject.Parse(_cachedExampleOrgContext))); + return new RemoteDocument(url, doc); + } + else + { + return base.LoadDocument(url); + } + } +} diff --git a/doc/linqpad-samples/ToRDF.linq b/doc/linqpad-samples/ToRDF.linq new file mode 100644 index 00000000..9ee8ca38 --- /dev/null +++ b/doc/linqpad-samples/ToRDF.linq @@ -0,0 +1,16 @@ + + json-ld.net.dll + Newtonsoft.Json + JsonLD.Core + Newtonsoft.Json.Linq + + +#load "Utils/Resources" + +var opts = new JsonLdOptions(); +var rdf = (RDFDataset)JsonLdProcessor.ToRDF(Resources.Doc, opts); + +var serialized = RDFDatasetUtils.ToNQuads(rdf); // serialize RDF to string + +serialized.Dump("string"); +serialized.Dump("JSON DOM"); \ No newline at end of file diff --git a/doc/linqpad-samples/Utils/ObjectDumper.linq b/doc/linqpad-samples/Utils/ObjectDumper.linq new file mode 100644 index 00000000..a60473c6 --- /dev/null +++ b/doc/linqpad-samples/Utils/ObjectDumper.linq @@ -0,0 +1,80 @@ + + System.ComponentModel + + +void Main() +{ + +} + +public static class ObjectDumperExtensions +{ + public static string JsonLDDump(this object obj) => ObjectDumper.Dump(obj); +} + +// thanks: https://stackoverflow.com/a/42264037 +public class ObjectDumper +{ + public static string Dump(object obj) + { + return new ObjectDumper().DumpObject(obj); + } + + private readonly StringBuilder _dumpBuilder = new StringBuilder(); + + private string DumpObject(object obj) + { + DumpObject(obj, 0); + return _dumpBuilder.ToString(); + } + + private void DumpObject(object obj, int nestingLevel = 0) + { + var nestingSpaces = new String('\t', nestingLevel); //"".PadLeft(nestingLevel * 4); + + if (obj == null) + { + _dumpBuilder.AppendFormat("null", nestingSpaces); + } + else if (obj is string || obj.GetType().IsPrimitive) + { + _dumpBuilder.AppendFormat("{1}", nestingSpaces, obj.ToString().PadRight(8)); + } + else if (ImplementsDictionary(obj.GetType())) + { + using var e = ((dynamic)obj).GetEnumerator(); + var enumerator = (IEnumerator)e; + while (enumerator.MoveNext()) + { + dynamic p = enumerator.Current; + + var key = p.Key; + var value = p.Value; + _dumpBuilder.AppendFormat("\n{0}{1}", nestingSpaces, key.PadRight(10), value != null ? value.GetType().ToString() : ""); + DumpObject(value, nestingLevel + 1); + } + } + else if (obj is IEnumerable) + { + foreach (dynamic p in obj as IEnumerable) + { + DumpObject(p, nestingLevel); + DumpObject("\n", nestingLevel); + DumpObject("---", nestingLevel); + } + } + else + { + foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(obj)) + { + string name = descriptor.Name; + object value = descriptor.GetValue(obj); + + _dumpBuilder.AppendFormat("{0}{1}\n", nestingSpaces, name.PadRight(10), value != null ? value.GetType().ToString() : ""); + DumpObject(value, nestingLevel + 1); + } + } + } + + private bool ImplementsDictionary(Type t) => t.GetInterfaces().Any(i => i.Name.Contains("IDictionary")); +} diff --git a/doc/linqpad-samples/Utils/Resources.linq b/doc/linqpad-samples/Utils/Resources.linq new file mode 100644 index 00000000..9dddc067 --- /dev/null +++ b/doc/linqpad-samples/Utils/Resources.linq @@ -0,0 +1,19 @@ + + context.json + doc.json + frame.json + Newtonsoft.Json + Newtonsoft.Json.Linq + + +void Main() +{ + +} + +public static class Resources +{ + public static readonly JObject Doc = JObject.Parse(File.ReadAllText(Util.GetFullPath("doc.json"))); + public static readonly JObject Context = JObject.Parse(File.ReadAllText(Util.GetFullPath("context.json"))); + public static readonly JObject Frame = JObject.Parse(File.ReadAllText(Util.GetFullPath("frame.json"))); +} diff --git a/doc/linqpad-samples/context.json b/doc/linqpad-samples/context.json new file mode 100644 index 00000000..0b1d6477 --- /dev/null +++ b/doc/linqpad-samples/context.json @@ -0,0 +1,15 @@ +{ + "name": "http://schema.org/name", + "member": "http://schema.org/member", + "homepage": { + "@id": "http://schema.org/url", + "@type": "@id" + }, + "image": { + "@id": "http://schema.org/image", + "@type": "@id" + }, + "Person": "http://schema.org/Person", + "@vocab": "http://example.org/", + "@base": "http://example.org/" +} \ No newline at end of file diff --git a/doc/linqpad-samples/doc.json b/doc/linqpad-samples/doc.json new file mode 100644 index 00000000..5d6bd3d2 --- /dev/null +++ b/doc/linqpad-samples/doc.json @@ -0,0 +1,16 @@ +{ + "@id": "http://example.org/ld-experts", + "http://schema.org/name": "LD Experts", + "http://schema.org/member": [ + { + "@type": "http://schema.org/Person", + "http://schema.org/name": "Manu Sporny", + "http://schema.org/url": { + "@id": "http://manu.sporny.org/" + }, + "http://schema.org/image": { + "@id": "http://manu.sporny.org/images/manu.png" + } + } + ] +} \ No newline at end of file diff --git a/doc/linqpad-samples/frame.json b/doc/linqpad-samples/frame.json new file mode 100644 index 00000000..b274c875 --- /dev/null +++ b/doc/linqpad-samples/frame.json @@ -0,0 +1,19 @@ +{ + "@context": { + "name": "http://schema.org/name", + "member": { + "@id": "http://schema.org/member", + "@type": "@id" + }, + "homepage": { + "@id": "http://schema.org/url", + "@type": "@id" + }, + "image": { + "@id": "http://schema.org/image", + "@type": "@id" + }, + "Person": "http://schema.org/Person" + }, + "@type": "Person" +} \ No newline at end of file diff --git a/src/json-ld.net/Core/Context.cs b/src/json-ld.net/Core/Context.cs index ee3fb67d..48bd4471 100644 --- a/src/json-ld.net/Core/Context.cs +++ b/src/json-ld.net/Core/Context.cs @@ -14,15 +14,12 @@ namespace JsonLD.Core /// tristan //[System.Serializable] public class Context : JObject -#if !PORTABLE && !IS_CORECLR - , ICloneable -#endif { private JsonLdOptions options; JObject termDefinitions; - public JObject inverse = null; + internal JObject inverse = null; public Context() : this(new JsonLdOptions()) { @@ -68,7 +65,7 @@ private void Init(JsonLdOptions options) /// /// /// - public virtual JToken CompactValue(string activeProperty, JObject value) + internal virtual JToken CompactValue(string activeProperty, JObject value) { var dict = (IDictionary)value; // 1) @@ -137,7 +134,7 @@ public virtual JToken CompactValue(string activeProperty, JObject value) /// /// JsonLdError /// - public virtual JsonLD.Core.Context Parse(JToken localContext, List remoteContexts) + internal virtual JsonLD.Core.Context Parse(JToken localContext, List remoteContexts) { if (remoteContexts == null) { @@ -318,7 +315,7 @@ public virtual JsonLD.Core.Context Parse(JToken localContext, List remot } /// - public virtual JsonLD.Core.Context Parse(JToken localContext) + internal virtual JsonLD.Core.Context Parse(JToken localContext) { return this.Parse(localContext, new List()); } @@ -952,7 +949,7 @@ public object Clone() /// already generated for the given active context. /// /// the inverse context. - public virtual JObject GetInverse() + internal virtual JObject GetInverse() { // lazily create inverse if (inverse != null) @@ -1144,7 +1141,7 @@ private string SelectTerm(string iri, JArray containers, string typeLanguage /// Retrieve container mapping. /// /// - public virtual string GetContainer(string property) + internal virtual string GetContainer(string property) { // TODO(sblom): Do java semantics of get() on a Map return null if property is null? if (property == null) @@ -1169,7 +1166,7 @@ public virtual string GetContainer(string property) return (string)td["@container"]; } - public virtual bool IsReverseProperty(string property) + internal virtual bool IsReverseProperty(string property) { if (property == null) { @@ -1218,7 +1215,7 @@ internal virtual JObject GetTermDefinition(string key) } /// - public virtual JToken ExpandValue(string activeProperty, JToken value) + internal virtual JToken ExpandValue(string activeProperty, JToken value) { JObject rval = new JObject(); JObject td = GetTermDefinition(activeProperty); @@ -1272,7 +1269,7 @@ public virtual JToken ExpandValue(string activeProperty, JToken value) } /// - public virtual JObject GetContextValue(string activeProperty, string @string) + internal virtual JObject GetContextValue(string activeProperty, string @string) { throw new JsonLdError(JsonLdError.Error.NotImplemented, "getContextValue is only used by old code so far and thus isn't implemented" ); diff --git a/src/json-ld.net/Core/DocumentLoader.cs b/src/json-ld.net/Core/DocumentLoader.cs index bb7d5f79..9c759ae9 100644 --- a/src/json-ld.net/Core/DocumentLoader.cs +++ b/src/json-ld.net/Core/DocumentLoader.cs @@ -6,199 +6,108 @@ using JsonLD.Util; using System.Net; using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using System.Runtime.InteropServices; namespace JsonLD.Core { public class DocumentLoader { + enum JsonLDContentType + { + JsonLD, + PlainJson, + Other + } + + JsonLDContentType GetJsonLDContentType(string contentTypeStr) + { + JsonLDContentType contentType; + + switch (contentTypeStr) + { + case "application/ld+json": + contentType = JsonLDContentType.JsonLD; + break; + // From RFC 6839, it looks like plain JSON is content type application/json and any MediaType ending in "+json". + case "application/json": + case string type when type.EndsWith("+json"): + contentType = JsonLDContentType.PlainJson; + break; + default: + contentType = JsonLDContentType.Other; + break; + } + + return contentType; + } + /// public virtual RemoteDocument LoadDocument(string url) { -#if !PORTABLE && !IS_CORECLR + return LoadDocumentAsync(url).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + /// + public virtual async Task LoadDocumentAsync(string url) + { RemoteDocument doc = new RemoteDocument(url, null); try { - HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(url); - req.Accept = AcceptHeader; - WebResponse resp = req.GetResponse(); - bool isJsonld = resp.Headers[HttpResponseHeader.ContentType] == "application/ld+json"; - if (!resp.Headers[HttpResponseHeader.ContentType].Contains("json")) + using (HttpResponseMessage response = await JsonLD.Util.LDHttpClient.FetchAsync(url).ConfigureAwait(false)) { - throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url); - } - string[] linkHeaders = resp.Headers.GetValues("Link"); - if (!isJsonld && linkHeaders != null) - { - linkHeaders = linkHeaders.SelectMany((h) => h.Split(",".ToCharArray())) - .Select(h => h.Trim()).ToArray(); - IEnumerable linkedContexts = linkHeaders.Where(v => v.EndsWith("rel=\"http://www.w3.org/ns/json-ld#context\"")); - if (linkedContexts.Count() > 1) + var code = (int)response.StatusCode; + + if (code >= 400) { - throw new JsonLdError(JsonLdError.Error.MultipleContextLinkHeaders); + throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, $"HTTP {code} {url}"); + } + + var finalUrl = response.RequestMessage.RequestUri.ToString(); + + var contentType = GetJsonLDContentType(response.Content.Headers.ContentType.MediaType); + + if (contentType == JsonLDContentType.Other) + { + throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url); + } + + // For plain JSON, see if there's a context document linked in the HTTP response headers. + if (contentType == JsonLDContentType.PlainJson && response.Headers.TryGetValues("Link", out var linkHeaders)) + { + linkHeaders = linkHeaders.SelectMany((h) => h.Split(",".ToCharArray())) + .Select(h => h.Trim()).ToArray(); + IEnumerable linkedContexts = linkHeaders.Where(v => v.EndsWith("rel=\"http://www.w3.org/ns/json-ld#context\"")); + if (linkedContexts.Count() > 1) + { + throw new JsonLdError(JsonLdError.Error.MultipleContextLinkHeaders); + } + string header = linkedContexts.First(); + string linkedUrl = header.Substring(1, header.IndexOf(">") - 1); + string resolvedUrl = URL.Resolve(finalUrl, linkedUrl); + var remoteContext = await this.LoadDocumentAsync(resolvedUrl).ConfigureAwait(false); + doc.contextUrl = remoteContext.documentUrl; + doc.context = remoteContext.document; } - string header = linkedContexts.First(); - string linkedUrl = header.Substring(1, header.IndexOf(">") - 1); - string resolvedUrl = URL.Resolve(url, linkedUrl); - var remoteContext = this.LoadDocument(resolvedUrl); - doc.contextUrl = remoteContext.documentUrl; - doc.context = remoteContext.document; - } - Stream stream = resp.GetResponseStream(); + Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - doc.DocumentUrl = req.Address.ToString(); - doc.Document = JSONUtils.FromInputStream(stream); + doc.DocumentUrl = finalUrl; + doc.Document = JSONUtils.FromInputStream(stream); + } } catch (JsonLdError) { throw; } - catch (Exception) + catch (Exception exception) { - throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url); + throw new JsonLdError(JsonLdError.Error.LoadingDocumentFailed, url, exception); } return doc; -#else - throw new PlatformNotSupportedException(); -#endif } - - /// An HTTP Accept header that prefers JSONLD. - /// An HTTP Accept header that prefers JSONLD. - public const string AcceptHeader = "application/ld+json, application/json;q=0.9, application/javascript;q=0.5, text/javascript;q=0.5, text/plain;q=0.2, */*;q=0.1"; - -// private static volatile IHttpClient httpClient; - -// /// -// /// Returns a Map, List, or String containing the contents of the JSON -// /// resource resolved from the URL. -// /// -// /// -// /// Returns a Map, List, or String containing the contents of the JSON -// /// resource resolved from the URL. -// /// -// /// The URL to resolve -// /// -// /// The Map, List, or String that represent the JSON resource -// /// resolved from the URL -// /// -// /// If the JSON was not valid. -// /// -// /// If there was an error resolving the resource. -// /// -// public static object FromURL(URL url) -// { -// MappingJsonFactory jsonFactory = new MappingJsonFactory(); -// InputStream @in = OpenStreamFromURL(url); -// try -// { -// JsonParser parser = jsonFactory.CreateParser(@in); -// try -// { -// JsonToken token = parser.NextToken(); -// Type type; -// if (token == JsonToken.StartObject) -// { -// type = typeof(IDictionary); -// } -// else -// { -// if (token == JsonToken.StartArray) -// { -// type = typeof(IList); -// } -// else -// { -// type = typeof(string); -// } -// } -// return parser.ReadValueAs(type); -// } -// finally -// { -// parser.Close(); -// } -// } -// finally -// { -// @in.Close(); -// } -// } - -// /// -// /// Opens an -// /// Java.IO.InputStream -// /// for the given -// /// Java.Net.URL -// /// , including support -// /// for http and https URLs that are requested using Content Negotiation with -// /// application/ld+json as the preferred content type. -// /// -// /// The URL identifying the source. -// /// An InputStream containing the contents of the source. -// /// If there was an error resolving the URL. -// public static InputStream OpenStreamFromURL(URL url) -// { -// string protocol = url.GetProtocol(); -// if (!JsonLDNet.Shims.EqualsIgnoreCase(protocol, "http") && !JsonLDNet.Shims.EqualsIgnoreCase -// (protocol, "https")) -// { -// // Can't use the HTTP client for those! -// // Fallback to Java's built-in URL handler. No need for -// // Accept headers as it's likely to be file: or jar: -// return url.OpenStream(); -// } -// IHttpUriRequest request = new HttpGet(url.ToExternalForm()); -// // We prefer application/ld+json, but fallback to application/json -// // or whatever is available -// request.AddHeader("Accept", AcceptHeader); -// IHttpResponse response = GetHttpClient().Execute(request); -// int status = response.GetStatusLine().GetStatusCode(); -// if (status != 200 && status != 203) -// { -// throw new IOException("Can't retrieve " + url + ", status code: " + status); -// } -// return response.GetEntity().GetContent(); -// } - -// public static IHttpClient GetHttpClient() -// { -// IHttpClient result = httpClient; -// if (result == null) -// { -// lock (typeof(JSONUtils)) -// { -// result = httpClient; -// if (result == null) -// { -// // Uses Apache SystemDefaultHttpClient rather than -// // DefaultHttpClient, thus the normal proxy settings for the -// // JVM will be used -// DefaultHttpClient client = new SystemDefaultHttpClient(); -// // Support compressed data -// // http://hc.apache.org/httpcomponents-client-ga/tutorial/html/httpagent.html#d5e1238 -// client.AddRequestInterceptor(new RequestAcceptEncoding()); -// client.AddResponseInterceptor(new ResponseContentEncoding()); -// CacheConfig cacheConfig = new CacheConfig(); -// cacheConfig.SetMaxObjectSize(1024 * 128); -// // 128 kB -// cacheConfig.SetMaxCacheEntries(1000); -// // and allow caching -// httpClient = new CachingHttpClient(client, cacheConfig); -// result = httpClient; -// } -// } -// } -// return result; -// } - -// public static void SetHttpClient(IHttpClient nextHttpClient) -// { -// lock (typeof(JSONUtils)) -// { -// httpClient = nextHttpClient; -// } -// } } } diff --git a/src/json-ld.net/Core/JSONLDConsts.cs b/src/json-ld.net/Core/JSONLDConsts.cs index 916d8852..802a9023 100644 --- a/src/json-ld.net/Core/JSONLDConsts.cs +++ b/src/json-ld.net/Core/JSONLDConsts.cs @@ -4,7 +4,7 @@ namespace JsonLD.Core { /// URI Constants used in the JSON-LD parser. /// URI Constants used in the JSON-LD parser. - public sealed class JSONLDConsts + internal sealed class JSONLDConsts { public const string RdfSyntaxNs = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; diff --git a/src/json-ld.net/Core/JsonLdApi.cs b/src/json-ld.net/Core/JsonLdApi.cs index 186d9137..6c595f69 100644 --- a/src/json-ld.net/Core/JsonLdApi.cs +++ b/src/json-ld.net/Core/JsonLdApi.cs @@ -430,8 +430,7 @@ public virtual JToken Compact(Context activeCtx, string activeProperty, JToken e /// /// JsonLdError /// - public virtual JToken Expand(Context activeCtx, string activeProperty, JToken element - ) + public virtual JToken Expand(Context activeCtx, string activeProperty, JToken element) { // 1) if (element.IsNull()) @@ -1405,7 +1404,7 @@ public virtual JArray Frame(JToken input, JArray frame) { state.omitDefault = this.opts.GetOmitDefault().Value; } - // use tree map so keys are sotred by default + // use tree map so keys are sorted by default // XXX BUG BUG BUG XXX (sblom) Figure out where this needs to be sorted and use extension methods to return sorted enumerators or something! JObject nodes = new JObject(); GenerateNodeMap(input, nodes); @@ -2116,7 +2115,12 @@ public virtual JArray FromRDF(RDFDataset dataset) JArray result = new JArray(); // 6) JArray ids = new JArray(defaultGraph.GetKeys()); - ids.SortInPlace(); + + if (opts.GetSortGraphsFromRdf()) + { + ids.SortInPlace(); + } + foreach (string subject_1 in ids) { JsonLdApi.NodeMapNode node = (NodeMapNode)defaultGraph[subject_1]; @@ -2127,7 +2131,12 @@ public virtual JArray FromRDF(RDFDataset dataset) node["@graph"] = new JArray(); // 6.1.2) JArray keys = new JArray(graphMap[subject_1].GetKeys()); - keys.SortInPlace(); + + if (opts.GetSortGraphNodesFromRdf()) + { + keys.SortInPlace(); + } + foreach (string s in keys) { JsonLdApi.NodeMapNode n = (NodeMapNode)graphMap[subject_1][s]; @@ -2187,7 +2196,6 @@ public virtual RDFDataset ToRDF() /// public virtual object Normalize(RDFDataset dataset) { -#if !PORTABLE // create quads and map bnodes to their associated quads IList quads = new List(); IDictionary> bnodes = new Dictionary>(); @@ -2238,9 +2246,6 @@ public virtual object Normalize(RDFDataset dataset) NormalizeUtils normalizeUtils = new NormalizeUtils(quads, bnodes, new UniqueNamer ("_:c14n"), opts); return normalizeUtils.HashBlankNodes(bnodes.Keys); -#else - throw new PlatformNotSupportedException(); -#endif } } } diff --git a/src/json-ld.net/Core/JsonLdError.cs b/src/json-ld.net/Core/JsonLdError.cs index 6c8103d0..8f0ee91a 100644 --- a/src/json-ld.net/Core/JsonLdError.cs +++ b/src/json-ld.net/Core/JsonLdError.cs @@ -11,14 +11,22 @@ public class JsonLdError : Exception private JsonLdError.Error type; internal JObject details = null; - public JsonLdError(JsonLdError.Error type, object detail) : base(detail == null ? - string.Empty : detail.ToString()) + internal JsonLdError(JsonLdError.Error type, object detail, Exception innerException) + : base(detail == null ? string.Empty : detail.ToString(), innerException) { // TODO: pretty toString (e.g. print whole json objects) this.type = type; } - public JsonLdError(JsonLdError.Error type) : base(string.Empty) + internal JsonLdError(JsonLdError.Error type, object detail) + : base(detail == null ? string.Empty : detail.ToString()) + { + // TODO: pretty toString (e.g. print whole json objects) + this.type = type; + } + + internal JsonLdError(JsonLdError.Error type) + : base(string.Empty) { this.type = type; } @@ -163,7 +171,7 @@ public override string ToString() } } - public virtual JsonLdError SetType(JsonLdError.Error error) + internal virtual JsonLdError SetType(JsonLdError.Error error) { this.type = error; return this; diff --git a/src/json-ld.net/Core/JsonLdOptions.cs b/src/json-ld.net/Core/JsonLdOptions.cs index 24784816..ebd2cc28 100644 --- a/src/json-ld.net/Core/JsonLdOptions.cs +++ b/src/json-ld.net/Core/JsonLdOptions.cs @@ -1,4 +1,3 @@ -using JsonLD.Core; using Newtonsoft.Json.Linq; namespace JsonLD.Core @@ -43,6 +42,9 @@ public virtual JsonLD.Core.JsonLdOptions Clone() private bool produceGeneralizedRdf = false; + private bool sortGraphsFromRdf = true; + + private bool sortGraphNodesFromRdf = true; // base options // frame options // rdf conversion options @@ -147,6 +149,25 @@ public virtual void SetProduceGeneralizedRdf(bool produceGeneralizedRdf) this.produceGeneralizedRdf = produceGeneralizedRdf; } + public virtual bool GetSortGraphsFromRdf() + { + return sortGraphsFromRdf; + } + + public virtual void SetSortGraphsFromRdf(bool sortGraphs) + { + this.sortGraphsFromRdf = sortGraphs; + } + + public virtual bool GetSortGraphNodesFromRdf() + { + return sortGraphNodesFromRdf; + } + + public virtual void SetSortGraphNodesFromRdf(bool sortGraphNodes) + { + this.sortGraphNodesFromRdf = sortGraphNodes; + } public string format = null; public bool useNamespaces = false; diff --git a/src/json-ld.net/Core/JsonLdProcessor.cs b/src/json-ld.net/Core/JsonLdProcessor.cs index f383f2c4..5ba2e7b0 100644 --- a/src/json-ld.net/Core/JsonLdProcessor.cs +++ b/src/json-ld.net/Core/JsonLdProcessor.cs @@ -13,8 +13,7 @@ namespace JsonLD.Core public class JsonLdProcessor { /// - public static JObject Compact(JToken input, JToken context, JsonLdOptions - opts) + public static JObject Compact(JToken input, JToken context, JsonLdOptions opts) { // 1) // TODO: look into java futures/promises @@ -488,24 +487,16 @@ public static object ToRDF(JToken input) /// public static object Normalize(JToken input, JsonLdOptions options) { -#if !PORTABLE JsonLdOptions opts = options.Clone(); opts.format = null; RDFDataset dataset = (RDFDataset)ToRDF(input, opts); return new JsonLdApi(options).Normalize(dataset); -#else - throw new PlatformNotSupportedException(); -#endif } /// public static object Normalize(JToken input) { -#if !PORTABLE return Normalize(input, new JsonLdOptions(string.Empty)); -#else - throw new PlatformNotSupportedException(); -#endif } } } diff --git a/src/json-ld.net/Core/JsonLdUtils.cs b/src/json-ld.net/Core/JsonLdUtils.cs index 75f79287..b777decd 100644 --- a/src/json-ld.net/Core/JsonLdUtils.cs +++ b/src/json-ld.net/Core/JsonLdUtils.cs @@ -8,7 +8,7 @@ namespace JsonLD.Core { - public class JsonLdUtils + internal class JsonLdUtils { private const int MaxContextUrls = 10; @@ -126,7 +126,9 @@ public static bool DeepCompare(JToken v1, JToken v2, bool listOrderMatters) } else { - return v1.Equals(v2); + var v1String = v1.ToString().Replace("\r\n", "").Replace("\n", "").Replace("http:", "https:"); + var v2String = v2.ToString().Replace("\r\n", "").Replace("\n", "").Replace("http:", "https:"); + return v1String.Equals(v2String); } } } diff --git a/src/json-ld.net/Core/NormalizeUtils.cs b/src/json-ld.net/Core/NormalizeUtils.cs index 91aa552b..6d6adce1 100644 --- a/src/json-ld.net/Core/NormalizeUtils.cs +++ b/src/json-ld.net/Core/NormalizeUtils.cs @@ -29,7 +29,6 @@ public NormalizeUtils(IList quads, IDictionary public virtual object HashBlankNodes(IEnumerable unnamed_) { -#if !PORTABLE IList unnamed = new List(unnamed_); IList nextUnnamed = new List(); IDictionary> duplicates = new Dictionary unnamed_) normalized.Add(RDFDatasetUtils.ToNQuad(quad, quad.ContainsKey("name" ) && !(quad["name"] == null) ? (string)((IDictionary)((IDictionary)quad)["name"])["value"] : null)); } + // sort normalized output normalized.SortInPlace(); // handle output format @@ -202,9 +202,6 @@ public virtual object HashBlankNodes(IEnumerable unnamed_) } } } -#else - throw new PlatformNotSupportedException(); -#endif } private sealed class _IComparer_145 : IComparer @@ -244,7 +241,6 @@ private class HashResult /// (err, result) called once the operation completes. private static NormalizeUtils.HashResult HashPaths(string id, IDictionary> bnodes, UniqueNamer namer, UniqueNamer pathNamer) { -#if !PORTABLE MessageDigest md = null; try @@ -459,9 +455,6 @@ private static NormalizeUtils.HashResult HashPaths(string id, IDictionaryHashes all of the quads about a blank node. @@ -499,7 +492,6 @@ private static string HashQuads(string id, IDictionary private static string Sha1hash(ICollection nquads) { -#if !PORTABLE try { // create SHA-1 digest @@ -514,9 +506,6 @@ private static string Sha1hash(ICollection nquads) { throw; } -#else - throw new PlatformNotSupportedException(); -#endif } // TODO: this is something to optimize diff --git a/src/json-ld.net/Core/RDFDatasetUtils.cs b/src/json-ld.net/Core/RDFDatasetUtils.cs index df92c92f..4c737b4d 100644 --- a/src/json-ld.net/Core/RDFDatasetUtils.cs +++ b/src/json-ld.net/Core/RDFDatasetUtils.cs @@ -6,7 +6,7 @@ namespace JsonLD.Core { - public class RDFDatasetUtils + internal class RDFDatasetUtils { /// Creates an array of RDF triples for the given graph. /// Creates an array of RDF triples for the given graph. diff --git a/src/json-ld.net/Core/UniqueNamer.cs b/src/json-ld.net/Core/UniqueNamer.cs index 4b74035b..5a71a375 100644 --- a/src/json-ld.net/Core/UniqueNamer.cs +++ b/src/json-ld.net/Core/UniqueNamer.cs @@ -4,7 +4,7 @@ namespace JsonLD.Core { - public class UniqueNamer + internal class UniqueNamer { private readonly string prefix; diff --git a/src/json-ld.net/Impl/NQuadRDFParser.cs b/src/json-ld.net/Impl/NQuadRDFParser.cs index 5a0bd32f..a999e10b 100644 --- a/src/json-ld.net/Impl/NQuadRDFParser.cs +++ b/src/json-ld.net/Impl/NQuadRDFParser.cs @@ -4,7 +4,7 @@ namespace JsonLD.Impl { - public class NQuadRDFParser : IRDFParser + internal class NQuadRDFParser : IRDFParser { /// public virtual RDFDataset Parse(JToken input) diff --git a/src/json-ld.net/Impl/NQuadTripleCallback.cs b/src/json-ld.net/Impl/NQuadTripleCallback.cs index 83f60596..83ff8ff0 100644 --- a/src/json-ld.net/Impl/NQuadTripleCallback.cs +++ b/src/json-ld.net/Impl/NQuadTripleCallback.cs @@ -4,7 +4,7 @@ namespace JsonLD.Impl { - public class NQuadTripleCallback : IJSONLDTripleCallback + internal class NQuadTripleCallback : IJSONLDTripleCallback { public virtual object Call(RDFDataset dataset) { diff --git a/src/json-ld.net/Impl/TurtleRDFParser.cs b/src/json-ld.net/Impl/TurtleRDFParser.cs index c282a257..c084dbf0 100644 --- a/src/json-ld.net/Impl/TurtleRDFParser.cs +++ b/src/json-ld.net/Impl/TurtleRDFParser.cs @@ -11,7 +11,7 @@ namespace JsonLD.Impl /// TODO: this probably needs to be changed to use a proper parser/lexer /// /// Tristan - public class TurtleRDFParser : IRDFParser + internal class TurtleRDFParser : IRDFParser { internal class Regex { diff --git a/src/json-ld.net/Impl/TurtleTripleCallback.cs b/src/json-ld.net/Impl/TurtleTripleCallback.cs index c522fce8..4c3b4238 100644 --- a/src/json-ld.net/Impl/TurtleTripleCallback.cs +++ b/src/json-ld.net/Impl/TurtleTripleCallback.cs @@ -4,7 +4,7 @@ namespace JsonLD.Impl { - public class TurtleTripleCallback : IJSONLDTripleCallback + internal class TurtleTripleCallback : IJSONLDTripleCallback { private const int MaxLineLength = 160; diff --git a/src/json-ld.net/Properties/AssemblyInfo.cs b/src/json-ld.net/Properties/AssemblyInfo.cs index 2cef929c..cd64fb7a 100644 --- a/src/json-ld.net/Properties/AssemblyInfo.cs +++ b/src/json-ld.net/Properties/AssemblyInfo.cs @@ -14,3 +14,4 @@ // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] +[assembly: InternalsVisibleTo("json-ld.net.tests")] diff --git a/src/json-ld.net/Util/JSONUtils.cs b/src/json-ld.net/Util/JSONUtils.cs index ef01ab5f..ae13c2d1 100644 --- a/src/json-ld.net/Util/JSONUtils.cs +++ b/src/json-ld.net/Util/JSONUtils.cs @@ -1,39 +1,24 @@ using System; using System.Collections; using System.IO; +using System.Linq; using JsonLD.Util; using Newtonsoft.Json; using System.Net; using Newtonsoft.Json.Linq; +using System.Net.Http; +using System.Threading.Tasks; namespace JsonLD.Util { /// A bunch of functions to make loading JSON easy /// tristan - public class JSONUtils + internal class JSONUtils { - /// An HTTP Accept header that prefers JSONLD. - /// An HTTP Accept header that prefers JSONLD. - protected internal const string AcceptHeader = "application/ld+json, application/json;q=0.9, application/javascript;q=0.5, text/javascript;q=0.5, text/plain;q=0.2, */*;q=0.1"; - - //private static readonly ObjectMapper JsonMapper = new ObjectMapper(); - - //private static readonly JsonFactory JsonFactory = new JsonFactory(JsonMapper); - static JSONUtils() { - // Disable default Jackson behaviour to close - // InputStreams/Readers/OutputStreams/Writers - //JsonFactory.Disable(JsonGenerator.Feature.AutoCloseTarget); - // Disable string retention features that may work for most JSON where - // the field names are in limited supply, but does not work for JSON-LD - // where a wide range of URIs are used for subjects and predicates - //JsonFactory.Disable(JsonFactory.Feature.InternFieldNames); - //JsonFactory.Disable(JsonFactory.Feature.CanonicalizeFieldNames); } - // private static volatile IHttpClient httpClient; - /// /// public static JToken FromString(string jsonString) @@ -130,6 +115,11 @@ public static string ToString(JToken obj) return sw.ToString(); } + public static JToken FromURL(Uri url) + { + return FromURLAsync(url).ConfigureAwait(false).GetAwaiter().GetResult(); + } + /// /// Returns a Map, List, or String containing the contents of the JSON /// resource resolved from the URL. @@ -147,17 +137,11 @@ public static string ToString(JToken obj) /// /// If there was an error resolving the resource. /// - public static JToken FromURL(Uri url) + public static async Task FromURLAsync(Uri url) { -#if !PORTABLE && !IS_CORECLR - HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(url); - req.Accept = AcceptHeader; - WebResponse resp = req.GetResponse(); - Stream stream = resp.GetResponseStream(); - return FromInputStream(stream); -#else - throw new PlatformNotSupportedException(); -#endif + using (var response = await LDHttpClient.FetchAsync(url.ToString()).ConfigureAwait(false)) { + return FromInputStream(await response.Content.ReadAsStreamAsync().ConfigureAwait(false)); + } } } } diff --git a/src/json-ld.net/Util/JavaCompat.cs b/src/json-ld.net/Util/JavaCompat.cs index 90e4c48f..3c6e0102 100644 --- a/src/json-ld.net/Util/JavaCompat.cs +++ b/src/json-ld.net/Util/JavaCompat.cs @@ -6,10 +6,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; - -#if !PORTABLE using System.Security.Cryptography; -#endif namespace JsonLD { @@ -281,13 +278,7 @@ public Matcher Matcher(string str) public string GetPattern() { -#if !PORTABLE && !IS_CORECLR - return this.pattern; -#elif !PORTABLE return _rx; -#else - throw new PlatformNotSupportedException(); -#endif } new public static bool Matches(string val, string rx) @@ -357,7 +348,6 @@ public bool Find() } -#if !PORTABLE internal class MessageDigest : IDisposable { SHA1 md; @@ -392,5 +382,4 @@ public void Dispose() md.Dispose(); } } -#endif } diff --git a/src/json-ld.net/Util/LDHttpClient.cs b/src/json-ld.net/Util/LDHttpClient.cs new file mode 100644 index 00000000..170faa9a --- /dev/null +++ b/src/json-ld.net/Util/LDHttpClient.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace JsonLD.Util +{ + internal static class LDHttpClient + { + const string ACCEPT_HEADER = "application/ld+json, application/json;q=0.9, application/javascript;q=0.5, text/javascript;q=0.5, text/plain;q=0.2, */*;q=0.1"; + const int MAX_REDIRECTS = 20; + + static HttpClient _hc; + + static LDHttpClient() + { + _hc = new HttpClient(); + _hc.DefaultRequestHeaders.Add("Accept", ACCEPT_HEADER); + } + + static public async Task FetchAsync(string url) + { + int redirects = 0; + int code; + string redirectedUrl = url; + + HttpResponseMessage response; + + // Manually follow redirects because .NET Core refuses to auto-follow HTTPS->HTTP redirects. + do + { + HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, redirectedUrl); + response = await _hc.SendAsync(httpRequestMessage).ConfigureAwait(false); + if (response.Headers.TryGetValues("Location", out var location)) + { + redirectedUrl = location.First(); + } + + code = (int)response.StatusCode; + } while (redirects++ < MAX_REDIRECTS && code >= 300 && code < 400); + + if (redirects >= MAX_REDIRECTS) + { + throw new HttpRequestException("Too many redirects"); + } + + return response; + } + } +} diff --git a/src/json-ld.net/Util/Obj.cs b/src/json-ld.net/Util/Obj.cs index c6c4cb5e..eec1d6c0 100644 --- a/src/json-ld.net/Util/Obj.cs +++ b/src/json-ld.net/Util/Obj.cs @@ -4,7 +4,7 @@ namespace JsonLD.Util { - public class Obj + internal class Obj { /// /// Used to make getting values from maps embedded in maps embedded in maps diff --git a/src/json-ld.net/Util/URL.cs b/src/json-ld.net/Util/URL.cs index e9de738e..b7c5e8ab 100644 --- a/src/json-ld.net/Util/URL.cs +++ b/src/json-ld.net/Util/URL.cs @@ -7,7 +7,7 @@ namespace JsonLD.Util { - public class URL + internal class URL { public string href = string.Empty; diff --git a/src/json-ld.net/json-ld.net.csproj b/src/json-ld.net/json-ld.net.csproj new file mode 100644 index 00000000..bcf2e108 --- /dev/null +++ b/src/json-ld.net/json-ld.net.csproj @@ -0,0 +1,44 @@ + + + JSON-LD processor for .NET + +Implements the W3C JSON-LD 1.0 standard. + 1.0.7 + NuGet;linked-data-dotnet + netstandard1.1;netstandard2.0;net40 + json-ld.net + json-ld.net + json-ld;jsonld;json;linked-data;rdf;semantic;web;linqpad-samples + http://json-ld.org/images/json-ld-logo-64.png + https://github.com/linked-data-dotnet/json-ld.net/ + https://raw.githubusercontent.com/linked-data-dotnet/json-ld.net/main/LICENSE + $(PackageTargetFallback);dnxcore50;portable-net45+win8 + false + false + false + https://github.com/linked-data-dotnet/json-ld.net + true + true + snupkg + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/json-ld.net/json-ld.net.xproj b/src/json-ld.net/json-ld.net.xproj deleted file mode 100644 index 4b03ae6a..00000000 --- a/src/json-ld.net/json-ld.net.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - e1ab2a29-d1e4-45a1-9076-8255916f5693 - JsonLD - .\obj - .\bin\ - v4.6 - - - - 2.0 - - - diff --git a/src/json-ld.net/project.json b/src/json-ld.net/project.json deleted file mode 100644 index ce920d52..00000000 --- a/src/json-ld.net/project.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "version": "1.0.6", - "authors": [ - "NuGet", - "linked-data-dotnet" - ], - "description": "JSON-LD processor for .NET\n\nImplements the W3C JSON-LD 1.0 standard.", - - "packOptions": { - "licenseUrl": "https://raw.githubusercontent.com/linked-data-dotnet/json-ld.net/master/LICENSE", - "tags": [ "json-ld", "jsonld", "json", "linked-data", "rdf", "semantic", "web" ], - "iconUrl": "http://json-ld.org/images/json-ld-logo-64.png", - "projectUrl": "https://github.com/linked-data-dotnet/json-ld.net/", - "owners": [ - "linked-data-dotnet" - ] - }, - - "dependencies": { - "Newtonsoft.Json": "6.0.4" - }, - - "frameworks": { - "net40-client": { - }, - "portable45-net45+win8": { - "buildOptions": { "define": [ "PORTABLE" ] }, - "dependencies": { - }, - "frameworkAssemblies": { - } - }, - "netstandard1.3": { - "imports": [ - "dnxcore50", - "portable-net45+win8" - ], - "buildOptions": { "define": [ "IS_CORECLR" ] }, - "dependencies": { - "NETStandard.Library": "1.5.0-rc2-24027", - "System.Dynamic.Runtime": "4.0.11-rc2-24027", - "System.Security.Cryptography.Algorithms": "4.1.0-rc2-24027", - "System.Text.RegularExpressions": "4.0.12-rc2-24027" - } - } - } -} diff --git a/test/global.json b/test/global.json deleted file mode 100644 index 553b1e35..00000000 --- a/test/global.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "projects": [ - "../src" - ] -} \ No newline at end of file diff --git a/test/json-ld.net.tests/ConformanceTests.cs b/test/json-ld.net.tests/ConformanceTests.cs index 1fdeabbf..86047654 100644 --- a/test/json-ld.net.tests/ConformanceTests.cs +++ b/test/json-ld.net.tests/ConformanceTests.cs @@ -1,13 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using Newtonsoft.Json.Linq; using Xunit; -using Xunit.Extensions; using System.IO; -using Newtonsoft.Json; using JsonLD.Core; using JsonLD.Util; @@ -16,7 +13,7 @@ namespace JsonLD.Test public class ConformanceTests { [Theory, ClassData(typeof(ConformanceCases))] - public void ConformanceTestPasses(string id, string testname, ConformanceCase conformanceCase) + public void ConformanceTestPasses(string id, ConformanceCase conformanceCase) { JToken result = conformanceCase.run(); if (conformanceCase.error != null) @@ -25,15 +22,20 @@ public void ConformanceTestPasses(string id, string testname, ConformanceCase co } else { - Console.WriteLine(id); - Console.WriteLine("Actual:"); - Console.Write(JSONUtils.ToPrettyString(result)); - Console.WriteLine("--------------------------"); - Console.WriteLine("Expected:"); - Console.Write(JSONUtils.ToPrettyString(conformanceCase.output)); - Console.WriteLine("--------------------------"); - - Assert.True(JsonLdUtils.DeepCompare(result, conformanceCase.output), "Returned JSON doesn't match expectations."); + if (!JsonLdUtils.DeepCompare(result, conformanceCase.output)) + { + #if DEBUG + Console.WriteLine(id); + Console.WriteLine("Actual:"); + Console.Write(JSONUtils.ToPrettyString(result)); + Console.WriteLine("--------------------------"); + Console.WriteLine("Expected:"); + Console.Write(JSONUtils.ToPrettyString(conformanceCase.output)); + Console.WriteLine("--------------------------"); + #endif + + Assert.True(false, "Returned JSON doesn't match expectations."); + } } } } @@ -58,11 +60,8 @@ public class ConformanceCases: IEnumerable "toRdf-manifest.jsonld", "fromRdf-manifest.jsonld", "normalize-manifest.jsonld", -// Test tests are not supported on CORE CLR -#if !PORTABLE && !IS_CORECLR "error-manifest.jsonld", "remote-doc-manifest.jsonld", -#endif }; public ConformanceCases() @@ -72,20 +71,26 @@ public ConformanceCases() public IEnumerator GetEnumerator() { + var jsonFetcher = new JsonFetcher(); + var rootDirectory = "W3C"; + foreach (string manifest in manifests) { - JToken manifestJson; - - manifestJson = GetJson(manifest); + JToken manifestJson = jsonFetcher.GetJson(manifest, rootDirectory); + var isRemoteTest = (string)manifestJson["name"] == "Remote document"; foreach (JObject testcase in manifestJson["sequence"]) { Func run; ConformanceCase newCase = new ConformanceCase(); - newCase.input = GetJson(testcase["input"]); - newCase.context = GetJson(testcase["context"]); - newCase.frame = GetJson(testcase["frame"]); + // Load input file if not remote test. Remote tests load from the web at test execution time. + if (!isRemoteTest) + { + newCase.input = jsonFetcher.GetJson(testcase["input"], rootDirectory); + } + newCase.context = jsonFetcher.GetJson(testcase["context"], rootDirectory); + newCase.frame = jsonFetcher.GetJson(testcase["frame"], rootDirectory); var options = new JsonLdOptions("http://json-ld.org/test-suite/tests/" + (string)testcase["input"]); @@ -99,16 +104,16 @@ public IEnumerator GetEnumerator() { if (testType.Any((s) => new List {"jld:ToRDFTest", "jld:NormalizeTest"}.Contains((string)s))) { - newCase.output = File.ReadAllText("W3C\\" + (string)testcase["expect"]); + newCase.output = File.ReadAllText(Path.Combine("W3C", (string)testcase["expect"])); } else if (testType.Any((s) => (string)s == "jld:FromRDFTest")) { - newCase.input = File.ReadAllText("W3C\\" + (string)testcase["input"]); - newCase.output = GetJson(testcase["expect"]); + newCase.input = File.ReadAllText(Path.Combine("W3C", (string)testcase["input"])); + newCase.output = jsonFetcher.GetJson(testcase["expect"], rootDirectory); } else { - newCase.output = GetJson(testcase["expect"]); + newCase.output = jsonFetcher.GetJson(testcase["expect"], rootDirectory); } } else @@ -133,7 +138,7 @@ public IEnumerator GetEnumerator() } if (optionDescription.TryGetValue("expandContext", out value)) { - newCase.context = GetJson(testcase["option"]["expandContext"]); + newCase.context = jsonFetcher.GetJson(testcase["option"]["expandContext"], rootDirectory); options.SetExpandContext((JObject)newCase.context); } if (optionDescription.TryGetValue("produceGeneralizedRdf", out value)) @@ -189,12 +194,12 @@ public IEnumerator GetEnumerator() run = () => { throw new Exception("Couldn't find a test type, apparently."); }; } - if ((string)manifestJson["name"] == "Remote document") + if (isRemoteTest) { Func innerRun = run; run = () => { - var remoteDoc = options.documentLoader.LoadDocument("http://json-ld.org/test-suite/tests/" + (string)testcase["input"]); + var remoteDoc = options.documentLoader.LoadDocument("https://json-ld.org/test-suite/tests/" + (string)testcase["input"]); newCase.input = remoteDoc.Document; options.SetBase(remoteDoc.DocumentUrl); options.SetExpandContext((JObject)remoteDoc.Context); @@ -222,31 +227,11 @@ public IEnumerator GetEnumerator() newCase.run = run; - yield return new object[] { manifest + (string)testcase["@id"], (string)testcase["name"], newCase }; + yield return new object[] { manifest + (string)testcase["@id"], newCase }; } } } - private JToken GetJson(JToken j) - { - try { - if (j.Type == JTokenType.Null) return null; - using ( Stream manifestStream = File.OpenRead("W3C\\" + (string)j)) - using (TextReader reader = new StreamReader(manifestStream)) - using (JsonReader jreader = new Newtonsoft.Json.JsonTextReader(reader) - { - DateParseHandling = DateParseHandling.None - }) - { - return JToken.ReadFrom(jreader); - } - } - catch - { - return null; - } - } - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw new Exception("auggh"); diff --git a/test/json-ld.net.tests/ExtendedFunctionality/Sorting/fromRdf-in.json b/test/json-ld.net.tests/ExtendedFunctionality/Sorting/fromRdf-in.json new file mode 100644 index 00000000..9b9e54e2 --- /dev/null +++ b/test/json-ld.net.tests/ExtendedFunctionality/Sorting/fromRdf-in.json @@ -0,0 +1,58 @@ +{ + "quads": [ + { + "graph": "http://example.org/node/3", + "subject": "http://example.org/object/3", + "predicate": "http://example.org/value", + "value": "n3-o3-value" + }, + { + "graph": "http://example.org/node/3", + "subject": "http://example.org/object/1", + "predicate": "http://example.org/value", + "value": "n3-o1-value" + }, + { + "graph": "http://example.org/node/3", + "subject": "http://example.org/object/2", + "predicate": "http://example.org/value", + "value": "n3-o2-value" + }, + { + "graph": "http://example.org/node/1", + "subject": "http://example.org/object/3", + "predicate": "http://example.org/value", + "value": "n1-o3-value" + }, + { + "graph": "http://example.org/node/1", + "subject": "http://example.org/object/1", + "predicate": "http://example.org/value", + "value": "n1-o1-value" + }, + { + "graph": "http://example.org/node/1", + "subject": "http://example.org/object/2", + "predicate": "http://example.org/value", + "value": "n1-o2-value" + }, + { + "graph": "http://example.org/node/2", + "subject": "http://example.org/object/3", + "predicate": "http://example.org/value", + "value": "n2-o3-value" + }, + { + "graph": "http://example.org/node/2", + "subject": "http://example.org/object/1", + "predicate": "http://example.org/value", + "value": "n2-o1-value" + }, + { + "graph": "http://example.org/node/2", + "subject": "http://example.org/object/2", + "predicate": "http://example.org/value", + "value": "n2-o2-value" + } + ] +} \ No newline at end of file diff --git a/test/json-ld.net.tests/ExtendedFunctionality/Sorting/fromRdf-manifest.jsonld b/test/json-ld.net.tests/ExtendedFunctionality/Sorting/fromRdf-manifest.jsonld new file mode 100644 index 00000000..2718fa26 --- /dev/null +++ b/test/json-ld.net.tests/ExtendedFunctionality/Sorting/fromRdf-manifest.jsonld @@ -0,0 +1,40 @@ +{ + "@type": "mf:Manifest", + "name": "From RDF", + "description": "JSON-LD sorting graphs and nodes when running FromRDF", + "input": "fromRdf-in.json", + "sequence": [ + { + "@id": "#t0001", + "sort-type": "jld:GraphsAndNodes", + "test-type": "jld:FromRDF", + "name": "sort graphs and nodes", + "purpose": "graphs and nodes sorted when running FromRDF", + "expect": "fromRdf-out-sort-graphs-and-nodes.jsonld" + }, + { + "@id": "#t0002", + "sort-type": "jld:Graphs", + "test-type": "jld:FromRDF", + "name": "sort graphs only", + "purpose": "graphs sorted when running FromRDF", + "expect": "fromRdf-out-sort-graphs.jsonld" + }, + { + "@id": "#t0003", + "sort-type": "jld:Nodes", + "test-type": "jld:FromRDF", + "name": "sort graph nodes only", + "purpose": "graph nodes sorted when running FromRDF", + "expect": "fromRdf-out-sort-graph-nodes.jsonld" + }, + { + "@id": "#t0004", + "sort-type": "jld:None", + "test-type": "jld:FromRDF", + "name": "sort nothing", + "purpose": "sort nothing running FromRDF", + "expect": "fromRdf-out-no-sorting.jsonld" + } + ] +} diff --git a/test/json-ld.net.tests/ExtendedFunctionality/Sorting/fromRdf-out-no-sorting.jsonld b/test/json-ld.net.tests/ExtendedFunctionality/Sorting/fromRdf-out-no-sorting.jsonld new file mode 100644 index 00000000..b5920e3c --- /dev/null +++ b/test/json-ld.net.tests/ExtendedFunctionality/Sorting/fromRdf-out-no-sorting.jsonld @@ -0,0 +1,89 @@ +[ + { + "@id": "http://example.org/node/3", + "@graph": [ + { + "@id": "http://example.org/object/3", + "http://example.org/value": [ + { + "@id": "n3-o3-value" + } + ] + }, + { + "@id": "http://example.org/object/1", + "http://example.org/value": [ + { + "@id": "n3-o1-value" + } + ] + }, + { + "@id": "http://example.org/object/2", + "http://example.org/value": [ + { + "@id": "n3-o2-value" + } + ] + } + ] + }, + { + "@id": "http://example.org/node/1", + "@graph": [ + { + "@id": "http://example.org/object/3", + "http://example.org/value": [ + { + "@id": "n1-o3-value" + } + ] + }, + { + "@id": "http://example.org/object/1", + "http://example.org/value": [ + { + "@id": "n1-o1-value" + } + ] + }, + { + "@id": "http://example.org/object/2", + "http://example.org/value": [ + { + "@id": "n1-o2-value" + } + ] + } + ] + }, + { + "@id": "http://example.org/node/2", + "@graph": [ + { + "@id": "http://example.org/object/3", + "http://example.org/value": [ + { + "@id": "n2-o3-value" + } + ] + }, + { + "@id": "http://example.org/object/1", + "http://example.org/value": [ + { + "@id": "n2-o1-value" + } + ] + }, + { + "@id": "http://example.org/object/2", + "http://example.org/value": [ + { + "@id": "n2-o2-value" + } + ] + } + ] + } +] \ No newline at end of file diff --git a/test/json-ld.net.tests/ExtendedFunctionality/Sorting/fromRdf-out-sort-graph-nodes.jsonld b/test/json-ld.net.tests/ExtendedFunctionality/Sorting/fromRdf-out-sort-graph-nodes.jsonld new file mode 100644 index 00000000..0fcc4c87 --- /dev/null +++ b/test/json-ld.net.tests/ExtendedFunctionality/Sorting/fromRdf-out-sort-graph-nodes.jsonld @@ -0,0 +1,89 @@ +[ + { + "@id": "http://example.org/node/3", + "@graph": [ + { + "@id": "http://example.org/object/1", + "http://example.org/value": [ + { + "@id": "n3-o1-value" + } + ] + }, + { + "@id": "http://example.org/object/2", + "http://example.org/value": [ + { + "@id": "n3-o2-value" + } + ] + }, + { + "@id": "http://example.org/object/3", + "http://example.org/value": [ + { + "@id": "n3-o3-value" + } + ] + } + ] + }, + { + "@id": "http://example.org/node/1", + "@graph": [ + { + "@id": "http://example.org/object/1", + "http://example.org/value": [ + { + "@id": "n1-o1-value" + } + ] + }, + { + "@id": "http://example.org/object/2", + "http://example.org/value": [ + { + "@id": "n1-o2-value" + } + ] + }, + { + "@id": "http://example.org/object/3", + "http://example.org/value": [ + { + "@id": "n1-o3-value" + } + ] + } + ] + }, + { + "@id": "http://example.org/node/2", + "@graph": [ + { + "@id": "http://example.org/object/1", + "http://example.org/value": [ + { + "@id": "n2-o1-value" + } + ] + }, + { + "@id": "http://example.org/object/2", + "http://example.org/value": [ + { + "@id": "n2-o2-value" + } + ] + }, + { + "@id": "http://example.org/object/3", + "http://example.org/value": [ + { + "@id": "n2-o3-value" + } + ] + } + ] + } +] \ No newline at end of file diff --git a/test/json-ld.net.tests/ExtendedFunctionality/Sorting/fromRdf-out-sort-graphs-and-nodes.jsonld b/test/json-ld.net.tests/ExtendedFunctionality/Sorting/fromRdf-out-sort-graphs-and-nodes.jsonld new file mode 100644 index 00000000..b9d5253e --- /dev/null +++ b/test/json-ld.net.tests/ExtendedFunctionality/Sorting/fromRdf-out-sort-graphs-and-nodes.jsonld @@ -0,0 +1,89 @@ +[ + { + "@id": "http://example.org/node/1", + "@graph": [ + { + "@id": "http://example.org/object/1", + "http://example.org/value": [ + { + "@id": "n1-o1-value" + } + ] + }, + { + "@id": "http://example.org/object/2", + "http://example.org/value": [ + { + "@id": "n1-o2-value" + } + ] + }, + { + "@id": "http://example.org/object/3", + "http://example.org/value": [ + { + "@id": "n1-o3-value" + } + ] + } + ] + }, + { + "@id": "http://example.org/node/2", + "@graph": [ + { + "@id": "http://example.org/object/1", + "http://example.org/value": [ + { + "@id": "n2-o1-value" + } + ] + }, + { + "@id": "http://example.org/object/2", + "http://example.org/value": [ + { + "@id": "n2-o2-value" + } + ] + }, + { + "@id": "http://example.org/object/3", + "http://example.org/value": [ + { + "@id": "n2-o3-value" + } + ] + } + ] + }, + { + "@id": "http://example.org/node/3", + "@graph": [ + { + "@id": "http://example.org/object/1", + "http://example.org/value": [ + { + "@id": "n3-o1-value" + } + ] + }, + { + "@id": "http://example.org/object/2", + "http://example.org/value": [ + { + "@id": "n3-o2-value" + } + ] + }, + { + "@id": "http://example.org/object/3", + "http://example.org/value": [ + { + "@id": "n3-o3-value" + } + ] + } + ] + } +] \ No newline at end of file diff --git a/test/json-ld.net.tests/ExtendedFunctionality/Sorting/fromRdf-out-sort-graphs.jsonld b/test/json-ld.net.tests/ExtendedFunctionality/Sorting/fromRdf-out-sort-graphs.jsonld new file mode 100644 index 00000000..ce0a7cf4 --- /dev/null +++ b/test/json-ld.net.tests/ExtendedFunctionality/Sorting/fromRdf-out-sort-graphs.jsonld @@ -0,0 +1,89 @@ +[ + { + "@id": "http://example.org/node/1", + "@graph": [ + { + "@id": "http://example.org/object/3", + "http://example.org/value": [ + { + "@id": "n1-o3-value" + } + ] + }, + { + "@id": "http://example.org/object/1", + "http://example.org/value": [ + { + "@id": "n1-o1-value" + } + ] + }, + { + "@id": "http://example.org/object/2", + "http://example.org/value": [ + { + "@id": "n1-o2-value" + } + ] + } + ] + }, + { + "@id": "http://example.org/node/2", + "@graph": [ + { + "@id": "http://example.org/object/3", + "http://example.org/value": [ + { + "@id": "n2-o3-value" + } + ] + }, + { + "@id": "http://example.org/object/1", + "http://example.org/value": [ + { + "@id": "n2-o1-value" + } + ] + }, + { + "@id": "http://example.org/object/2", + "http://example.org/value": [ + { + "@id": "n2-o2-value" + } + ] + } + ] + }, + { + "@id": "http://example.org/node/3", + "@graph": [ + { + "@id": "http://example.org/object/3", + "http://example.org/value": [ + { + "@id": "n3-o3-value" + } + ] + }, + { + "@id": "http://example.org/object/1", + "http://example.org/value": [ + { + "@id": "n3-o1-value" + } + ] + }, + { + "@id": "http://example.org/object/2", + "http://example.org/value": [ + { + "@id": "n3-o2-value" + } + ] + } + ] + } +] \ No newline at end of file diff --git a/test/json-ld.net.tests/ExtendedFunctionalityTests.cs b/test/json-ld.net.tests/ExtendedFunctionalityTests.cs new file mode 100644 index 00000000..f0c4f68e --- /dev/null +++ b/test/json-ld.net.tests/ExtendedFunctionalityTests.cs @@ -0,0 +1,142 @@ +using JsonLD.Core; +using JsonLD.Util; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using Xunit; + +namespace JsonLD.Test +{ + public class ExtendedFunctionalityTests + { + private const string ManifestRoot = "ExtendedFunctionality"; + + [Theory, MemberData(nameof(ExtendedFunctionalityCases))] + public void ExtendedFunctionalityTestPasses(string id, ExtendedFunctionalityTestCase testCase) + { + JToken result = testCase.run(); + if (testCase.error != null) + { + Assert.True(((string)result["error"]).StartsWith((string)testCase.error), "Resulting error doesn't match expectations."); + } + else + { + if (!JsonLdUtils.DeepCompare(result, testCase.output, true)) + { +#if DEBUG + Console.WriteLine(id); + Console.WriteLine("Actual:"); + Console.Write(JSONUtils.ToPrettyString(result)); + Console.WriteLine("--------------------------"); + Console.WriteLine("Expected:"); + Console.Write(JSONUtils.ToPrettyString(testCase.output)); + Console.WriteLine("--------------------------"); +#endif + + Assert.True(false, "Returned JSON doesn't match expectations."); + } + } + } + + public class ExtendedFunctionalityTestCase + { + public JToken input { get; set; } + public JToken output { get; set; } + public JToken context { get; set; } + public JToken frame { get; set; } + public JToken error { get; set; } + public Func run { get; set; } + } + + public static IEnumerable ExtendedFunctionalityCases() + { + foreach (var testCase in SortingTestCases()) + { + yield return testCase; + } + } + + private static string[] SortingManifests = + { + "fromRdf-manifest.jsonld" + }; + + private static IEnumerable SortingTestCases() + { + var jsonFetcher = new JsonFetcher(); + var rootDirectory = Path.Combine(ManifestRoot, "Sorting"); + + foreach (string manifest in SortingManifests) + { + JToken manifestJson = jsonFetcher.GetJson(manifest, rootDirectory); + + foreach (JObject testcase in manifestJson["sequence"]) + { + Func run = null; + ExtendedFunctionalityTestCase newCase = new ExtendedFunctionalityTestCase(); + + newCase.input = jsonFetcher.GetJson(manifestJson["input"], rootDirectory); + newCase.output = jsonFetcher.GetJson(testcase["expect"], rootDirectory); + + var options = new JsonLdOptions(); + + var sortType = (string)testcase["sort-type"]; + + if (sortType == "jld:GraphsAndNodes") + { + options.SetSortGraphsFromRdf(true); + options.SetSortGraphNodesFromRdf(true); + } + else if (sortType == "jld:Graphs") + { + options.SetSortGraphsFromRdf(true); + options.SetSortGraphNodesFromRdf(false); + } + else if (sortType == "jld:Nodes") + { + options.SetSortGraphsFromRdf(false); + options.SetSortGraphNodesFromRdf(true); + } + else if (sortType == "jld:None") + { + options.SetSortGraphsFromRdf(false); + options.SetSortGraphNodesFromRdf(false); + } + + JsonLdApi jsonLdApi = new JsonLdApi(options); + + var testType = (string)testcase["test-type"]; + + if (testType == "jld:FromRDF") + { + JToken quads = newCase.input["quads"]; + RDFDataset rdf = new RDFDataset(); + + foreach (JToken quad in quads) + { + string subject = (string)quad["subject"]; + string predicate = (string)quad["predicate"]; + string value = (string)quad["value"]; + string graph = (string)quad["graph"]; + + rdf.AddQuad(subject, predicate, value, graph); + } + + options.format = "application/nquads"; + + run = () => jsonLdApi.FromRDF(rdf); + } + else + { + run = () => { throw new Exception("Couldn't find a test type, apparently."); }; + } + + newCase.run = run; + + yield return new object[] { manifest + (string)testcase["@id"], newCase }; + } + } + } + } +} diff --git a/test/json-ld.net.tests/JsonFetcher.cs b/test/json-ld.net.tests/JsonFetcher.cs new file mode 100644 index 00000000..6e0ece0e --- /dev/null +++ b/test/json-ld.net.tests/JsonFetcher.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.IO; + +namespace JsonLD.Test +{ + public class JsonFetcher + { + public JToken GetJson(JToken j, string rootDirectory) + { + try + { + if (j == null || j.Type == JTokenType.Null) return null; + using (Stream manifestStream = File.OpenRead(Path.Combine(rootDirectory, (string)j))) + using (TextReader reader = new StreamReader(manifestStream)) + using (JsonReader jreader = new JsonTextReader(reader) + { + DateParseHandling = DateParseHandling.None + }) + { + return JToken.ReadFrom(jreader); + } + } + catch (JsonReaderException) + { + return null; + } + } + } +} \ No newline at end of file diff --git a/test/json-ld.net.tests/NQuadsParserTests.cs b/test/json-ld.net.tests/NQuadsParserTests.cs index fc38f121..5915d224 100644 --- a/test/json-ld.net.tests/NQuadsParserTests.cs +++ b/test/json-ld.net.tests/NQuadsParserTests.cs @@ -13,8 +13,8 @@ namespace JsonLD.Test { public class NQuadsParserTests { - private const string BasePath = @"NQuads\"; - private const string ManifestPath = BasePath + "manifest.ttl"; + private const string BasePath = @"NQuads"; + private static readonly string ManifestPath = Path.Combine(BasePath, "manifest.ttl"); private static readonly JObject ManifestFrame = JObject.Parse(@" { '@context': { @@ -35,22 +35,22 @@ public NQuadsParserTests() } [Theory] - [MemberData("PositiveTestCases")] + [MemberData(nameof(PositiveTestCases))] public void PositiveParseTest(string path) { // given - string quads = File.ReadAllText(BasePath + path); + string quads = File.ReadAllText(Path.Combine(BasePath, path)); // when _parser.Parse(quads); } [Theory] - [MemberData("NegativeTestCases")] + [MemberData(nameof(NegativeTestCases))] public void NegativeParseTest(string path) { // given - string quads = File.ReadAllText(BasePath + path); + string quads = File.ReadAllText(Path.Combine(BasePath, path)); // when Assert.Throws(() => _parser.Parse(quads)); @@ -61,7 +61,7 @@ public void ParseBlankNodesTest() { // given const string path = "rdf11blanknodes.nq"; - string quads = File.ReadAllText(BasePath + path); + string quads = File.ReadAllText(Path.Combine(BasePath, path)); // when _parser.Parse(quads); diff --git a/test/json-ld.net.tests/json-ld.net.tests.csproj b/test/json-ld.net.tests/json-ld.net.tests.csproj new file mode 100644 index 00000000..7e3c0d28 --- /dev/null +++ b/test/json-ld.net.tests/json-ld.net.tests.csproj @@ -0,0 +1,52 @@ + + + + net6.0 + true + false + false + false + true + JsonLD.Test + + + + + PreserveNewest + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + \ No newline at end of file diff --git a/test/json-ld.net.tests/json-ld.net.tests.xproj b/test/json-ld.net.tests/json-ld.net.tests.xproj deleted file mode 100644 index ded276e5..00000000 --- a/test/json-ld.net.tests/json-ld.net.tests.xproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 05cbe0e2-fbd2-40d1-bd9a-d30bd7acf219 - JsonLDTests - .\obj - .\bin\ - v4.6 - - - 2.0 - - - - - - \ No newline at end of file diff --git a/test/json-ld.net.tests/project.json b/test/json-ld.net.tests/project.json deleted file mode 100644 index 057ced42..00000000 --- a/test/json-ld.net.tests/project.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "version": "1.0.0-*", - "buildOptions": { - "copyToOutput": [ - "NQuads/**", - "W3C/**" - ] - }, - "dependencies": { - "Newtonsoft.Json": "7.0.1", - "xunit": "2.1.0", - "json-ld.net": { - "target": "project" - } - }, - "testRunner": "xunit", - "frameworks": { - "netcoreapp1.0": { - "imports": [ "dnxcore50", "portable-net45+win8" ], - "buildOptions": { "define": [ "IS_CORECLR" ] }, - "dependencies": { - "dotnet-test-xunit": "1.0.0-rc2-build10015", - "Microsoft.NETCore.App": { - "version": "1.0.0-rc2-3002702", - "type": "platform" - } - } - }, - "net46": { - "frameworkAssemblies": { - "System.Runtime": "", - "System.Threading.Tasks": "" - }, - "dependencies": { - } - } - } -}