From 565a0345cc7f002085338ea88ee7074345ccc7df Mon Sep 17 00:00:00 2001 From: marcel Date: Sun, 5 Nov 2023 17:09:47 +0100 Subject: [PATCH] Add tools to manage datamodel based on NGSI-LD, JSON-Schema and SHACL This PR builds the foundation of the NGSI-LD Datamodel. It contains 3 tools: - validate.js: This tool allow to validate a concise NGSI-LD model with a Schema - jsonldConverter.js: This tool converts between normalized, concise and expanded NGSI-LD models - jsonschema2shacl.js: This tool converts a jsonschema to a SHACL description In addition the PR provides a Makefile with the targets - setup: Install npm and pip packages - lint: Lint all node.js scripts and shells - test: Unit Tests and Script tests Related Epic: #427 Related User story: #433, #434, #435, #436 Signed-off-by: marcel --- .github/workflows/build.yaml | 5 +- semantic-model/datamodel/README.md | 495 ++++++++++++++++++ .../datamodel/examples/plasmacutter_data.json | 9 + .../examples/plasmacutter_schema.json | 79 +++ .../examples/simple_plasmacutter2_data.json | 5 + .../examples/simple_plasmacutter2_schema.json | 17 + .../examples/simple_plasmacutter_data.json | 5 + .../examples/simple_plasmacutter_schema.json | 17 + semantic-model/datamodel/tools/.eslintrc.js | 26 + semantic-model/datamodel/tools/Makefile | 30 ++ .../datamodel/tools/jsonldConverter.js | 133 +++++ .../datamodel/tools/jsonschema2shacl.js | 83 +++ .../datamodel/tools/lib/jsonldUtils.js | 140 +++++ .../datamodel/tools/lib/shaclUtils.js | 259 +++++++++ semantic-model/datamodel/tools/package.json | 34 ++ .../datamodel/tools/tests/jsonld2jsonld/c0 | 38 ++ .../tools/tests/jsonld2jsonld/payload1.jsonld | 13 + .../tests/jsonld2jsonld/payload1.jsonld_n_c0 | 53 ++ .../tests/jsonld2jsonld/payload1.jsonld_r_c0 | 66 +++ .../tests/jsonld2jsonld/payload1.jsonld_x_c0 | 37 ++ .../tools/tests/jsonld2jsonld/payload2.jsonld | 37 ++ .../tests/jsonld2jsonld/payload2.jsonld_n_c0 | 53 ++ .../tests/jsonld2jsonld/payload2.jsonld_r_c0 | 66 +++ .../tests/jsonld2jsonld/payload2.jsonld_x_c0 | 37 ++ .../tools/tests/jsonld2jsonld/payload3.jsonld | 24 + .../tests/jsonld2jsonld/payload3.jsonld_n_c0 | 97 ++++ .../tests/jsonld2jsonld/payload3.jsonld_r_c0 | 116 ++++ .../tests/jsonld2jsonld/payload3.jsonld_x_c0 | 63 +++ .../tools/tests/jsonld2jsonld/test.sh | 27 + .../datamodel/tools/tests/schema2shacl/c0 | 38 ++ .../tools/tests/schema2shacl/schema1_c0.json | 17 + .../tests/schema2shacl/schema1_c0.json_id | 1 + .../tests/schema2shacl/schema1_c0.json_result | 7 + .../tools/tests/schema2shacl/schema2_c0.json | 17 + .../tests/schema2shacl/schema2_c0.json_id | 1 + .../tests/schema2shacl/schema2_c0.json_result | 6 + .../tools/tests/schema2shacl/schema3_c0.json | 79 +++ .../tests/schema2shacl/schema3_c0.json_id | 1 + .../tests/schema2shacl/schema3_c0.json_result | 40 ++ .../tools/tests/schema2shacl/test.sh | 27 + .../datamodel/tools/tests/testJsonldUtils.js | 151 ++++++ .../datamodel/tools/tests/testShaclUtils.js | 476 +++++++++++++++++ .../datamodel/tools/tests/validation/test.sh | 28 + .../tests/validation/validation2_data1.json | 5 + .../validation/validation2_data1.json_id | 1 + .../validation/validation2_data1.json_result | 1 + .../tests/validation/validation2_schema.json | 17 + .../tests/validation/validation3_data1.json | 9 + .../validation/validation3_data1.json_id | 1 + .../validation/validation3_data1.json_result | 1 + .../tests/validation/validation3_data2.json | 9 + .../validation/validation3_data2.json_id | 1 + .../validation/validation3_data2.json_result | 12 + .../tests/validation/validation3_data3.json | 9 + .../validation/validation3_data3.json_id | 1 + .../validation/validation3_data3.json_result | 10 + .../tests/validation/validation3_data4.json | 7 + .../validation/validation3_data4.json_id | 1 + .../validation/validation3_data4.json_result | 10 + .../tests/validation/validation3_schema.json | 79 +++ .../tests/validation/validation_data1.json | 5 + .../tests/validation/validation_data1.json_id | 1 + .../validation/validation_data1.json_result | 1 + .../tests/validation/validation_data2.json | 5 + .../tests/validation/validation_data2.json_id | 1 + .../validation/validation_data2.json_result | 12 + .../tests/validation/validation_data3.json | 5 + .../tests/validation/validation_data3.json_id | 1 + .../validation/validation_data3.json_result | 10 + .../tests/validation/validation_schema.json | 17 + semantic-model/datamodel/tools/validate.js | 92 ++++ 71 files changed, 3276 insertions(+), 1 deletion(-) create mode 100644 semantic-model/datamodel/README.md create mode 100644 semantic-model/datamodel/examples/plasmacutter_data.json create mode 100644 semantic-model/datamodel/examples/plasmacutter_schema.json create mode 100644 semantic-model/datamodel/examples/simple_plasmacutter2_data.json create mode 100644 semantic-model/datamodel/examples/simple_plasmacutter2_schema.json create mode 100644 semantic-model/datamodel/examples/simple_plasmacutter_data.json create mode 100644 semantic-model/datamodel/examples/simple_plasmacutter_schema.json create mode 100644 semantic-model/datamodel/tools/.eslintrc.js create mode 100644 semantic-model/datamodel/tools/Makefile create mode 100644 semantic-model/datamodel/tools/jsonldConverter.js create mode 100644 semantic-model/datamodel/tools/jsonschema2shacl.js create mode 100644 semantic-model/datamodel/tools/lib/jsonldUtils.js create mode 100644 semantic-model/datamodel/tools/lib/shaclUtils.js create mode 100644 semantic-model/datamodel/tools/package.json create mode 100644 semantic-model/datamodel/tools/tests/jsonld2jsonld/c0 create mode 100644 semantic-model/datamodel/tools/tests/jsonld2jsonld/payload1.jsonld create mode 100644 semantic-model/datamodel/tools/tests/jsonld2jsonld/payload1.jsonld_n_c0 create mode 100644 semantic-model/datamodel/tools/tests/jsonld2jsonld/payload1.jsonld_r_c0 create mode 100644 semantic-model/datamodel/tools/tests/jsonld2jsonld/payload1.jsonld_x_c0 create mode 100644 semantic-model/datamodel/tools/tests/jsonld2jsonld/payload2.jsonld create mode 100644 semantic-model/datamodel/tools/tests/jsonld2jsonld/payload2.jsonld_n_c0 create mode 100644 semantic-model/datamodel/tools/tests/jsonld2jsonld/payload2.jsonld_r_c0 create mode 100644 semantic-model/datamodel/tools/tests/jsonld2jsonld/payload2.jsonld_x_c0 create mode 100644 semantic-model/datamodel/tools/tests/jsonld2jsonld/payload3.jsonld create mode 100644 semantic-model/datamodel/tools/tests/jsonld2jsonld/payload3.jsonld_n_c0 create mode 100644 semantic-model/datamodel/tools/tests/jsonld2jsonld/payload3.jsonld_r_c0 create mode 100644 semantic-model/datamodel/tools/tests/jsonld2jsonld/payload3.jsonld_x_c0 create mode 100644 semantic-model/datamodel/tools/tests/jsonld2jsonld/test.sh create mode 100644 semantic-model/datamodel/tools/tests/schema2shacl/c0 create mode 100644 semantic-model/datamodel/tools/tests/schema2shacl/schema1_c0.json create mode 100644 semantic-model/datamodel/tools/tests/schema2shacl/schema1_c0.json_id create mode 100644 semantic-model/datamodel/tools/tests/schema2shacl/schema1_c0.json_result create mode 100644 semantic-model/datamodel/tools/tests/schema2shacl/schema2_c0.json create mode 100644 semantic-model/datamodel/tools/tests/schema2shacl/schema2_c0.json_id create mode 100644 semantic-model/datamodel/tools/tests/schema2shacl/schema2_c0.json_result create mode 100644 semantic-model/datamodel/tools/tests/schema2shacl/schema3_c0.json create mode 100644 semantic-model/datamodel/tools/tests/schema2shacl/schema3_c0.json_id create mode 100644 semantic-model/datamodel/tools/tests/schema2shacl/schema3_c0.json_result create mode 100644 semantic-model/datamodel/tools/tests/schema2shacl/test.sh create mode 100644 semantic-model/datamodel/tools/tests/testJsonldUtils.js create mode 100644 semantic-model/datamodel/tools/tests/testShaclUtils.js create mode 100644 semantic-model/datamodel/tools/tests/validation/test.sh create mode 100644 semantic-model/datamodel/tools/tests/validation/validation2_data1.json create mode 100644 semantic-model/datamodel/tools/tests/validation/validation2_data1.json_id create mode 100644 semantic-model/datamodel/tools/tests/validation/validation2_data1.json_result create mode 100644 semantic-model/datamodel/tools/tests/validation/validation2_schema.json create mode 100644 semantic-model/datamodel/tools/tests/validation/validation3_data1.json create mode 100644 semantic-model/datamodel/tools/tests/validation/validation3_data1.json_id create mode 100644 semantic-model/datamodel/tools/tests/validation/validation3_data1.json_result create mode 100644 semantic-model/datamodel/tools/tests/validation/validation3_data2.json create mode 100644 semantic-model/datamodel/tools/tests/validation/validation3_data2.json_id create mode 100644 semantic-model/datamodel/tools/tests/validation/validation3_data2.json_result create mode 100644 semantic-model/datamodel/tools/tests/validation/validation3_data3.json create mode 100644 semantic-model/datamodel/tools/tests/validation/validation3_data3.json_id create mode 100644 semantic-model/datamodel/tools/tests/validation/validation3_data3.json_result create mode 100644 semantic-model/datamodel/tools/tests/validation/validation3_data4.json create mode 100644 semantic-model/datamodel/tools/tests/validation/validation3_data4.json_id create mode 100644 semantic-model/datamodel/tools/tests/validation/validation3_data4.json_result create mode 100644 semantic-model/datamodel/tools/tests/validation/validation3_schema.json create mode 100644 semantic-model/datamodel/tools/tests/validation/validation_data1.json create mode 100644 semantic-model/datamodel/tools/tests/validation/validation_data1.json_id create mode 100644 semantic-model/datamodel/tools/tests/validation/validation_data1.json_result create mode 100644 semantic-model/datamodel/tools/tests/validation/validation_data2.json create mode 100644 semantic-model/datamodel/tools/tests/validation/validation_data2.json_id create mode 100644 semantic-model/datamodel/tools/tests/validation/validation_data2.json_result create mode 100644 semantic-model/datamodel/tools/tests/validation/validation_data3.json create mode 100644 semantic-model/datamodel/tools/tests/validation/validation_data3.json_id create mode 100644 semantic-model/datamodel/tools/tests/validation/validation_data3.json_result create mode 100644 semantic-model/datamodel/tools/tests/validation/validation_schema.json create mode 100644 semantic-model/datamodel/tools/validate.js diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9833e87d..88bf1df1 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -17,4 +17,7 @@ jobs: - name: Build shacl2flink run: | sudo apt install sqlite3 sqlite3-pcre - cd semantic-model/shacl2flink && make setup && make lint test build test-kms \ No newline at end of file + cd semantic-model/shacl2flink && make setup && make lint test build test-kms + - name: Build datamodel tools + run: | + cd semantic-model/datamodel/tools && make setup && make lint test \ No newline at end of file diff --git a/semantic-model/datamodel/README.md b/semantic-model/datamodel/README.md new file mode 100644 index 00000000..14f32065 --- /dev/null +++ b/semantic-model/datamodel/README.md @@ -0,0 +1,495 @@ +# Digital Twin Datamodel + +The following motivates, descibes and defines version 0.1 of the Datamodel. It is in alpha stage and subject to changes. + +## JSON-LD (Linked Data) + +JSON-LD, which stands for "JavaScript Object Notation for Linked Data," is a powerful data serialization format that extends the capabilities of traditional JSON. Its strength lies in its ability to represent structured data in a way that is both human-readable and machine-understandable. JSON-LD is particularly well-suited for the web and semantic data integration for several key reasons: + + * **Semantic Structure:** JSON-LD allows you to add context to your data, defining the meaning of each piece of information. This semantic structure enables machines to understand the data and its relationships, fostering interoperability and knowledge sharing. + + * **Linked Data:** JSON-LD is designed to facilitate the linking of data across the web. It enables you to reference and interconnect data from various sources and domains, forming a comprehensive and coherent information ecosystem. + + * **SEO and Searchability:** Search engines like Google understand and favor JSON-LD for structuring data. Implementing JSON-LD can improve your website's visibility in search results by providing search engines with valuable information about your content. + + * **Interoperability:** JSON-LD supports the integration of data from diverse sources, making it an ideal choice for data exchange, data sharing, and data synchronization between applications, platforms, and services. + + * **Easy to Read:** JSON-LD retains the simplicity and human-readability of traditional JSON, making it accessible for both developers and non-technical users. Its natural syntax encourages widespread adoption. + + * **Standards-Based:** JSON-LD is based on W3C standards and recommendations, ensuring a well-defined and widely accepted approach to structuring and sharing linked data on the web. + +In summary, JSON-LD is a versatile and powerful tool for structuring data with semantic meaning, linking data across the web, improving search engine visibility, fostering interoperability, and promoting data exchange. It plays a crucial role in the modern web ecosystem and is a valuable asset for businesses and organizations looking to harness the full potential of their data. + +## JSON-LD Forms + +Since JSON-LD represents graph data, it can become very explicit and details. However, In many cases aspects of a graph can also be simplified and described implicitly. + +The following shows a so called *compacted* JSON-LD expression. It contains a *context* and a minimized key *name*: + +``` +{ + "@context": { + "name": "http://schema.org/name" + }, + "@id": "https://iri/max.mustermann", + "name": "Max Mustermann" +} +``` + +This is an implicit representation of a *expanded* form +``` +[{ + "@id": "https://iri/max.mustermann", + "http://schema.org/name": [{"@value": "Max Mustermann"}] +}] +``` + +Note that in the Expanded form, the *context* is missing, but everything is now provided with *namespaces* and *@value* which indicates that "Max Mustermann" is a *literal*, i.e. string, number or boolean. +The expanded form can easiliy be transformed into a *Semantic Web* graph representation: + +```meermaid +A(https://iri/max.mustermann) -- http://schema.org/name --> B("Max Mustermann") +``` + +which can also be serialized as turtle graph: + +``` +@prefix schema: . + schema:name "Max Mustermann" . +``` + +## NGSI-LD (Next Generation Service Interface for Linked Data) + +[NGSI-LD](https://www.etsi.org/deliver/etsi_gs/CIM/001_099/009/01.07.01_60/gs_CIM009v010701p.pdf) is an open standard developed by the European Telecommunications Standards Institute (ETSI) as part of the NGSI (Next Generation Service Interface) framework. It extends the capabilities of JSON-LD to enable a powerful, standardized approach to managing and exchanging context information for the Internet of Things (IoT) and smart city applications. + +Key features and concepts of NGSI-LD: + +* **Linked Data Model:** NGSI-LD is based on the principles of Linked Data, making it a part of the Semantic Web ecosystem. It allows the representation of real-world entities and their attributes as linked data resources. + + * **Entity-Attribute-Value (EAV):** NGSI-LD follows an Entity-Attribute-Value (EAV) model where entities (e.g., IoT devices or physical objects) have attributes (e.g., temperature, location) with associated values (e.g., 25°C, GPS coordinates). + + * **Context Information:** NGSI-LD is designed for sharing context information about entities. This context information can include real-time data, historical data, metadata, and relationships between entities. + +* **Interoperability:** One of the main goals of NGSI-LD is to enable interoperability between different IoT platforms, systems, and services. It provides a common data representation format and query language for IoT context information. + +* **Standardized APIs:** NGSI-LD specifies a set of standardized APIs for querying, updating, and subscribing to context information. This helps developers create applications that can work with a variety of data sources and platforms. + +* **Scalability:** NGSI-LD is designed to handle vast amounts of context information generated by IoT devices, sensors, and other sources, making it suitable for smart city and industrial IoT applications. + +* **Semantic Descriptions:** Similar to JSON-LD, NGSI-LD uses semantic descriptions (context) to define the meaning of data attributes. This enables data to be easily understood and used by both humans and machines. + +* **Real-Time Updates:** NGSI-LD supports real-time updates and notifications, making it ideal for applications that require immediate access to changing context information. + +NGSI-LD is a significant advancement in the field of IoT, as it provides a standardized approach for managing and exchanging context data, enabling more efficient and interoperable IoT solutions. It leverages the power of Linked Data to create a dynamic and interconnected IoT ecosystem. NGSI-LD is already used heavily in smart city applications + + +## NGSI-LD forms + +Since NGSI-LD is extending JSON-LD, it inherits the capability of creating different forms like *expanded* or *compacted*. In addition, it provides a simplification called *concise* form and a more explicit form, called *normalized* form. + +NGSI-LD reuqires from every entity to have at least *id* and *type*. All other data is either a *property* or a *relationship*: +``` +{ + "@context": [ + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld", + { + "@vocab": "https://industry-fusion.org/base/v0.1/" + } + ], + "id": "urn:x:1", + "type": "cutter", + "hasFilter": { + "type": "Relationship", + "object": "urn:filter:1" + }, + "machine_state": { + "type": "Property", + "value": "Testing" + } +} + +``` + +The *type* field is here redundant, that is why NGSI-LD defines a *concise* form which is reducing redudancy: + +``` +{ + "@context": [ + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld", + { + "@vocab": "https://industry-fusion.org/base/v0.1/" + } + ], + "id": "urn:x:1", + "type": "cutter", + "hasFilter": { + "object": "urn:filter:1" + }, + "machine_state": "Testing" +} +``` + +Since NGSI-LD is JSON-LD compliant, it can be *compacted* and *extended*. Note that both forms, *normalized* and *concise* NGSI-LD, are already *compacted* JSON-LD forms. The expanded *normalized* form of the above example looks like + +``` +[ + { + "https://industry-fusion.org/base/v0.1/hasFilter": [ + { + "https://uri.etsi.org/ngsi-ld/hasObject": [ + { + "@id": "urn:filter:1" + } + ], + "@type": [ + "https://uri.etsi.org/ngsi-ld/Relationship" + ] + } + ], + "@id": "urn:x:1", + "https://industry-fusion.org/base/v0.1/machine_state": [ + { + "@type": [ + "https://uri.etsi.org/ngsi-ld/Property" + ], + "https://uri.etsi.org/ngsi-ld/hasValue": [ + { + "@value": "Testing" + } + ] + } + ], + "@type": [ + "https://industry-fusion.org/base/v0.1/cutter" + ] + } +] +``` + +## Validation with JSON-Schema and SHACL + +Validation of JSON objects is typically done with [JSON-Schema](https://json-schema.org/). A plain JSON object structure can therefore be validated. However, as described above, JSON-LD represent a graph and has different forms. For instance, the following two expressions are equivalent in JSON-LD but cannot be schemed with JSON-Schema: + +Expression 1 + +``` +[{ + "@context": [ + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld", + { + "@vocab": "https://industry-fusion.org/base/v0.1/" + } + ], + "id": "urn:x:1", + "type": "cutter", + "hasFilter": { + "type": "Relationship", + "object": "urn:filter:1" + }, + "machine_state": { + "type": "Property", + "value": "Testing" + } +}, +{ + "@context": [ + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld", + { + "@vocab": "https://industry-fusion.org/base/v0.1/" + } + ], + "id": "urn:y:1", + "type": "filter", + "machine_state": { + "type": "Property", + "value": "Testing" + } +} +] +``` + +Expression 2 + +``` +{ + "@context": [ + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld", + { + "@vocab": "https://industry-fusion.org/base/v0.1/" + } + ], + "id": "urn:x:1", + "type": "cutter", + "hasFilter": { + "type": "Relationship", + "object": { + "id": "urn:y:1", + "type": "filter", + "machine_state": { + "type": "Property", + "value": "Testing" + } + } + }, + "machine_state": { + "type": "Property", + "value": "Testing" + } +} +``` + +In addition, JSON-schema is not able to properly process *Namespaces*. + +Therefore, a proper validation must consider the Graph structure of JSON-LD. A standard, which allows to define Constraints within a Graph is [SHACL](https://www.w3.org/TR/shacl/). + + +## JSON-LD Validation with JSON-Schema + +As shown in the last section, it is impossible to use JSON-Schema to validate JSON-LD objects properly. However, as a compromise, many non-linked data related attributes can be validated if one is applying a propoer normalization. Therefore, we use the JSON-Schema in the following to validate a *concise* NGSI-LD form with a pre-defined *context*. + +In the following, we describe the validation schema. +We use the default *contex* https://industryfusion.github.io/contexts/v0.1/context.jsonld. An example for a *concise* form with this *conext* can be seen in the following. It contains an *ECLASS* type, one *ECLASS* property and attributes (properties and relationships) `machine_state` and `hasFilter` from the default vocabulary. The object has an ID expressed as URN: + +``` +{ + "@context": "https://industryfusion.github.io/contexts/v0.1/context.jsonld", + "machine_state": "Testing", + "hasFilter": { + "object": "urn:filter:1" + }, + "eclass:0173-1#02-AAH880#003": "10", + "id": "urn:x:1", + "type": "eclass:0173-1#01-AKJ975#017" +} +``` + +In order to validate it with a JSON-Schema, first the base object must be described: + +``` + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "", + "title": "Plasmacutter", + "description": "Plasmacutter template for IFF", + "type": "object", + "properties": { + "type": { + "const": "" + }, + "id": { + "type": "string", + "pattern": "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{1,31}:([a-zA-Z0-9()+,.:=@;$_!*'-]|%[0-9a-fA-F]{2})*$" + } + }, + "required": ["type", "id"], + "allOf": [] + } +``` +This specifies the mandatory `id` and `type` field of every NGSI-LD object. `id` must be an *URN* and `type` must contain a *compacted* form. +The "$schema" field must be "https://json-schema.org/draft/2020-12/schema", the $id of the schema must be a valid URL with the additional constraint that all '#' fields must be URL-Encoded. An example for a schema and related data can be seen in the following: + +``` +# JSON-Schema: +[{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/eclass%230173-1%2301-AKJ975%23017", + "title": "Cutter", + "description": "Cutter template for IFF", + "type": "object", + "properties": { + "type": { + "const": "eclass:0173-1#01-AKJ975#017" + }, + "id": { + "type": "string", + "pattern": "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{1,31}:([a-zA-Z0-9()+,.:=@;$_!*'-]|%[0-9a-fA-F]{2})*$" + } + }, + "required": ["type", "id"] +}] + +# JSON-LD Object +{ + "@context": "https://industryfusion.github.io/contexts/v0.1/context.jsonld", + "id": "urn:iff:abc123", + "type": "eclass:0173-1#01-AKJ975#017" +} +``` + +The *properties* and *relationships* can be grouped and aggregated by the `allOf` array. In the following a *string* property is validated by a schema: + +``` +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/base-objects/v0.1/cutter/properties", + "title": "Cutter properties", + "description": "Properties for class cutter", + "type": "object", + "properties": { + "machine_state": { + "type": "string", + "title": "Machine Status", + "description": "Current status of the machine (Online_Idle, Run, Online_Error, Online_Maintenance, Setup, Testing)", + "enum": [ + "Online_Idle", + "Run", + "Online_Error", + "Online_Maintenance", + "Setup", + "Testing" + ] + } + } +} +``` +The following validates a *relationship*: + + +``` + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/base-objects/v0.1/cutter/relationships", + "title": "IFF template for cutter relationship", + "description": "Cutter template for IFF", + "type": "object", + "properties": { + "hasFilter": { + "relationship": "eclass:0173-1#01-ACK991#016", + "$ref": "https://industry-fusion.org/base-objects/v0.1/link" + } + } + }, + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/base-objects/v0.1/link", + "title": "IFF template for cutter relationship", + "description": "Cutter template for IFF", + "type": "object", + "properties": { + "object": { + "type": "string", + "pattern": "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{0,31}:[a-zA-Z0-9()+,\\-.:=@;$_!*']*[a-zA-Z0-9()+,\\-.:=@;$_!*']$" + } + }, + "required": ["object"] + }, + +``` +Overall, the resulting full JSON-Schema looks like follows: + +``` +# JSON-Schema: +[{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/eclass%230173-1%2301-AKJ975%23017", + "title": "Cutter", + "description": "Cutter template for IFF", + "type": "object", + "properties": { + "type": { + "const": "eclass:0173-1#01-AKJ975#017" + }, + "id": { + "type": "string", + "pattern": "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{1,31}:([a-zA-Z0-9()+,.:=@;$_!*'-]|%[0-9a-fA-F]{2})*$" + } + }, + "allOf": [ + { + "$ref": "https://industry-fusion.org/base-objects/v0.1/cutter/properties" + }, + { + "$ref": "https://industry-fusion.org/base-objects/v0.1/cutter/relationships" + } + ], + "required": ["type", "id"] +}] + +# JSON-LD Object +{ + "@context": "https://industryfusion.github.io/contexts/v0.1/context.jsonld", + "id": "urn:iff:abc123", + "type": "eclass:0173-1#01-AKJ975#017", + "hasFilter": { + "object": "urn:iff:filter:1" + }, + "machine_state": "Testing" +} + +``` + +## Forbidden JSON-Schema keywords: +The following JSON-Schema Keywords from the standard are forbidded: + +* anyOf, oneOf +* if, then, else +* prefixItems, items +* valid, error, annotations +* additionalProperties +* propertyNames +* $vocabulary, $defs +* multipleOf +* uniqueItems +* maxContains, minContains +* maxProperties, minProperties +* dependentRequired + +## Added JSON-Schmea Keywords: +The following JSON-Schema Keywords are added to the standard: + +* **relationship:** Contains the *compacted* type for a NGSI-LD relationship. + +## Integrating ECLASS Properties +`ECLASS` provides additional data for every `IRDI` which can be added/mapped to `JSON-Schema`: + +- `Preferred Name` is mapped to `title` field +- `Definition` is mapped to `description` field +- The unit symbol of `Unit` is mapped to `unit` field +- `Type` of the field is mapped to `datatype` and xsd-type as described [here](https://eclass.eu/support/technical-specification/data-model/datatype-to-xsd-mapping) +- The `JSON` `type` field of every `ECLASS` property is `string`. + +For [example](./schema-ngsild-eclass/schema.json), the `ECLASS` property `` is described in the `JSON-Schema` as: + +``` + "eclass:0173-1#02-AAH880#003": { + "type": "string", + "datatype": "double", + "title": "min. cutting current", + "description": "specification of the minimum cutting current", + "unit": "A" + } +``` + +## Translating JSON-Schema to SHACL + +## Tools +This section describes the tools which are used for validation, data conversion and SHACL creation. The tools can be found in the `./tools` + +### Validation + +The validation tool is `validate.js`. + +#### Install + +``` +npm install +``` + +#### Usage + +``` +Optionen: + --version Version anzeigen [boolean] + -s, --schema Schema File [string] [erforderlich] + -d, --datafile File to validate [string] [erforderlich] + -i, --schemaid Schema-id to validate [string] [erforderlich] + -h, --help Hilfe anzeigen [boolean] +``` + +#### Examples + +`node tools/validate.js -s examples/plasmacutter_schema.json -d examples/plasmacutter_data.json -i https://industry-fusion.org/eclass#0173-1#01-AKJ975#017` + +### Convert JSON-Schema to SHACL + +### Convert NGSI-LD forms \ No newline at end of file diff --git a/semantic-model/datamodel/examples/plasmacutter_data.json b/semantic-model/datamodel/examples/plasmacutter_data.json new file mode 100644 index 00000000..799eb2dd --- /dev/null +++ b/semantic-model/datamodel/examples/plasmacutter_data.json @@ -0,0 +1,9 @@ +{ + "@context": "https://industryfusion.github.io/contexts/v0.1/context.jsonld", + "id": "urn:iff:abc123", + "type": "eclass:0173-1#01-AKJ975#017", + "hasFilter": { + "object": "urn:iff:filter:1" + }, + "machine_state": "Testing" +} diff --git a/semantic-model/datamodel/examples/plasmacutter_schema.json b/semantic-model/datamodel/examples/plasmacutter_schema.json new file mode 100644 index 00000000..ebc29cba --- /dev/null +++ b/semantic-model/datamodel/examples/plasmacutter_schema.json @@ -0,0 +1,79 @@ +[ + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/eclass%230173-1%2301-AKJ975%23017", + "title": "Cutter", + "description": "Cutter template for IFF", + "type": "object", + "properties": { + "type": { + "const": "eclass:0173-1#01-AKJ975#017" + }, + "id": { + "type": "string", + "pattern": "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{1,31}:([a-zA-Z0-9()+,.:=@;$_!*'-]|%[0-9a-fA-F]{2})*$" + } + }, + "required": ["type", "id"], + "allOf": [ + { + "$ref": "https://industry-fusion.org/base-objects/v0.1/cutter/properties" + }, + { + "$ref": "https://industry-fusion.org/base-objects/v0.1/cutter/relationships" + } + ] + }, + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/base-objects/v0.1/cutter/relationships", + "title": "IFF template for cutter relationship", + "description": "Cutter template for IFF", + "type": "object", + "properties": { + "hasFilter": { + "relationship": "eclass:0173-1#01-ACK991#016", + "$ref": "https://industry-fusion.org/base-objects/v0.1/link" + } + } + }, + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/base-objects/v0.1/link", + "title": "IFF template for cutter relationship", + "description": "Cutter template for IFF", + "type": "object", + "properties": { + "object": { + "type": "string", + "pattern": "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{0,31}:[a-zA-Z0-9()+,\\-.:=@;$_!*']*[a-zA-Z0-9()+,\\-.:=@;$_!*']$" + } + }, + "required": ["object"] + }, + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/base-objects/v0.1/cutter/properties", + "title": "Cutter properties", + "description": "Properties for class cutter", + "type": "object", + "properties": { + "machine_state": { + "type": "string", + "title": "Machine Status", + "description": "Current status of the machine (Online_Idle, Run, Online_Error, Online_Maintenance, Setup, Testing)", + "enum": [ + "Online_Idle", + "Run", + "Online_Error", + "Online_Maintenance", + "Setup", + "Testing" + ] + } + }, + "required": [ + "machine_state" + ] + } +] \ No newline at end of file diff --git a/semantic-model/datamodel/examples/simple_plasmacutter2_data.json b/semantic-model/datamodel/examples/simple_plasmacutter2_data.json new file mode 100644 index 00000000..40072f30 --- /dev/null +++ b/semantic-model/datamodel/examples/simple_plasmacutter2_data.json @@ -0,0 +1,5 @@ +{ + "@context": "https://industryfusion.github.io/contexts/v0.1/context.jsonld", + "id": "urn:iff:abc123", + "type": "Plasmacutter" +} diff --git a/semantic-model/datamodel/examples/simple_plasmacutter2_schema.json b/semantic-model/datamodel/examples/simple_plasmacutter2_schema.json new file mode 100644 index 00000000..35c93de9 --- /dev/null +++ b/semantic-model/datamodel/examples/simple_plasmacutter2_schema.json @@ -0,0 +1,17 @@ +[{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/base/v0.1/Plasmacutter", + "title": "Cutter", + "description": "Cutter template for IFF", + "type": "object", + "properties": { + "type": { + "const": "plasmacutter" + }, + "id": { + "type": "string", + "pattern": "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{1,31}:([a-zA-Z0-9()+,.:=@;$_!*'-]|%[0-9a-fA-F]{2})*$" + } + }, + "required": ["type", "id"] +}] \ No newline at end of file diff --git a/semantic-model/datamodel/examples/simple_plasmacutter_data.json b/semantic-model/datamodel/examples/simple_plasmacutter_data.json new file mode 100644 index 00000000..10da1870 --- /dev/null +++ b/semantic-model/datamodel/examples/simple_plasmacutter_data.json @@ -0,0 +1,5 @@ +{ + "@context": "https://industryfusion.github.io/contexts/v0.1/context.jsonld", + "id": "urn:iff:abc123", + "type": "eclass:0173-1#01-AKJ975#017" +} diff --git a/semantic-model/datamodel/examples/simple_plasmacutter_schema.json b/semantic-model/datamodel/examples/simple_plasmacutter_schema.json new file mode 100644 index 00000000..72149bdd --- /dev/null +++ b/semantic-model/datamodel/examples/simple_plasmacutter_schema.json @@ -0,0 +1,17 @@ +[{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/eclass%230173-1%2301-AKJ975%23017", + "title": "Cutter", + "description": "Cutter template for IFF", + "type": "object", + "properties": { + "type": { + "const": "eclass:0173-1#01-AKJ975#017" + }, + "id": { + "type": "string", + "pattern": "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{1,31}:([a-zA-Z0-9()+,.:=@;$_!*'-]|%[0-9a-fA-F]{2})*$" + } + }, + "required": ["type", "id"] +}] \ No newline at end of file diff --git a/semantic-model/datamodel/tools/.eslintrc.js b/semantic-model/datamodel/tools/.eslintrc.js new file mode 100644 index 00000000..0e3df72f --- /dev/null +++ b/semantic-model/datamodel/tools/.eslintrc.js @@ -0,0 +1,26 @@ +module.exports = { + env: { + browser: true, + commonjs: true, + es2021: true + }, + extends: 'standard', + overrides: [ + { + env: { + node: true + }, + files: [ + '.eslintrc.{js,cjs}' + ], + parserOptions: { + sourceType: 'script' + } + } + ], + parserOptions: { + ecmaVersion: 'latest' + }, + rules: { + } +} diff --git a/semantic-model/datamodel/tools/Makefile b/semantic-model/datamodel/tools/Makefile new file mode 100644 index 00000000..c62793ec --- /dev/null +++ b/semantic-model/datamodel/tools/Makefile @@ -0,0 +1,30 @@ +# +# Copyright (c) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +setup: + pip3 install pyshacl + npm install + +lint: + npm run lint + shellcheck tests/*/test.sh + +test: + npm run test + cd tests/validation && bash ./test.sh + cd tests/jsonld2jsonld && bash ./test.sh + cd tests/schema2shacl && bash ./test.sh \ No newline at end of file diff --git a/semantic-model/datamodel/tools/jsonldConverter.js b/semantic-model/datamodel/tools/jsonldConverter.js new file mode 100644 index 00000000..60d1b77c --- /dev/null +++ b/semantic-model/datamodel/tools/jsonldConverter.js @@ -0,0 +1,133 @@ +/** +* Copyright (c) 2023 Intel Corporation +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +'use strict' + +const fs = require('fs') +const yargs = require('yargs') +const jsonld = require('jsonld') +const jsonldUtils = require('./lib/jsonldUtils') +let jsonFileName +const argv = yargs + .option('concise', { + alias: 'n', + description: 'Create concise/compacted from', + demandOption: false, + type: 'boolean' + }) + .option('expand', { + alias: 'x', + description: 'Create expanded from', + demandOption: false, + type: 'boolean' + }) + .option('normalize', { + alias: 'r', + description: 'Create normalized from', + demandOption: false, + type: 'boolean' + }) + .option('context', { + alias: 'c', + description: 'JSON-LD-Context', + demandOption: false, + type: 'string' + }) + .command({ + command: '$0 ', + describe: 'Convert a JSON-LD file into different normal forms.', + handler: (argv) => { + const { filename } = argv + jsonFileName = filename + } + }) + .help() + .alias('help', 'h') + .argv + +const jsonText = fs.readFileSync(jsonFileName, 'utf8') +const jsonObj = JSON.parse(jsonText) +let jsonArr + +if (!(argv.x === undefined) && !(argv.n === undefined)) { + console.error('Expand and Concise are mutally exclusive. Bye!') + process.exit(1) +} +if (!(argv.r === undefined) && !(argv.n === undefined)) { + console.error('Normalized and Concise are mutally exclusive. Bye!') + process.exit(1) +} +if (!(argv.x === undefined) && !(argv.r === undefined)) { + console.error('Normalized and Expanded are mutally exclusive. Bye!') + process.exit(1) +} +if (argv.x === undefined && argv.r === undefined && argv.n === undefined) { + console.error('No processing switch selected. Bye!') + process.exit(1) +} + +if (!Array.isArray(jsonObj)) { + jsonArr = [jsonObj] +} else { + jsonArr = jsonObj +} + +async function expand (objArr, contextArr) { + const expanded = await Promise.all(objArr.map(async (jsonObj, index) => { + jsonObj['@context'] = contextArr[index] + const res = await jsonld.expand(jsonObj) + return res[0] + })) + return expanded +} + +async function compact (objArr, contextArr) { + return await Promise.all(objArr.map(async (jsonObj, index) => jsonld.compact(jsonObj, contextArr[index]))) +} + +(async (jsonArr) => { + if (!(argv.n === undefined)) { + const mergedContexts = jsonldUtils.mergeContexts(jsonArr, argv.c) + if (mergedContexts !== undefined && mergedContexts.find(x => x === null)) { + console.error('Error: For Compaction, context must be either defined in all objects or externally. Exiting!') + process.exit(1) + } + // Compaction to find Properties in compacted form + const expanded = await expand(jsonArr, mergedContexts) + const concised = jsonldUtils.conciseExpandedForm(expanded) + const compacted = await compact(concised, mergedContexts) + console.log(JSON.stringify(compacted, null, 2)) + } + if (!(argv.x === undefined)) { + const mergedContexts = jsonldUtils.mergeContexts(jsonArr, argv.c) + if (mergedContexts !== undefined && mergedContexts.find(x => x === null)) { + console.error('Error: For Extraction, context must be either defined in all objects or externally. Exiting!') + process.exit(1) + } + const expanded = await expand(jsonArr, mergedContexts) + console.log(JSON.stringify(expanded, null, 2)) + } + if (!(argv.r === undefined)) { + const mergedContexts = jsonldUtils.mergeContexts(jsonArr, argv.c) + if (mergedContexts !== undefined && mergedContexts.find(x => x === null)) { + console.error('Error: For Normalization, context must be either defined in all objects or externally. Exiting!') + process.exit(1) + } + const expanded = await expand(jsonArr, mergedContexts) + const normalized = jsonldUtils.normalizeExpandedForm(expanded) + const compacted = await compact(normalized, mergedContexts) + console.log(JSON.stringify(compacted, null, 2)) + } +})(jsonArr) diff --git a/semantic-model/datamodel/tools/jsonschema2shacl.js b/semantic-model/datamodel/tools/jsonschema2shacl.js new file mode 100644 index 00000000..93dc8c79 --- /dev/null +++ b/semantic-model/datamodel/tools/jsonschema2shacl.js @@ -0,0 +1,83 @@ +/** +* Copyright (c) 2023 Intel Corporation +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +'use strict' + +const $RefParser = require('json-schema-ref-parser') +const fs = require('fs') +const yargs = require('yargs') +const ShaclUtils = require('./lib/shaclUtils') + +const argv = yargs + .command('$0', 'Converting an IFF Schema file for NGSI-LD objects into a SHACL constraint.') + .option('schema', { + alias: 's', + description: 'Schema File containing array of Schemas', + demandOption: true, + type: 'string' + }) + .option('schemaid', { + alias: 'i', + description: 'Schma-id of object to generate SHACL for', + demandOption: true, + type: 'string' + }) + .option('context', { + alias: 'c', + description: 'JSON-LD-Context', + demandOption: true, + type: 'string' + }) + .help() + .alias('help', 'h') + .argv + +// Read in an array of JSON-Schemas +const jsonSchemaText = fs.readFileSync(argv.s, 'utf8') +const jsonSchema = JSON.parse(jsonSchemaText); + +(async (jsconSchema) => { + const myResolver = { + order: 1, + + canRead: function (file) { + return true + }, + + read: function (file, callback, $refs) { + return jsonSchema.find((schema) => schema.$id === file.url) + } + } + const options = { + resolve: { + file: false, + http: false, + test: myResolver + } + } + try { + const schema = await $RefParser.dereference(jsonSchema, options) + return schema + } catch (err) { + console.error(err) + } +})(jsonSchema) + .then(async (schema) => { + await ShaclUtils.loadContext(argv.c) + return schema + }) + .then(schema => { + ShaclUtils.shaclize(schema, argv.i) + }) diff --git a/semantic-model/datamodel/tools/lib/jsonldUtils.js b/semantic-model/datamodel/tools/lib/jsonldUtils.js new file mode 100644 index 00000000..bd4a503d --- /dev/null +++ b/semantic-model/datamodel/tools/lib/jsonldUtils.js @@ -0,0 +1,140 @@ + +/** +* Copyright (c) 2023 Intel Corporation +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +'use strict' +const url = require('url') +const fs = require('fs') + +function loadContextFromFile (fileName) { + const context = fs.readFileSync(fileName, 'utf8') + const contextParsed = JSON.parse(context) + return contextParsed +} + +/** + * Merge local context from jsonld and external given context + * into a joint array + * @param {object} jsonArr + * @param {array or string} context + * @returns mergedContext + */ +function mergeContexts (jsonArr, context) { + function mergeContext (localContext, context) { + let mergedContext = [] + if (!Array.isArray(localContext) && localContext !== undefined) { + mergedContext = [localContext] + } + if (context === undefined) { + if (mergedContext.length === 0) { + return null + } + return mergedContext + } else if (!Array.isArray(context)) { + context = [context] + } + context.forEach(c => { + if (typeof (c) !== 'string' || mergedContext.find(x => c === x) === undefined) { + mergedContext.push(c) + } + }) + return mergedContext + } + if (context !== undefined) { + const parseContextUrl = new url.URL(context) + if (parseContextUrl.protocol === 'file:') { + context = loadContextFromFile(parseContextUrl.pathname) + } + } + if (jsonArr === undefined) { + return mergeContext(undefined, context) + } + return jsonArr.map(jsonObj => { + const localContext = jsonObj['@context'] + return mergeContext(localContext, context) + }) +} + +/** + * Expects NGSI-LD object in expanded form and transforms to NGSI-LD concise form + * @param {object} expanded + * @returns concise and expanded form + */ +function conciseExpandedForm (expanded) { + function filterAttribute (attr) { + if (typeof (attr) === 'object') { + if ('@type' in attr && (attr['@type'][0] === 'https://uri.etsi.org/ngsi-ld/Property' || + attr['@type'][0] === 'https://uri.etsi.org/ngsi-ld/Relationship')) { + delete attr['@type'] + } + if ('https://uri.etsi.org/ngsi-ld/hasValue' in attr) { + attr['@value'] = attr['https://uri.etsi.org/ngsi-ld/hasValue'][0]['@value'] + delete attr['https://uri.etsi.org/ngsi-ld/hasValue'] + } + } + } + expanded.forEach(c => { + Object.keys(c).forEach(key => { + if (Array.isArray(c[key])) { + c[key].forEach(a => filterAttribute(a)) + } else { + filterAttribute(c[key]) + } + }) + }) + return expanded +} + +/** + * Expects NGSI-LD object in expanded form and transforms to NGSI-LD normalized + * @param {object} expanded + * @returns normalized NGSI-LD and expanded form + */ +function normalizeExpandedForm (expanded) { + function extendAttribute (attr) { + if (typeof (attr) === 'object') { + if (!('@type' in attr)) { + if ('https://uri.etsi.org/ngsi-ld/hasValue' in attr || '@value' in attr || '@id' in attr) { + attr['@type'] = ['https://uri.etsi.org/ngsi-ld/Property'] + } else if ('https://uri.etsi.org/ngsi-ld/hasObject' in attr) { + attr['@type'] = ['https://uri.etsi.org/ngsi-ld/Relationship'] + } + if ('@value' in attr) { + attr['https://uri.etsi.org/ngsi-ld/hasValue'] = attr['@value'] + delete attr['@value'] + } else if ('@id' in attr) { + attr['https://uri.etsi.org/ngsi-ld/hasValue'] = { '@id': attr['@id'] } + delete attr['@id'] + } + } + } + } + expanded.forEach(c => { + Object.keys(c).forEach(key => { + if (Array.isArray(c[key])) { + c[key].forEach(a => extendAttribute(a)) + } else { + extendAttribute(c[key]) + } + }) + }) + return expanded +} + +module.exports = { + mergeContexts, + conciseExpandedForm, + normalizeExpandedForm +} diff --git a/semantic-model/datamodel/tools/lib/shaclUtils.js b/semantic-model/datamodel/tools/lib/shaclUtils.js new file mode 100644 index 00000000..fe3a7717 --- /dev/null +++ b/semantic-model/datamodel/tools/lib/shaclUtils.js @@ -0,0 +1,259 @@ +/** +* Copyright (c) 2023 Intel Corporation +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +'use strict' +const $rdf = require('rdflib') +const ContextUtil = require('jsonld-context-parser').Util +const url = require('url') +const fs = require('fs') +const path = require('path') +const ContextParser = require('jsonld-context-parser').ContextParser +const myParser = new ContextParser() + +const RDF = $rdf.Namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#') +const SHACL = $rdf.Namespace('http://www.w3.org/ns/shacl#') +const IFFK = $rdf.Namespace('https://industry-fusion.org/knowledge/v0.1/') + +let globalContext +let globalPrefixHash + +class NodeShape { + constructor (targetClass) { + this.targetClass = targetClass + this.properties = [] + } + + addPropertyShape (propertyShape) { + this.properties.push(propertyShape) + } + + get properties () { + return this._properties + } + + set properties (prop) { + this._properties = prop + } +} + +class PropertyShape { + constructor (mincount, maxcount, nodeKind, path, isProperty) { + this.mincount = mincount + this.maxcount = maxcount + this.nodeKind = nodeKind + this.path = path + this.constraints = [] + this.isProperty = isProperty + } + + addConstraint (constraint) { + this.constraints.push(constraint) + } + + set propertyNode (node) { + this._propertyNode = node + } + + get propertyNode () { + return this._propertyNode + } +} + +class Constraint { + constructor (type, params) { + this.type = type + this.params = params + } +} + +function dumpPropertyShape (propertyShape, store) { + const propNode = propertyShape.propertyNode + store.add(propNode, SHACL('minCount'), propertyShape.mincount) + store.add(propNode, SHACL('maxCount'), propertyShape.maxcount) + store.add(propNode, SHACL('nodeKind'), SHACL('BlankNode')) + store.add(propNode, SHACL('path'), propertyShape.path) + const attributeNode = $rdf.blankNode() + store.add(propNode, SHACL('property'), attributeNode) + const ngsildPrefix = globalPrefixHash['ngsi-ld'] + const NGSILD = $rdf.Namespace(ngsildPrefix) + if (propertyShape.isProperty) { + store.add(attributeNode, SHACL('path'), NGSILD('hasValue')) + } else { + store.add(attributeNode, SHACL('path'), NGSILD('hasObject')) + } + store.add(attributeNode, SHACL('minCount'), 1) + store.add(attributeNode, SHACL('maxCount'), 1) + store.add(attributeNode, SHACL('nodeKind'), propertyShape.nodeKind) + const constraints = propertyShape.constraints + constraints.forEach((constraint) => { + store.add(attributeNode, constraint.type, constraint.params) + }) +} + +function dumpNodeShape (nodeShape, store) { + const nodeName = decodeURIComponent(globalContext.expandTerm(nodeShape.targetClass)) + const parsedUrl = new url.URL(nodeName) + let shapeNamePrefix = parsedUrl.hash.substring(1) + const pathname = parsedUrl.pathname + if (shapeNamePrefix === '') { + shapeNamePrefix = path.basename(pathname) + } + const shapeName = shapeNamePrefix + 'Shape' + store.add(IFFK(shapeName), RDF('type'), SHACL('NodeShape')) + store.add(IFFK(shapeName), SHACL('targetClass'), $rdf.sym(nodeName)) + nodeShape.properties.forEach((property) => { + const propNode = $rdf.blankNode() + property.propertyNode = propNode + store.add(IFFK(shapeName), SHACL('property'), propNode) + dumpPropertyShape(property, store) + }) +} + +function scanNodeShape (typeschema) { + const id = typeschema.$id + + const nodeShape = new NodeShape(id) + scanProperties(nodeShape, typeschema) + return nodeShape +} + +function scanProperties (nodeShape, typeschema) { + let required = [] + if ('required' in typeschema) { + required = typeschema.required + } + if ('properties' in typeschema) { + Object.keys(typeschema.properties).forEach( + (property) => { + if (property === 'type' || property === 'id') { + return + } + let nodeKind = SHACL('Literal') + let klass = null + let isProperty = true + if ('relationship' in typeschema.properties[property]) { + nodeKind = SHACL('IRI') + klass = typeschema.properties[property].relationship + klass = globalContext.expandTerm(klass, true) + isProperty = false + } + let mincount = 0 + const maxcount = 1 + if (required.includes(property)) { + mincount = 1 + } + let path = property + if (!ContextUtil.isValidIri(path)) { + path = globalContext.expandTerm(path, true) + } + const propertyShape = new PropertyShape(mincount, maxcount, nodeKind, $rdf.sym(path), isProperty) + nodeShape.addPropertyShape(propertyShape) + if (klass !== null) { + propertyShape.addConstraint(new Constraint(SHACL('class'), $rdf.sym(klass))) + } + scanConstraints(propertyShape, typeschema.properties[property]) + }) + } + if ('allOf' in typeschema) { + typeschema.allOf.forEach((elem) => { + scanProperties(nodeShape, elem) + }) + } +} + +function scanConstraints (propertyShape, typeschema) { + if ('enum' in typeschema) { + propertyShape.addConstraint(new Constraint(SHACL('in'), typeschema.enum)) + } + if ('datatype' in typeschema) { + // datatype constraints are not used actively. It is not testing the value but only checks if the formal + // datatype "tag" conforms + // propertyShape.addConstraint(new Constraint(SHACL('datatype'), typeschema.datatype)) + } + if ('maximum' in typeschema) { + propertyShape.addConstraint(new Constraint(SHACL('maxInclusive'), typeschema.maximum)) + } + if ('minimum' in typeschema) { + propertyShape.addConstraint(new Constraint(SHACL('minInclusive'), typeschema.minimum)) + } + if ('exclusiveMinimum' in typeschema) { + propertyShape.addConstraint(new Constraint(SHACL('minExclusive'), typeschema.exclusiveMinimum)) + } + if ('exclusiveMaximum' in typeschema) { + propertyShape.addConstraint(new Constraint(SHACL('maxExclusive'), typeschema.exclusiveMaximum)) + } + if ('maxLength' in typeschema) { + propertyShape.addConstraint(new Constraint(SHACL('maxLength'), typeschema.maxLength)) + } + if ('minLength' in typeschema) { + propertyShape.addConstraint(new Constraint(SHACL('minLength'), typeschema.minLength)) + } +} + +function shaclize (schemas, id) { + id = encodeHash(id) + const store = new $rdf.IndexedFormula() + const typeschema = schemas.find((schema) => schema.$id === id) + const nodeShape = scanNodeShape(typeschema) + dumpShacl(nodeShape, store) + const serializer = new $rdf.Serializer(store) + serializer.setFlags('u') + serializer.setNamespaces(globalPrefixHash) + const turtle = serializer.statementsToN3(store.statementsMatching(undefined, undefined, undefined, undefined)) + console.log(turtle) +} + +function encodeHash (id) { + const url = new URL(id) + const hash = encodeURIComponent(url.hash) + return `${url.protocol}//${url.hostname}${url.pathname}${hash}` +} + +function dumpShacl (nodeShape, store) { + dumpNodeShape(nodeShape, store) +} + +async function loadContext (uriOrContext) { + const parseUrl = new url.URL(uriOrContext) + if (parseUrl.protocol === 'file:') { + uriOrContext = JSON.parse(fs.readFileSync(parseUrl.pathname, 'utf-8')) + } + const context = await myParser.parse(uriOrContext) + globalContext = context + const prefixHash = {} + Object.keys(context.getContextRaw()).filter((key) => key !== '@vocab').forEach((key) => { + const value = context.getContextRaw()[key] + if (typeof value === 'string') { + if (ContextUtil.isPrefixIriEndingWithGenDelim(value)) { + prefixHash[key] = value + } + } else if (typeof value === 'object') { + if (ContextUtil.isPrefixIriEndingWithGenDelim(value['@id'])) { + prefixHash[key] = value['@id'] + } + } + }) + globalPrefixHash = prefixHash +} + +module.exports = { + NodeShape, + PropertyShape, + Constraint, + scanNodeShape, + dumpNodeShape, + shaclize, + loadContext +} diff --git a/semantic-model/datamodel/tools/package.json b/semantic-model/datamodel/tools/package.json new file mode 100644 index 00000000..a4a6d39c --- /dev/null +++ b/semantic-model/datamodel/tools/package.json @@ -0,0 +1,34 @@ +{ + "name": "pdt-validation", + "version": "0.1.0", + "description": "This package contains tools for validating NGSI-LD data for the Process Data Twin (PDT)", + "main": "validate.js", + "scripts": { + "test": "./node_modules/.bin/nyc --lines 80 --check-coverage ./node_modules/.bin/mocha tests/*.js", + "lint": "./node_modules/.bin/eslint *.js" + }, + "author": "Marcel Wagner", + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.12.0", + + "json-schema-ref-parser": "^9.0.9", + "jsonld-context-parser": "^2.4.0", + + + "rdflib": "^2.2.32", + + "yargs": "^17.7.2" + }, + "devDependencies": { + "rewire": "^7.0.0", + "nyc": "^15.1.0", + "mocha": "^10.2.0", + "chai": "^4.3.10", + "eslint": "^8.53.0", + "eslint-config-standard": "^17.1.0", + "eslint-plugin-import": "^2.29.0", + "eslint-plugin-n": "^16.2.0", + "eslint-plugin-promise": "^6.1.1" + } +} diff --git a/semantic-model/datamodel/tools/tests/jsonld2jsonld/c0 b/semantic-model/datamodel/tools/tests/jsonld2jsonld/c0 new file mode 100644 index 00000000..6d7e122e --- /dev/null +++ b/semantic-model/datamodel/tools/tests/jsonld2jsonld/c0 @@ -0,0 +1,38 @@ +[ + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld", + { + "@vocab": "https://industry-fusion.org/base/v0.1/", + "eclass": { + "@id": "https://industry-fusion.org/eclass#", + "@prefix": true + }, + "xsd": { + "@id": "http://www.w3.org/2001/XMLSchema#", + "@prefix": true + }, + "iffb": { + "@id": "https://industry-fusion.org/base/v0.1/", + "@prefix": true + }, + "iffk": { + "@id": "https://industry-fusion.org/knowledge/v0.1/", + "@prefix": true + }, + "rdf": { + "@id": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "@prefix": true + }, + "rdfs": { + "@id": "http://www.w3.org/2000/01/rdf-schema#", + "@prefix": true + }, + "schema": { + "@id": "http://schema.org/", + "@prefix": true + }, + "sh": { + "@id": "http://www.w3.org/ns/shacl#", + "@prefix": true + } + } +] diff --git a/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload1.jsonld b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload1.jsonld new file mode 100644 index 00000000..94610b65 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload1.jsonld @@ -0,0 +1,13 @@ +{ + "machine_state": "Testing", + "machine_state_from_smartbox": "Offline_Idle", + "hasFilter": { + "object": "urn:filter:1" + }, + "testiri": { + "@id": "iffk:testiri" + }, + "eclass:0173-1#02-AAH880#003": "10", + "id": "urn:x:1", + "type": "eclass:0173-1#01-AKJ975#017" +} diff --git a/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload1.jsonld_n_c0 b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload1.jsonld_n_c0 new file mode 100644 index 00000000..b6d02f84 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload1.jsonld_n_c0 @@ -0,0 +1,53 @@ +[ + { + "@context": [ + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld", + { + "@vocab": "https://industry-fusion.org/base/v0.1/", + "eclass": { + "@id": "https://industry-fusion.org/eclass#", + "@prefix": true + }, + "xsd": { + "@id": "http://www.w3.org/2001/XMLSchema#", + "@prefix": true + }, + "iffb": { + "@id": "https://industry-fusion.org/base/v0.1/", + "@prefix": true + }, + "iffk": { + "@id": "https://industry-fusion.org/knowledge/v0.1/", + "@prefix": true + }, + "rdf": { + "@id": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "@prefix": true + }, + "rdfs": { + "@id": "http://www.w3.org/2000/01/rdf-schema#", + "@prefix": true + }, + "schema": { + "@id": "http://schema.org/", + "@prefix": true + }, + "sh": { + "@id": "http://www.w3.org/ns/shacl#", + "@prefix": true + } + } + ], + "id": "urn:x:1", + "type": "eclass:0173-1#01-AKJ975#017", + "hasFilter": { + "object": "urn:filter:1" + }, + "machine_state": "Testing", + "machine_state_from_smartbox": "Offline_Idle", + "testiri": { + "id": "iffk:testiri" + }, + "eclass:0173-1#02-AAH880#003": "10" + } +] diff --git a/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload1.jsonld_r_c0 b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload1.jsonld_r_c0 new file mode 100644 index 00000000..da8c4394 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload1.jsonld_r_c0 @@ -0,0 +1,66 @@ +[ + { + "@context": [ + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld", + { + "@vocab": "https://industry-fusion.org/base/v0.1/", + "eclass": { + "@id": "https://industry-fusion.org/eclass#", + "@prefix": true + }, + "xsd": { + "@id": "http://www.w3.org/2001/XMLSchema#", + "@prefix": true + }, + "iffb": { + "@id": "https://industry-fusion.org/base/v0.1/", + "@prefix": true + }, + "iffk": { + "@id": "https://industry-fusion.org/knowledge/v0.1/", + "@prefix": true + }, + "rdf": { + "@id": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "@prefix": true + }, + "rdfs": { + "@id": "http://www.w3.org/2000/01/rdf-schema#", + "@prefix": true + }, + "schema": { + "@id": "http://schema.org/", + "@prefix": true + }, + "sh": { + "@id": "http://www.w3.org/ns/shacl#", + "@prefix": true + } + } + ], + "id": "urn:x:1", + "type": "eclass:0173-1#01-AKJ975#017", + "hasFilter": { + "type": "Relationship", + "object": "urn:filter:1" + }, + "machine_state": { + "type": "Property", + "value": "Testing" + }, + "machine_state_from_smartbox": { + "type": "Property", + "value": "Offline_Idle" + }, + "testiri": { + "type": "Property", + "value": { + "id": "iffk:testiri" + } + }, + "eclass:0173-1#02-AAH880#003": { + "type": "Property", + "value": "10" + } + } +] diff --git a/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload1.jsonld_x_c0 b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload1.jsonld_x_c0 new file mode 100644 index 00000000..72713926 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload1.jsonld_x_c0 @@ -0,0 +1,37 @@ +[ + { + "https://industry-fusion.org/eclass#0173-1#02-AAH880#003": [ + { + "@value": "10" + } + ], + "https://industry-fusion.org/base/v0.1/hasFilter": [ + { + "https://uri.etsi.org/ngsi-ld/hasObject": [ + { + "@id": "urn:filter:1" + } + ] + } + ], + "@id": "urn:x:1", + "https://industry-fusion.org/base/v0.1/machine_state": [ + { + "@value": "Testing" + } + ], + "https://industry-fusion.org/base/v0.1/machine_state_from_smartbox": [ + { + "@value": "Offline_Idle" + } + ], + "https://industry-fusion.org/base/v0.1/testiri": [ + { + "@id": "https://industry-fusion.org/knowledge/v0.1/testiri" + } + ], + "@type": [ + "https://industry-fusion.org/eclass#0173-1#01-AKJ975#017" + ] + } +] diff --git a/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload2.jsonld b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload2.jsonld new file mode 100644 index 00000000..b3840783 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload2.jsonld @@ -0,0 +1,37 @@ +[ + { + "https://industry-fusion.org/eclass#0173-1#02-AAH880#003": [ + { + "@value": "10" + } + ], + "https://industry-fusion.org/base/v0.1/hasFilter": [ + { + "https://uri.etsi.org/ngsi-ld/hasObject": [ + { + "@id": "urn:filter:1" + } + ] + } + ], + "@id": "urn:x:1", + "https://industry-fusion.org/base/v0.1/machine_state": [ + { + "@value": "Testing" + } + ], + "https://industry-fusion.org/base/v0.1/machine_state_from_smartbox": [ + { + "@value": "Offline_Idle" + } + ], + "https://industry-fusion.org/base/v0.1/testiri": [ + { + "@id": "https://industry-fusion.org/knowledge/v0.1/testiri" + } + ], + "@type": [ + "https://industry-fusion.org/knowledge/v0.1/0173-1#01-AKJ975#017" + ] + } +] diff --git a/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload2.jsonld_n_c0 b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload2.jsonld_n_c0 new file mode 100644 index 00000000..6b6f244c --- /dev/null +++ b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload2.jsonld_n_c0 @@ -0,0 +1,53 @@ +[ + { + "@context": [ + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld", + { + "@vocab": "https://industry-fusion.org/base/v0.1/", + "eclass": { + "@id": "https://industry-fusion.org/eclass#", + "@prefix": true + }, + "xsd": { + "@id": "http://www.w3.org/2001/XMLSchema#", + "@prefix": true + }, + "iffb": { + "@id": "https://industry-fusion.org/base/v0.1/", + "@prefix": true + }, + "iffk": { + "@id": "https://industry-fusion.org/knowledge/v0.1/", + "@prefix": true + }, + "rdf": { + "@id": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "@prefix": true + }, + "rdfs": { + "@id": "http://www.w3.org/2000/01/rdf-schema#", + "@prefix": true + }, + "schema": { + "@id": "http://schema.org/", + "@prefix": true + }, + "sh": { + "@id": "http://www.w3.org/ns/shacl#", + "@prefix": true + } + } + ], + "id": "urn:x:1", + "type": "iffk:0173-1#01-AKJ975#017", + "hasFilter": { + "object": "urn:filter:1" + }, + "machine_state": "Testing", + "machine_state_from_smartbox": "Offline_Idle", + "testiri": { + "id": "iffk:testiri" + }, + "eclass:0173-1#02-AAH880#003": "10" + } +] diff --git a/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload2.jsonld_r_c0 b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload2.jsonld_r_c0 new file mode 100644 index 00000000..bd0638c9 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload2.jsonld_r_c0 @@ -0,0 +1,66 @@ +[ + { + "@context": [ + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld", + { + "@vocab": "https://industry-fusion.org/base/v0.1/", + "eclass": { + "@id": "https://industry-fusion.org/eclass#", + "@prefix": true + }, + "xsd": { + "@id": "http://www.w3.org/2001/XMLSchema#", + "@prefix": true + }, + "iffb": { + "@id": "https://industry-fusion.org/base/v0.1/", + "@prefix": true + }, + "iffk": { + "@id": "https://industry-fusion.org/knowledge/v0.1/", + "@prefix": true + }, + "rdf": { + "@id": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "@prefix": true + }, + "rdfs": { + "@id": "http://www.w3.org/2000/01/rdf-schema#", + "@prefix": true + }, + "schema": { + "@id": "http://schema.org/", + "@prefix": true + }, + "sh": { + "@id": "http://www.w3.org/ns/shacl#", + "@prefix": true + } + } + ], + "id": "urn:x:1", + "type": "iffk:0173-1#01-AKJ975#017", + "hasFilter": { + "type": "Relationship", + "object": "urn:filter:1" + }, + "machine_state": { + "type": "Property", + "value": "Testing" + }, + "machine_state_from_smartbox": { + "type": "Property", + "value": "Offline_Idle" + }, + "testiri": { + "type": "Property", + "value": { + "id": "iffk:testiri" + } + }, + "eclass:0173-1#02-AAH880#003": { + "type": "Property", + "value": "10" + } + } +] diff --git a/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload2.jsonld_x_c0 b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload2.jsonld_x_c0 new file mode 100644 index 00000000..478de1b9 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload2.jsonld_x_c0 @@ -0,0 +1,37 @@ +[ + { + "@id": "urn:x:1", + "@type": [ + "https://industry-fusion.org/knowledge/v0.1/0173-1#01-AKJ975#017" + ], + "https://industry-fusion.org/base/v0.1/hasFilter": [ + { + "https://uri.etsi.org/ngsi-ld/hasObject": [ + { + "@id": "urn:filter:1" + } + ] + } + ], + "https://industry-fusion.org/base/v0.1/machine_state": [ + { + "@value": "Testing" + } + ], + "https://industry-fusion.org/base/v0.1/machine_state_from_smartbox": [ + { + "@value": "Offline_Idle" + } + ], + "https://industry-fusion.org/base/v0.1/testiri": [ + { + "@id": "https://industry-fusion.org/knowledge/v0.1/testiri" + } + ], + "https://industry-fusion.org/eclass#0173-1#02-AAH880#003": [ + { + "@value": "10" + } + ] + } +] diff --git a/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload3.jsonld b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload3.jsonld new file mode 100644 index 00000000..36d9a67c --- /dev/null +++ b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload3.jsonld @@ -0,0 +1,24 @@ +[{ + "machine_state": "Testing", + "machine_state_from_smartbox": "Offline_Idle", + "hasFilter": { + "type": "Relationship", + "object": "urn:filter:1" + }, + "testiri": { + "@id": "iffk:testiri" + }, + "eclass:0173-1#02-AAH880#003": { + "value": "10", + "type": "Property" + }, + "id": "urn:x:1", + "type": "eclass:0173-1#01-AKJ975#017" +}, +{ + "machine_state": "Testing", + "machine_state_from_smartbox": "Offline_Idle", + "id": "urn:filter:1", + "type": "eclass:0173-1#01-ACK991#016" +} +] diff --git a/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload3.jsonld_n_c0 b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload3.jsonld_n_c0 new file mode 100644 index 00000000..a9993a77 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload3.jsonld_n_c0 @@ -0,0 +1,97 @@ +[ + { + "@context": [ + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld", + { + "@vocab": "https://industry-fusion.org/base/v0.1/", + "eclass": { + "@id": "https://industry-fusion.org/eclass#", + "@prefix": true + }, + "xsd": { + "@id": "http://www.w3.org/2001/XMLSchema#", + "@prefix": true + }, + "iffb": { + "@id": "https://industry-fusion.org/base/v0.1/", + "@prefix": true + }, + "iffk": { + "@id": "https://industry-fusion.org/knowledge/v0.1/", + "@prefix": true + }, + "rdf": { + "@id": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "@prefix": true + }, + "rdfs": { + "@id": "http://www.w3.org/2000/01/rdf-schema#", + "@prefix": true + }, + "schema": { + "@id": "http://schema.org/", + "@prefix": true + }, + "sh": { + "@id": "http://www.w3.org/ns/shacl#", + "@prefix": true + } + } + ], + "id": "urn:x:1", + "type": "eclass:0173-1#01-AKJ975#017", + "hasFilter": { + "object": "urn:filter:1" + }, + "machine_state": "Testing", + "machine_state_from_smartbox": "Offline_Idle", + "testiri": { + "id": "iffk:testiri" + }, + "eclass:0173-1#02-AAH880#003": "10" + }, + { + "@context": [ + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld", + { + "@vocab": "https://industry-fusion.org/base/v0.1/", + "eclass": { + "@id": "https://industry-fusion.org/eclass#", + "@prefix": true + }, + "xsd": { + "@id": "http://www.w3.org/2001/XMLSchema#", + "@prefix": true + }, + "iffb": { + "@id": "https://industry-fusion.org/base/v0.1/", + "@prefix": true + }, + "iffk": { + "@id": "https://industry-fusion.org/knowledge/v0.1/", + "@prefix": true + }, + "rdf": { + "@id": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "@prefix": true + }, + "rdfs": { + "@id": "http://www.w3.org/2000/01/rdf-schema#", + "@prefix": true + }, + "schema": { + "@id": "http://schema.org/", + "@prefix": true + }, + "sh": { + "@id": "http://www.w3.org/ns/shacl#", + "@prefix": true + } + } + ], + "id": "urn:filter:1", + "type": "eclass:0173-1#01-ACK991#016", + "machine_state": "Testing", + "machine_state_from_smartbox": "Offline_Idle" + } +] diff --git a/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload3.jsonld_r_c0 b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload3.jsonld_r_c0 new file mode 100644 index 00000000..c3cd9469 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload3.jsonld_r_c0 @@ -0,0 +1,116 @@ +[ + { + "@context": [ + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld", + { + "@vocab": "https://industry-fusion.org/base/v0.1/", + "eclass": { + "@id": "https://industry-fusion.org/eclass#", + "@prefix": true + }, + "xsd": { + "@id": "http://www.w3.org/2001/XMLSchema#", + "@prefix": true + }, + "iffb": { + "@id": "https://industry-fusion.org/base/v0.1/", + "@prefix": true + }, + "iffk": { + "@id": "https://industry-fusion.org/knowledge/v0.1/", + "@prefix": true + }, + "rdf": { + "@id": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "@prefix": true + }, + "rdfs": { + "@id": "http://www.w3.org/2000/01/rdf-schema#", + "@prefix": true + }, + "schema": { + "@id": "http://schema.org/", + "@prefix": true + }, + "sh": { + "@id": "http://www.w3.org/ns/shacl#", + "@prefix": true + } + } + ], + "id": "urn:x:1", + "type": "eclass:0173-1#01-AKJ975#017", + "hasFilter": { + "type": "Relationship", + "object": "urn:filter:1" + }, + "machine_state": { + "type": "Property", + "value": "Testing" + }, + "machine_state_from_smartbox": { + "type": "Property", + "value": "Offline_Idle" + }, + "testiri": { + "type": "Property", + "value": { + "id": "iffk:testiri" + } + }, + "eclass:0173-1#02-AAH880#003": { + "type": "Property", + "value": "10" + } + }, + { + "@context": [ + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld", + { + "@vocab": "https://industry-fusion.org/base/v0.1/", + "eclass": { + "@id": "https://industry-fusion.org/eclass#", + "@prefix": true + }, + "xsd": { + "@id": "http://www.w3.org/2001/XMLSchema#", + "@prefix": true + }, + "iffb": { + "@id": "https://industry-fusion.org/base/v0.1/", + "@prefix": true + }, + "iffk": { + "@id": "https://industry-fusion.org/knowledge/v0.1/", + "@prefix": true + }, + "rdf": { + "@id": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "@prefix": true + }, + "rdfs": { + "@id": "http://www.w3.org/2000/01/rdf-schema#", + "@prefix": true + }, + "schema": { + "@id": "http://schema.org/", + "@prefix": true + }, + "sh": { + "@id": "http://www.w3.org/ns/shacl#", + "@prefix": true + } + } + ], + "id": "urn:filter:1", + "type": "eclass:0173-1#01-ACK991#016", + "machine_state": { + "type": "Property", + "value": "Testing" + }, + "machine_state_from_smartbox": { + "type": "Property", + "value": "Offline_Idle" + } + } +] diff --git a/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload3.jsonld_x_c0 b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload3.jsonld_x_c0 new file mode 100644 index 00000000..80d4b3f8 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/jsonld2jsonld/payload3.jsonld_x_c0 @@ -0,0 +1,63 @@ +[ + { + "https://industry-fusion.org/eclass#0173-1#02-AAH880#003": [ + { + "@type": [ + "https://uri.etsi.org/ngsi-ld/Property" + ], + "https://uri.etsi.org/ngsi-ld/hasValue": [ + { + "@value": "10" + } + ] + } + ], + "https://industry-fusion.org/base/v0.1/hasFilter": [ + { + "https://uri.etsi.org/ngsi-ld/hasObject": [ + { + "@id": "urn:filter:1" + } + ], + "@type": [ + "https://uri.etsi.org/ngsi-ld/Relationship" + ] + } + ], + "@id": "urn:x:1", + "https://industry-fusion.org/base/v0.1/machine_state": [ + { + "@value": "Testing" + } + ], + "https://industry-fusion.org/base/v0.1/machine_state_from_smartbox": [ + { + "@value": "Offline_Idle" + } + ], + "https://industry-fusion.org/base/v0.1/testiri": [ + { + "@id": "https://industry-fusion.org/knowledge/v0.1/testiri" + } + ], + "@type": [ + "https://industry-fusion.org/eclass#0173-1#01-AKJ975#017" + ] + }, + { + "@id": "urn:filter:1", + "https://industry-fusion.org/base/v0.1/machine_state": [ + { + "@value": "Testing" + } + ], + "https://industry-fusion.org/base/v0.1/machine_state_from_smartbox": [ + { + "@value": "Offline_Idle" + } + ], + "@type": [ + "https://industry-fusion.org/eclass#0173-1#01-ACK991#016" + ] + } +] diff --git a/semantic-model/datamodel/tools/tests/jsonld2jsonld/test.sh b/semantic-model/datamodel/tools/tests/jsonld2jsonld/test.sh new file mode 100644 index 00000000..3bfc9d5d --- /dev/null +++ b/semantic-model/datamodel/tools/tests/jsonld2jsonld/test.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# +# Copyright (c) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +while IFS='_' read -ra ADDR; do + payload=${ADDR[0]} + switch=${ADDR[1]} + context=${ADDR[2]} + comparewith=${payload}_${switch}_${context} + echo comparewith "$comparewith" + command="node ../../jsonldConverter.js $payload -$switch -c file://$PWD/$context" + echo Executing: "$command" + $command | diff "${comparewith}" - || exit 1 +done <<< "$(ls payload*.jsonld_?_c*)" diff --git a/semantic-model/datamodel/tools/tests/schema2shacl/c0 b/semantic-model/datamodel/tools/tests/schema2shacl/c0 new file mode 100644 index 00000000..6d7e122e --- /dev/null +++ b/semantic-model/datamodel/tools/tests/schema2shacl/c0 @@ -0,0 +1,38 @@ +[ + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld", + { + "@vocab": "https://industry-fusion.org/base/v0.1/", + "eclass": { + "@id": "https://industry-fusion.org/eclass#", + "@prefix": true + }, + "xsd": { + "@id": "http://www.w3.org/2001/XMLSchema#", + "@prefix": true + }, + "iffb": { + "@id": "https://industry-fusion.org/base/v0.1/", + "@prefix": true + }, + "iffk": { + "@id": "https://industry-fusion.org/knowledge/v0.1/", + "@prefix": true + }, + "rdf": { + "@id": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "@prefix": true + }, + "rdfs": { + "@id": "http://www.w3.org/2000/01/rdf-schema#", + "@prefix": true + }, + "schema": { + "@id": "http://schema.org/", + "@prefix": true + }, + "sh": { + "@id": "http://www.w3.org/ns/shacl#", + "@prefix": true + } + } +] diff --git a/semantic-model/datamodel/tools/tests/schema2shacl/schema1_c0.json b/semantic-model/datamodel/tools/tests/schema2shacl/schema1_c0.json new file mode 100644 index 00000000..35c93de9 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/schema2shacl/schema1_c0.json @@ -0,0 +1,17 @@ +[{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/base/v0.1/Plasmacutter", + "title": "Cutter", + "description": "Cutter template for IFF", + "type": "object", + "properties": { + "type": { + "const": "plasmacutter" + }, + "id": { + "type": "string", + "pattern": "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{1,31}:([a-zA-Z0-9()+,.:=@;$_!*'-]|%[0-9a-fA-F]{2})*$" + } + }, + "required": ["type", "id"] +}] \ No newline at end of file diff --git a/semantic-model/datamodel/tools/tests/schema2shacl/schema1_c0.json_id b/semantic-model/datamodel/tools/tests/schema2shacl/schema1_c0.json_id new file mode 100644 index 00000000..1c63c300 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/schema2shacl/schema1_c0.json_id @@ -0,0 +1 @@ +https://industry-fusion.org/base/v0.1/Plasmacutter diff --git a/semantic-model/datamodel/tools/tests/schema2shacl/schema1_c0.json_result b/semantic-model/datamodel/tools/tests/schema2shacl/schema1_c0.json_result new file mode 100644 index 00000000..dcadd9f6 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/schema2shacl/schema1_c0.json_result @@ -0,0 +1,7 @@ +@prefix iffb: . +@prefix iffk: . +@prefix sh: . + +iffk:PlasmacutterShape a sh:NodeShape; sh:targetClass iffb:Plasmacutter. + + diff --git a/semantic-model/datamodel/tools/tests/schema2shacl/schema2_c0.json b/semantic-model/datamodel/tools/tests/schema2shacl/schema2_c0.json new file mode 100644 index 00000000..72149bdd --- /dev/null +++ b/semantic-model/datamodel/tools/tests/schema2shacl/schema2_c0.json @@ -0,0 +1,17 @@ +[{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/eclass%230173-1%2301-AKJ975%23017", + "title": "Cutter", + "description": "Cutter template for IFF", + "type": "object", + "properties": { + "type": { + "const": "eclass:0173-1#01-AKJ975#017" + }, + "id": { + "type": "string", + "pattern": "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{1,31}:([a-zA-Z0-9()+,.:=@;$_!*'-]|%[0-9a-fA-F]{2})*$" + } + }, + "required": ["type", "id"] +}] \ No newline at end of file diff --git a/semantic-model/datamodel/tools/tests/schema2shacl/schema2_c0.json_id b/semantic-model/datamodel/tools/tests/schema2shacl/schema2_c0.json_id new file mode 100644 index 00000000..a1064ee6 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/schema2shacl/schema2_c0.json_id @@ -0,0 +1 @@ +https://industry-fusion.org/eclass#0173-1#01-AKJ975#017 diff --git a/semantic-model/datamodel/tools/tests/schema2shacl/schema2_c0.json_result b/semantic-model/datamodel/tools/tests/schema2shacl/schema2_c0.json_result new file mode 100644 index 00000000..7722ec68 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/schema2shacl/schema2_c0.json_result @@ -0,0 +1,6 @@ +@prefix sh: . + + + a sh:NodeShape; + sh:targetClass . + diff --git a/semantic-model/datamodel/tools/tests/schema2shacl/schema3_c0.json b/semantic-model/datamodel/tools/tests/schema2shacl/schema3_c0.json new file mode 100644 index 00000000..ebc29cba --- /dev/null +++ b/semantic-model/datamodel/tools/tests/schema2shacl/schema3_c0.json @@ -0,0 +1,79 @@ +[ + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/eclass%230173-1%2301-AKJ975%23017", + "title": "Cutter", + "description": "Cutter template for IFF", + "type": "object", + "properties": { + "type": { + "const": "eclass:0173-1#01-AKJ975#017" + }, + "id": { + "type": "string", + "pattern": "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{1,31}:([a-zA-Z0-9()+,.:=@;$_!*'-]|%[0-9a-fA-F]{2})*$" + } + }, + "required": ["type", "id"], + "allOf": [ + { + "$ref": "https://industry-fusion.org/base-objects/v0.1/cutter/properties" + }, + { + "$ref": "https://industry-fusion.org/base-objects/v0.1/cutter/relationships" + } + ] + }, + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/base-objects/v0.1/cutter/relationships", + "title": "IFF template for cutter relationship", + "description": "Cutter template for IFF", + "type": "object", + "properties": { + "hasFilter": { + "relationship": "eclass:0173-1#01-ACK991#016", + "$ref": "https://industry-fusion.org/base-objects/v0.1/link" + } + } + }, + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/base-objects/v0.1/link", + "title": "IFF template for cutter relationship", + "description": "Cutter template for IFF", + "type": "object", + "properties": { + "object": { + "type": "string", + "pattern": "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{0,31}:[a-zA-Z0-9()+,\\-.:=@;$_!*']*[a-zA-Z0-9()+,\\-.:=@;$_!*']$" + } + }, + "required": ["object"] + }, + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/base-objects/v0.1/cutter/properties", + "title": "Cutter properties", + "description": "Properties for class cutter", + "type": "object", + "properties": { + "machine_state": { + "type": "string", + "title": "Machine Status", + "description": "Current status of the machine (Online_Idle, Run, Online_Error, Online_Maintenance, Setup, Testing)", + "enum": [ + "Online_Idle", + "Run", + "Online_Error", + "Online_Maintenance", + "Setup", + "Testing" + ] + } + }, + "required": [ + "machine_state" + ] + } +] \ No newline at end of file diff --git a/semantic-model/datamodel/tools/tests/schema2shacl/schema3_c0.json_id b/semantic-model/datamodel/tools/tests/schema2shacl/schema3_c0.json_id new file mode 100644 index 00000000..a1064ee6 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/schema2shacl/schema3_c0.json_id @@ -0,0 +1 @@ +https://industry-fusion.org/eclass#0173-1#01-AKJ975#017 diff --git a/semantic-model/datamodel/tools/tests/schema2shacl/schema3_c0.json_result b/semantic-model/datamodel/tools/tests/schema2shacl/schema3_c0.json_result new file mode 100644 index 00000000..4e5cf580 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/schema2shacl/schema3_c0.json_result @@ -0,0 +1,40 @@ +@prefix iffb: . +@prefix sh: . +@prefix ngsi-ld: . + + + a sh:NodeShape; + sh:property + [ + sh:maxCount 1; + sh:minCount 0; + sh:nodeKind sh:BlankNode; + sh:path iffb:hasFilter; + sh:property + [ + sh:class + ; + sh:maxCount 1; + sh:minCount 1; + sh:nodeKind sh:IRI; + sh:path ngsi-ld:hasObject + ] + ], + [ + sh:maxCount 1; + sh:minCount 1; + sh:nodeKind sh:BlankNode; + sh:path iffb:machine_state; + sh:property + [ + sh:in + ( "Online_Idle" "Run" "Online_Error" + "Online_Maintenance" "Setup" "Testing" ); + sh:maxCount 1; + sh:minCount 1; + sh:nodeKind sh:Literal; + sh:path ngsi-ld:hasValue + ] + ]; + sh:targetClass . + diff --git a/semantic-model/datamodel/tools/tests/schema2shacl/test.sh b/semantic-model/datamodel/tools/tests/schema2shacl/test.sh new file mode 100644 index 00000000..a0508fb7 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/schema2shacl/test.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# +# Copyright (c) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +while IFS='_' read -ra ADDR; do + schemaname=${ADDR[0]} + context=${ADDR[1]%.json} + file=${schemaname}_${context}.json + comparewith=${file}_result + id=${file}_id + command="node ../../jsonschema2shacl.js -s $file -c file://$PWD/$context -i $(cat "$id")" + echo Executing: "$command" + $command | diff "${comparewith}" - || exit 1 +done <<< "$(ls schema*_*.json)" diff --git a/semantic-model/datamodel/tools/tests/testJsonldUtils.js b/semantic-model/datamodel/tools/tests/testJsonldUtils.js new file mode 100644 index 00000000..a8c83184 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/testJsonldUtils.js @@ -0,0 +1,151 @@ +/** +* Copyright (c) 2023 Intel Corporation +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +'use strict' + +const { assert } = require('chai') +const chai = require('chai') +global.should = chai.should() + +const rewire = require('rewire') +const ToTest = rewire('../lib/jsonldUtils.js') + +describe('Test loadContextFromFile', function () { + it('Should load context from filename', function () { + const fs = { + readFileSync: (filename, coding) => { + filename.should.equal('filename') + return '["context"]' + } + } + const revert = ToTest.__set__('fs', fs) + // ToTest.__set__('process', process) + const loadContextFromFile = ToTest.__get__('loadContextFromFile') + const result = loadContextFromFile('filename') + result.should.deep.equal(['context']) + revert() + }) +}) + +describe('Test mergeContexts', function () { + it('should return null with undefined contexts', function () { + const result = ToTest.mergeContexts(undefined, undefined) + assert(result === null) + }) + it('should return only the global context', function () { + const context = 'http://context' + const result = ToTest.mergeContexts(undefined, context) + result.should.deep.equal([context]) + }) + it('should return only the local context', function () { + const context = [{ '@context': 'http://context' }] + const result = ToTest.mergeContexts(context, undefined) + result.should.deep.equal([['http://context']]) + }) + it('should merge both contexts', function () { + const globalcontext = 'http://context2' + const context = [{ '@context': 'http://context' }] + const result = ToTest.mergeContexts(context, globalcontext) + result.should.deep.equal([['http://context', 'http://context2']]) + }) + it('should merge object with string contexts', function () { + const globalcontext = 'http://context2' + const context = [{ '@context': { ns: 'http://context' } }] + const result = ToTest.mergeContexts(context, globalcontext) + result.should.deep.equal([[{ ns: 'http://context' }, 'http://context2']]) + }) + it('should remove double context', function () { + const globalcontext = 'http://context2' + const context = [{ '@context': { ns: 'http://context' } }, { '@context': 'http://context2' }] + const result = ToTest.mergeContexts(context, globalcontext) + result.should.deep.equal([[{ ns: 'http://context' }, 'http://context2'], ['http://context2']]) + }) +}) +describe('Test conciseExpandedForm', function () { + it('should concise a property', function () { + const expandedForm = [{ + '@id': 'id', + '@type': 'type', + property: [{ + '@type': ['https://uri.etsi.org/ngsi-ld/Property'], + 'https://uri.etsi.org/ngsi-ld/hasValue': [{ '@value': 'value' }] + }] + }] + const expectedConciseForm = [{ + '@id': 'id', + '@type': 'type', + property: [{ '@value': 'value' }] + }] + const result = ToTest.conciseExpandedForm(expandedForm) + expectedConciseForm.should.deep.equal(result) + }) + it('should concise a relationship', function () { + const expandedForm = [{ + '@id': 'id', + '@type': 'type', + hasRelationship: [{ + '@type': ['https://uri.etsi.org/ngsi-ld/Relationship'], + 'https://uri.etsi.org/ngsi-ld/hasObject': [{ '@id': 'iri' }] + }] + }] + const expectedConciseForm = [{ + '@id': 'id', + '@type': 'type', + hasRelationship: [{ 'https://uri.etsi.org/ngsi-ld/hasObject': [{ '@id': 'iri' }] }] + }] + const result = ToTest.conciseExpandedForm(expandedForm) + expectedConciseForm.should.deep.equal(result) + }) +}) +describe('Test normalizeExpandedForm', function () { + it('should normalize a property', function () { + const expandedForm = [{ + '@id': 'id', + '@type': 'type', + property: [{ + 'https://uri.etsi.org/ngsi-ld/hasValue': [{ '@value': 'value' }] + }] + }] + const expectedNormalizedForm = [{ + '@id': 'id', + '@type': 'type', + property: [{ + '@type': ['https://uri.etsi.org/ngsi-ld/Property'], + 'https://uri.etsi.org/ngsi-ld/hasValue': [{ '@value': 'value' }] + }] + }] + const result = ToTest.normalizeExpandedForm(expandedForm) + expectedNormalizedForm.should.deep.equal(result) + }) + it('should normalize a property', function () { + const expandedForm = [{ + '@id': 'id', + '@type': 'type', + hasRelationship: [{ + 'https://uri.etsi.org/ngsi-ld/hasObject': [{ '@id': 'iri' }] + }] + }] + const expectedNormalizedForm = [{ + '@id': 'id', + '@type': 'type', + hasRelationship: [{ + '@type': ['https://uri.etsi.org/ngsi-ld/Relationship'], + 'https://uri.etsi.org/ngsi-ld/hasObject': [{ '@id': 'iri' }] + }] + }] + const result = ToTest.normalizeExpandedForm(expandedForm) + expectedNormalizedForm.should.deep.equal(result) + }) +}) \ No newline at end of file diff --git a/semantic-model/datamodel/tools/tests/testShaclUtils.js b/semantic-model/datamodel/tools/tests/testShaclUtils.js new file mode 100644 index 00000000..8fbaa902 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/testShaclUtils.js @@ -0,0 +1,476 @@ +/** +* Copyright (c) 2023 Intel Corporation +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +'use strict' + +const { assert } = require('chai') +const chai = require('chai') +global.should = chai.should() + +const rewire = require('rewire') +const ToTest = rewire('../lib/shaclUtils.js') + +describe('Test class NodeShape', function () { + it('Should manage properties', function () { + const nodeShape = new ToTest.NodeShape('targetClass') + nodeShape.properties = 'property' + const properties = nodeShape.properties + properties.should.deep.equal('property') + const nodeShape2 = new ToTest.NodeShape('targetClass') + nodeShape2.properties = ['property1'] + nodeShape2.addPropertyShape('propertyShape') + nodeShape2.addPropertyShape('propertyShape2') + const properties2 = nodeShape2.properties + properties2.should.deep.equal(['property1', 'propertyShape', 'propertyShape2']) + }) +}) +describe('Test class PropertyShape', function () { + it('Should manage properties', function () { + const propertyShape = new ToTest.PropertyShape(0, 1, 'nodeKind', 'path', true) + propertyShape.addConstraint('property') + propertyShape.addConstraint('property2') + const constraints = propertyShape.constraints + constraints.should.deep.equal(['property', 'property2']) + propertyShape.mincount.should.equal(0) + propertyShape.maxcount.should.equal(1) + propertyShape.nodeKind.should.equal('nodeKind') + propertyShape.path.should.equal('path') + propertyShape.isProperty.should.equal(true) + propertyShape.propertyNode = 'node' + propertyShape.propertyNode.should.equal('node') + }) +}) +describe('Test class Constraint', function () { + it('Should construct', function () { + const constraint = new ToTest.Constraint('type', 'param') + constraint.type.should.equal('type') + constraint.params.should.equal('param') + }) +}) +describe('Test dumpPropertyShape', function () { + it('Should dump property without constraints', function () { + const propertyShape = new ToTest.PropertyShape(1, 2, 'nodeKind', 'path', true) + propertyShape.propertyNode = 'propertyNode' + const storeAdds = [] + const store = { + add: (s, p, o) => { storeAdds.push([s, p, o]) } + } + const $rdf = { + blankNode: () => { return {} }, + Namespace: () => (x) => 'ngsild:' + x + } + const SHACL = (x) => 'shacl:' + x + const revert = ToTest.__set__('$rdf', $rdf) + ToTest.__set__('SHACL', SHACL) + ToTest.__set__('globalPrefixHash', { 'ngsi-ld': 'ngsi-ld' }) + const dumpPropertyShape = ToTest.__get__('dumpPropertyShape') + dumpPropertyShape(propertyShape, store) + storeAdds.should.deep.equal([ + ['propertyNode', 'shacl:minCount', 1], + ['propertyNode', 'shacl:maxCount', 2], + ['propertyNode', 'shacl:nodeKind', 'shacl:BlankNode'], + ['propertyNode', 'shacl:path', 'path'], + ['propertyNode', 'shacl:property', {}], + [{}, 'shacl:path', 'ngsild:hasValue'], + [{}, 'shacl:minCount', 1], + [{}, 'shacl:maxCount', 1], + [{}, 'shacl:nodeKind', 'nodeKind']]) + revert() + }) + it('Should dump property with constraints', function () { + const propertyShape = new ToTest.PropertyShape(0, 2, 'nodeKind', 'path', true) + propertyShape.propertyNode = 'propertyNode' + propertyShape.addConstraint(new ToTest.Constraint('type', 'params')) + propertyShape.addConstraint(new ToTest.Constraint('type2', ['p1', 'p2'])) + const storeAdds = [] + const store = { + add: (s, p, o) => { storeAdds.push([s, p, o]) } + } + const $rdf = { + blankNode: () => { return {} }, + Namespace: () => (x) => 'ngsild:' + x + } + const SHACL = (x) => 'shacl:' + x + const revert = ToTest.__set__('$rdf', $rdf) + ToTest.__set__('SHACL', SHACL) + ToTest.__set__('globalPrefixHash', { 'ngsi-ld': 'ngsi-ld' }) + const dumpPropertyShape = ToTest.__get__('dumpPropertyShape') + dumpPropertyShape(propertyShape, store) + storeAdds.should.deep.equal([ + ['propertyNode', 'shacl:minCount', 0], + ['propertyNode', 'shacl:maxCount', 2], + ['propertyNode', 'shacl:nodeKind', 'shacl:BlankNode'], + ['propertyNode', 'shacl:path', 'path'], + ['propertyNode', 'shacl:property', {}], + [{}, 'shacl:path', 'ngsild:hasValue'], + [{}, 'shacl:minCount', 1], + [{}, 'shacl:maxCount', 1], + [{}, 'shacl:nodeKind', 'nodeKind'], + [{}, 'type', 'params'], + [{}, 'type2', ['p1', 'p2']]]) + revert() + }) + it('Should dump relationship without constraints', function () { + const propertyShape = new ToTest.PropertyShape(1, 1, 'nodeKind', 'relationship', false) + propertyShape.propertyNode = 'propertyNode' + const storeAdds = [] + const store = { + add: (s, p, o) => { storeAdds.push([s, p, o]) } + } + const $rdf = { + blankNode: () => { return {} }, + Namespace: () => (x) => 'ngsild:' + x + } + const SHACL = (x) => 'shacl:' + x + const revert = ToTest.__set__('$rdf', $rdf) + ToTest.__set__('SHACL', SHACL) + ToTest.__set__('globalPrefixHash', { 'ngsi-ld': 'ngsi-ld' }) + const dumpPropertyShape = ToTest.__get__('dumpPropertyShape') + dumpPropertyShape(propertyShape, store) + storeAdds.should.deep.equal([ + ['propertyNode', 'shacl:minCount', 1], + ['propertyNode', 'shacl:maxCount', 1], + ['propertyNode', 'shacl:nodeKind', 'shacl:BlankNode'], + ['propertyNode', 'shacl:path', 'relationship'], + ['propertyNode', 'shacl:property', {}], + [{}, 'shacl:path', 'ngsild:hasObject'], + [{}, 'shacl:minCount', 1], + [{}, 'shacl:maxCount', 1], + [{}, 'shacl:nodeKind', 'nodeKind']]) + revert() + }) +}) +describe('Test dumpNodeShape', function () { + it('Should dump without properties', function () { + const nodeShape = new ToTest.NodeShape('http://example.com/targetClass') + const storeAdds = [] + const store = { + add: (s, p, o) => { storeAdds.push([s, p, o]) } + } + const $rdf = { + blankNode: () => { return {} }, + sym: (x) => 'sym:' + x + } + const globalContext = { + expandTerm: (x) => x + } + const revert = ToTest.__set__('SHACL', (x) => 'shacl:' + x) + ToTest.__set__('IFFK', (x) => 'iffk:' + x) + ToTest.__set__('RDF', (x) => 'rdf:' + x) + ToTest.__set__('globalContext', globalContext) + ToTest.__set__('$rdf', $rdf) + const dumpNodeShape = ToTest.__get__('dumpNodeShape') + dumpNodeShape(nodeShape, store) + storeAdds.should.deep.equal([ + ['iffk:targetClassShape', 'rdf:type', 'shacl:NodeShape'], + ['iffk:targetClassShape', 'shacl:targetClass', 'sym:http://example.com/targetClass'] + ]) + revert() + }) + it('Should dump relationship with properties', function () { + const nodeShape = new ToTest.NodeShape('http://example.com/targetClass') + const propertyShape = new ToTest.PropertyShape(0, 2, 'nodeKind', 'path', true) + nodeShape.addPropertyShape(propertyShape) + const storeAdds = [] + const store = { + add: (s, p, o) => { storeAdds.push([s, p, o]) } + } + const $rdf = { + blankNode: () => { return {} }, + sym: (x) => 'sym:' + x + } + const globalContext = { + expandTerm: (x) => x + } + const dumpPropertyShape = (x, y) => { x.propertyNode.should.deep.equal({}) } + const revert = ToTest.__set__('SHACL', (x) => 'shacl:' + x) + ToTest.__set__('IFFK', (x) => 'iffk:' + x) + ToTest.__set__('RDF', (x) => 'rdf:' + x) + ToTest.__set__('globalContext', globalContext) + ToTest.__set__('dumpPropertyShape', dumpPropertyShape) + ToTest.__set__('$rdf', $rdf) + const dumpNodeShape = ToTest.__get__('dumpNodeShape') + dumpNodeShape(nodeShape, store) + storeAdds.should.deep.equal([ + ['iffk:targetClassShape', 'rdf:type', 'shacl:NodeShape'], + ['iffk:targetClassShape', 'shacl:targetClass', 'sym:http://example.com/targetClass'], + ['iffk:targetClassShape', 'shacl:property', {}] + ]) + revert() + }) + it('Should dump with hash type', function () { + const nodeShape = new ToTest.NodeShape('http://example.com/example#targetClass#1#2') + const storeAdds = [] + const store = { + add: (s, p, o) => { storeAdds.push([s, p, o]) } + } + const $rdf = { + blankNode: () => { return {} }, + sym: (x) => 'sym:' + x + } + const globalContext = { + expandTerm: (x) => x + } + const revert = ToTest.__set__('SHACL', (x) => 'shacl:' + x) + ToTest.__set__('IFFK', (x) => 'iffk:' + x) + ToTest.__set__('RDF', (x) => 'rdf:' + x) + ToTest.__set__('globalContext', globalContext) + ToTest.__set__('$rdf', $rdf) + const dumpNodeShape = ToTest.__get__('dumpNodeShape') + dumpNodeShape(nodeShape, store) + storeAdds.should.deep.equal([ + ['iffk:targetClass#1#2Shape', 'rdf:type', 'shacl:NodeShape'], + ['iffk:targetClass#1#2Shape', 'shacl:targetClass', 'sym:http://example.com/example#targetClass#1#2'] + ]) + revert() + }) +}) +describe('Test scanProperties', function () { + it('Should dump without properties', function () { + const scanProperties = ToTest.__get__('scanProperties') + const typeSchema = { + properties: { + type: { + const: 'Plasmacutter' + }, + id: { + type: 'string', + pattern: "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{1,31}:([a-zA-Z0-9()+,.:=@;$_!*'-]|%[0-9a-fA-F]{2})*$" + } + + }, + required: ['type', 'id'] + } + const expectedNodeShape = { + _properties: [], + targetClass: 'targetClass' + } + const nodeShape = new ToTest.NodeShape('targetClass') + scanProperties(nodeShape, typeSchema) + nodeShape.should.deep.equal(expectedNodeShape) + }) +}) +describe('Test scanProperties', function () { + it('Should scan without properties', function () { + const scanProperties = ToTest.__get__('scanProperties') + const typeSchema = { + properties: { + type: { + const: 'Plasmacutter' + }, + id: { + type: 'string', + pattern: "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{1,31}:([a-zA-Z0-9()+,.:=@;$_!*'-]|%[0-9a-fA-F]{2})*$" + } + + }, + required: ['type', 'id'] + } + const expectedNodeShape = { + _properties: [], + targetClass: 'targetClass' + } + const nodeShape = new ToTest.NodeShape('targetClass') + scanProperties(nodeShape, typeSchema) + nodeShape.should.deep.equal(expectedNodeShape) + }) + it('Should scan with property', function () { + const scanProperties = ToTest.__get__('scanProperties') + const typeSchema = { + properties: { + machine_state: { + type: 'string', + title: 'Machine Status', + description: 'Current status of the machine (Online_Idle, Run, Online_Error, Online_Maintenance, Setup, Testing)', + enum: [ + 'Testing' + ] + } + } + } + const expectedNodeShape = { + targetClass: 'targetClass', + _properties: [ + { + mincount: 0, + maxcount: 1, + nodeKind: 'shacl:Literal', + path: 'sym:machine_state', + constraints: [ + { + type: 'shacl:in', + params: [ + 'Testing'] + }], + isProperty: true + } + ] + } + const nodeShape = new ToTest.NodeShape('targetClass') + scanProperties(nodeShape, typeSchema) + nodeShape.should.deep.equal(expectedNodeShape) + }) + it('Should scan with relationship', function () { + const scanProperties = ToTest.__get__('scanProperties') + const typeSchema = { + properties: { + hasFilter: { + relationship: 'eclass:0173-1#01-ACK991#016', + $ref: 'https://industry-fusion.org/base-objects/v0.1/link' + } + } + } + const expectedNodeShape = { + targetClass: 'targetClass', + _properties: [ + { + mincount: 0, + maxcount: 1, + nodeKind: 'shacl:IRI', + path: 'sym:hasFilter', + constraints: [ + { + type: 'shacl:class', + params: 'sym:eclass:0173-1#01-ACK991#016' + } + ], + isProperty: false + }] + } + const nodeShape = new ToTest.NodeShape('targetClass') + scanProperties(nodeShape, typeSchema) + nodeShape.should.deep.equal(expectedNodeShape) + }) + it('Should scan with allOf', function () { + const scanProperties = ToTest.__get__('scanProperties') + const typeSchema = { + allOf: [ + { + properties: { + machine_state: { + type: 'string', + title: 'Machine Status', + description: 'Current status of the machine (Online_Idle, Run, Online_Error, Online_Maintenance, Setup, Testing)', + enum: [ + 'Setup', + 'Testing' + ] + } + } + } + ] + } + const expectedNodeShape = { + targetClass: 'targetClass', + _properties: [ + { + mincount: 0, + maxcount: 1, + nodeKind: 'shacl:Literal', + path: 'sym:machine_state', + constraints: [ + { + type: 'shacl:in', + params: + [ + 'Setup', + 'Testing' + ] + } + ], + isProperty: true + } + ] + } + const nodeShape = new ToTest.NodeShape('targetClass') + scanProperties(nodeShape, typeSchema) + nodeShape.should.deep.equal(expectedNodeShape) + }) +}) +describe('Test scanConstraints', function () { + it('Should dump without properties', function () { + const scanConstraints = ToTest.__get__('scanConstraints') + const propertyShape = new ToTest.PropertyShape(0, 2, 'nodeKind', 'path', true) + const typeSchema = { + type: 'string', + title: 'Machine Status', + description: 'Current status of the machine (Online_Idle, Run, Online_Error, Online_Maintenance, Setup, Testing)', + enum: [ + 'Online_Idle' + ], + maximum: 2, + minimum: 1, + exclusiveMinimum: 0, + exclusiveMaximum: 3, + maxLength: 100, + minLength: 10 + } + const expectedConstraints = [ + { type: 'shacl:in', params: ['Online_Idle'] }, + { type: 'shacl:maxInclusive', params: 2 }, + { type: 'shacl:minInclusive', params: 1 }, + { type: 'shacl:minExclusive', params: 0 }, + { type: 'shacl:maxExclusive', params: 3 }, + { type: 'shacl:maxLength', params: 100 }, + { type: 'shacl:minLength', params: 10 } + ] + scanConstraints(propertyShape, typeSchema) + propertyShape.constraints.should.deep.equal(expectedConstraints) + }) +}) +describe('Test encodeHash', function () { + it('Should uri-encode hash', function () { + const encodeHash = ToTest.__get__('encodeHash') + const result = encodeHash('https://example.com/test#1#2#3') + result.should.equal('https://example.com/test%231%232%233') + }) +}) +describe('Test loadContext', function () { + it('Should resolve https uri', async function () { + const loadContext = ToTest.__get__('loadContext') + const context = { + getContextRaw: () => { + return { + '@vocab': 'https://industry-fusion.org/base/v0.1/', + eclass: { + '@id': 'https://industry-fusion.org/eclass#', + '@prefix': true + }, + xsd: { + '@id': 'http://www.w3.org/2001/XMLSchema#', + '@prefix': true + }, + iffb: { + '@id': 'https://industry-fusion.org/base/v0.1/', + '@prefix': true + } + } + } + } + const myParser = { + parse: async (x) => { return context } + } + const expectedResult = { + eclass: 'https://industry-fusion.org/eclass#', + xsd: 'http://www.w3.org/2001/XMLSchema#', + iffb: 'https://industry-fusion.org/base/v0.1/' + } + const revert = ToTest.__set__('myParser', myParser) + await loadContext('https://example.com/context') + const globalPrefixHash = ToTest.__get__('globalPrefixHash') + globalPrefixHash.should.deep.equal(expectedResult) + revert() + }) +}) diff --git a/semantic-model/datamodel/tools/tests/validation/test.sh b/semantic-model/datamodel/tools/tests/validation/test.sh new file mode 100644 index 00000000..abbdbcdd --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/test.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# +# Copyright (c) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +while IFS='_' read -ra ADDR; do + testname=${ADDR[0]} + schemaname=${testname}_schema.json + dataname=${testname}_${ADDR[1]} + resultname=${dataname}_result + idname=${dataname}_id + command="node ../../validate.js -s ${schemaname} -d ${dataname} -i $(cat "${idname}")" + echo -n "Executing test $command ... " + $command | diff "${resultname}" - || exit 1 + echo OK +done <<< "$(ls validation*_data*.json)" diff --git a/semantic-model/datamodel/tools/tests/validation/validation2_data1.json b/semantic-model/datamodel/tools/tests/validation/validation2_data1.json new file mode 100644 index 00000000..40072f30 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation2_data1.json @@ -0,0 +1,5 @@ +{ + "@context": "https://industryfusion.github.io/contexts/v0.1/context.jsonld", + "id": "urn:iff:abc123", + "type": "Plasmacutter" +} diff --git a/semantic-model/datamodel/tools/tests/validation/validation2_data1.json_id b/semantic-model/datamodel/tools/tests/validation/validation2_data1.json_id new file mode 100644 index 00000000..1c63c300 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation2_data1.json_id @@ -0,0 +1 @@ +https://industry-fusion.org/base/v0.1/Plasmacutter diff --git a/semantic-model/datamodel/tools/tests/validation/validation2_data1.json_result b/semantic-model/datamodel/tools/tests/validation/validation2_data1.json_result new file mode 100644 index 00000000..ee71d9c0 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation2_data1.json_result @@ -0,0 +1 @@ +The Datafile is compliant with Schema diff --git a/semantic-model/datamodel/tools/tests/validation/validation2_schema.json b/semantic-model/datamodel/tools/tests/validation/validation2_schema.json new file mode 100644 index 00000000..f429d56b --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation2_schema.json @@ -0,0 +1,17 @@ +[{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/base/v0.1/Plasmacutter", + "title": "Cutter", + "description": "Cutter template for IFF", + "type": "object", + "properties": { + "type": { + "const": "Plasmacutter" + }, + "id": { + "type": "string", + "pattern": "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{1,31}:([a-zA-Z0-9()+,.:=@;$_!*'-]|%[0-9a-fA-F]{2})*$" + } + }, + "required": ["type", "id"] +}] \ No newline at end of file diff --git a/semantic-model/datamodel/tools/tests/validation/validation3_data1.json b/semantic-model/datamodel/tools/tests/validation/validation3_data1.json new file mode 100644 index 00000000..799eb2dd --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation3_data1.json @@ -0,0 +1,9 @@ +{ + "@context": "https://industryfusion.github.io/contexts/v0.1/context.jsonld", + "id": "urn:iff:abc123", + "type": "eclass:0173-1#01-AKJ975#017", + "hasFilter": { + "object": "urn:iff:filter:1" + }, + "machine_state": "Testing" +} diff --git a/semantic-model/datamodel/tools/tests/validation/validation3_data1.json_id b/semantic-model/datamodel/tools/tests/validation/validation3_data1.json_id new file mode 100644 index 00000000..a1064ee6 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation3_data1.json_id @@ -0,0 +1 @@ +https://industry-fusion.org/eclass#0173-1#01-AKJ975#017 diff --git a/semantic-model/datamodel/tools/tests/validation/validation3_data1.json_result b/semantic-model/datamodel/tools/tests/validation/validation3_data1.json_result new file mode 100644 index 00000000..ee71d9c0 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation3_data1.json_result @@ -0,0 +1 @@ +The Datafile is compliant with Schema diff --git a/semantic-model/datamodel/tools/tests/validation/validation3_data2.json b/semantic-model/datamodel/tools/tests/validation/validation3_data2.json new file mode 100644 index 00000000..bf22e725 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation3_data2.json @@ -0,0 +1,9 @@ +{ + "@context": "https://industryfusion.github.io/contexts/v0.1/context.jsonld", + "id": "urn:iff:abc123", + "type": "eclass:0173-1#01-AKJ975#017", + "hasFilter": { + "object": "wrongurn" + }, + "machine_state": "Testing" +} diff --git a/semantic-model/datamodel/tools/tests/validation/validation3_data2.json_id b/semantic-model/datamodel/tools/tests/validation/validation3_data2.json_id new file mode 100644 index 00000000..a1064ee6 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation3_data2.json_id @@ -0,0 +1 @@ +https://industry-fusion.org/eclass#0173-1#01-AKJ975#017 diff --git a/semantic-model/datamodel/tools/tests/validation/validation3_data2.json_result b/semantic-model/datamodel/tools/tests/validation/validation3_data2.json_result new file mode 100644 index 00000000..e9cef687 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation3_data2.json_result @@ -0,0 +1,12 @@ +Not Compliant: +[ + { + instancePath: '/hasFilter/object', + schemaPath: 'https://industry-fusion.org/base-objects/v0.1/link/properties/object/pattern', + keyword: 'pattern', + params: { + pattern: "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{0,31}:[a-zA-Z0-9()+,\\-.:=@;$_!*']*[a-zA-Z0-9()+,\\-.:=@;$_!*']$" + }, + message: `must match pattern "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{0,31}:[a-zA-Z0-9()+,\\-.:=@;$_!*']*[a-zA-Z0-9()+,\\-.:=@;$_!*']$"` + } +] diff --git a/semantic-model/datamodel/tools/tests/validation/validation3_data3.json b/semantic-model/datamodel/tools/tests/validation/validation3_data3.json new file mode 100644 index 00000000..292b16ee --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation3_data3.json @@ -0,0 +1,9 @@ +{ + "@context": "https://industryfusion.github.io/contexts/v0.1/context.jsonld", + "id": "urn:iff:abc123", + "type": "eclass:0173-1#01-AKJ975#017", + "hasFilter": { + "object": "urn:iff:filter:1" + }, + "machine_state": "wrongState" +} diff --git a/semantic-model/datamodel/tools/tests/validation/validation3_data3.json_id b/semantic-model/datamodel/tools/tests/validation/validation3_data3.json_id new file mode 100644 index 00000000..a1064ee6 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation3_data3.json_id @@ -0,0 +1 @@ +https://industry-fusion.org/eclass#0173-1#01-AKJ975#017 diff --git a/semantic-model/datamodel/tools/tests/validation/validation3_data3.json_result b/semantic-model/datamodel/tools/tests/validation/validation3_data3.json_result new file mode 100644 index 00000000..c4011856 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation3_data3.json_result @@ -0,0 +1,10 @@ +Not Compliant: +[ + { + instancePath: '/machine_state', + schemaPath: 'https://industry-fusion.org/base-objects/v0.1/cutter/properties/properties/machine_state/enum', + keyword: 'enum', + params: { allowedValues: [Array] }, + message: 'must be equal to one of the allowed values' + } +] diff --git a/semantic-model/datamodel/tools/tests/validation/validation3_data4.json b/semantic-model/datamodel/tools/tests/validation/validation3_data4.json new file mode 100644 index 00000000..a0a280d7 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation3_data4.json @@ -0,0 +1,7 @@ +{ + "@context": "https://industryfusion.github.io/contexts/v0.1/context.jsonld", + "id": "urn:iff:abc123", + "type": "eclass:0173-1#01-AKJ975#017", + "hasFilter": "urn:iff:filter:1", + "machine_state": "Testing" +} diff --git a/semantic-model/datamodel/tools/tests/validation/validation3_data4.json_id b/semantic-model/datamodel/tools/tests/validation/validation3_data4.json_id new file mode 100644 index 00000000..a1064ee6 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation3_data4.json_id @@ -0,0 +1 @@ +https://industry-fusion.org/eclass#0173-1#01-AKJ975#017 diff --git a/semantic-model/datamodel/tools/tests/validation/validation3_data4.json_result b/semantic-model/datamodel/tools/tests/validation/validation3_data4.json_result new file mode 100644 index 00000000..3092ce90 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation3_data4.json_result @@ -0,0 +1,10 @@ +Not Compliant: +[ + { + instancePath: '/hasFilter', + schemaPath: 'https://industry-fusion.org/base-objects/v0.1/link/type', + keyword: 'type', + params: { type: 'object' }, + message: 'must be object' + } +] diff --git a/semantic-model/datamodel/tools/tests/validation/validation3_schema.json b/semantic-model/datamodel/tools/tests/validation/validation3_schema.json new file mode 100644 index 00000000..ebc29cba --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation3_schema.json @@ -0,0 +1,79 @@ +[ + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/eclass%230173-1%2301-AKJ975%23017", + "title": "Cutter", + "description": "Cutter template for IFF", + "type": "object", + "properties": { + "type": { + "const": "eclass:0173-1#01-AKJ975#017" + }, + "id": { + "type": "string", + "pattern": "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{1,31}:([a-zA-Z0-9()+,.:=@;$_!*'-]|%[0-9a-fA-F]{2})*$" + } + }, + "required": ["type", "id"], + "allOf": [ + { + "$ref": "https://industry-fusion.org/base-objects/v0.1/cutter/properties" + }, + { + "$ref": "https://industry-fusion.org/base-objects/v0.1/cutter/relationships" + } + ] + }, + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/base-objects/v0.1/cutter/relationships", + "title": "IFF template for cutter relationship", + "description": "Cutter template for IFF", + "type": "object", + "properties": { + "hasFilter": { + "relationship": "eclass:0173-1#01-ACK991#016", + "$ref": "https://industry-fusion.org/base-objects/v0.1/link" + } + } + }, + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/base-objects/v0.1/link", + "title": "IFF template for cutter relationship", + "description": "Cutter template for IFF", + "type": "object", + "properties": { + "object": { + "type": "string", + "pattern": "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{0,31}:[a-zA-Z0-9()+,\\-.:=@;$_!*']*[a-zA-Z0-9()+,\\-.:=@;$_!*']$" + } + }, + "required": ["object"] + }, + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/base-objects/v0.1/cutter/properties", + "title": "Cutter properties", + "description": "Properties for class cutter", + "type": "object", + "properties": { + "machine_state": { + "type": "string", + "title": "Machine Status", + "description": "Current status of the machine (Online_Idle, Run, Online_Error, Online_Maintenance, Setup, Testing)", + "enum": [ + "Online_Idle", + "Run", + "Online_Error", + "Online_Maintenance", + "Setup", + "Testing" + ] + } + }, + "required": [ + "machine_state" + ] + } +] \ No newline at end of file diff --git a/semantic-model/datamodel/tools/tests/validation/validation_data1.json b/semantic-model/datamodel/tools/tests/validation/validation_data1.json new file mode 100644 index 00000000..10da1870 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation_data1.json @@ -0,0 +1,5 @@ +{ + "@context": "https://industryfusion.github.io/contexts/v0.1/context.jsonld", + "id": "urn:iff:abc123", + "type": "eclass:0173-1#01-AKJ975#017" +} diff --git a/semantic-model/datamodel/tools/tests/validation/validation_data1.json_id b/semantic-model/datamodel/tools/tests/validation/validation_data1.json_id new file mode 100644 index 00000000..a1064ee6 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation_data1.json_id @@ -0,0 +1 @@ +https://industry-fusion.org/eclass#0173-1#01-AKJ975#017 diff --git a/semantic-model/datamodel/tools/tests/validation/validation_data1.json_result b/semantic-model/datamodel/tools/tests/validation/validation_data1.json_result new file mode 100644 index 00000000..ee71d9c0 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation_data1.json_result @@ -0,0 +1 @@ +The Datafile is compliant with Schema diff --git a/semantic-model/datamodel/tools/tests/validation/validation_data2.json b/semantic-model/datamodel/tools/tests/validation/validation_data2.json new file mode 100644 index 00000000..a7233622 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation_data2.json @@ -0,0 +1,5 @@ +{ + "@context": "https://industryfusion.github.io/contexts/v0.1/context.jsonld", + "id": "myid", + "type": "eclass:0173-1#01-AKJ975#017" +} diff --git a/semantic-model/datamodel/tools/tests/validation/validation_data2.json_id b/semantic-model/datamodel/tools/tests/validation/validation_data2.json_id new file mode 100644 index 00000000..a1064ee6 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation_data2.json_id @@ -0,0 +1 @@ +https://industry-fusion.org/eclass#0173-1#01-AKJ975#017 diff --git a/semantic-model/datamodel/tools/tests/validation/validation_data2.json_result b/semantic-model/datamodel/tools/tests/validation/validation_data2.json_result new file mode 100644 index 00000000..f9eaceba --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation_data2.json_result @@ -0,0 +1,12 @@ +Not Compliant: +[ + { + instancePath: '/id', + schemaPath: '#/properties/id/pattern', + keyword: 'pattern', + params: { + pattern: "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{1,31}:([a-zA-Z0-9()+,.:=@;$_!*'-]|%[0-9a-fA-F]{2})*$" + }, + message: `must match pattern "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{1,31}:([a-zA-Z0-9()+,.:=@;$_!*'-]|%[0-9a-fA-F]{2})*$"` + } +] diff --git a/semantic-model/datamodel/tools/tests/validation/validation_data3.json b/semantic-model/datamodel/tools/tests/validation/validation_data3.json new file mode 100644 index 00000000..ce61a57c --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation_data3.json @@ -0,0 +1,5 @@ +{ + "@context": "https://industryfusion.github.io/contexts/v0.1/context.jsonld", + "id": "urn:iff:abc123", + "type": "mytype" +} diff --git a/semantic-model/datamodel/tools/tests/validation/validation_data3.json_id b/semantic-model/datamodel/tools/tests/validation/validation_data3.json_id new file mode 100644 index 00000000..a1064ee6 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation_data3.json_id @@ -0,0 +1 @@ +https://industry-fusion.org/eclass#0173-1#01-AKJ975#017 diff --git a/semantic-model/datamodel/tools/tests/validation/validation_data3.json_result b/semantic-model/datamodel/tools/tests/validation/validation_data3.json_result new file mode 100644 index 00000000..4372bdb9 --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation_data3.json_result @@ -0,0 +1,10 @@ +Not Compliant: +[ + { + instancePath: '/type', + schemaPath: '#/properties/type/const', + keyword: 'const', + params: { allowedValue: 'eclass:0173-1#01-AKJ975#017' }, + message: 'must be equal to constant' + } +] diff --git a/semantic-model/datamodel/tools/tests/validation/validation_schema.json b/semantic-model/datamodel/tools/tests/validation/validation_schema.json new file mode 100644 index 00000000..72149bdd --- /dev/null +++ b/semantic-model/datamodel/tools/tests/validation/validation_schema.json @@ -0,0 +1,17 @@ +[{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://industry-fusion.org/eclass%230173-1%2301-AKJ975%23017", + "title": "Cutter", + "description": "Cutter template for IFF", + "type": "object", + "properties": { + "type": { + "const": "eclass:0173-1#01-AKJ975#017" + }, + "id": { + "type": "string", + "pattern": "^urn:[a-zA-Z0-9][a-zA-Z0-9-]{1,31}:([a-zA-Z0-9()+,.:=@;$_!*'-]|%[0-9a-fA-F]{2})*$" + } + }, + "required": ["type", "id"] +}] \ No newline at end of file diff --git a/semantic-model/datamodel/tools/validate.js b/semantic-model/datamodel/tools/validate.js new file mode 100644 index 00000000..14c2d4ba --- /dev/null +++ b/semantic-model/datamodel/tools/validate.js @@ -0,0 +1,92 @@ +/** +* Copyright (c) 2023 Intel Corporation +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +'use strict' + +const fs = require('fs') +const url = require('url') +const Ajv = require('ajv/dist/2020') +const yargs = require('yargs') + +const removedKeywords = [ + 'anyOf', 'oneOf', 'if', 'then', 'else', 'prefixItems', 'items', + 'valid', 'error', 'annotation', 'additionalProperties', 'propertyNames', + '$vocabulary', '$defs', 'multipleOf', 'uniqueItems', 'maxContains', + 'minContains', 'maxProperties', 'minPropeties', 'dependentRequired'] +const addedKeywords = ['relationship'] + +const argv = yargs + .option('schema', { + alias: 's', + description: 'Schema File', + demandOption: true, + type: 'string' + }) + .option('datafile', { + alias: 'd', + description: 'File to validate', + demandOption: true, + type: 'string' + }) + .option('schemaid', { + alias: 'i', + description: 'Schema-id to validate', + demandOption: true, + type: 'string' + }) + .help() + .alias('help', 'h') + .argv + +const ajv = new Ajv({ + strict: true, + strictTypes: true, + strictSchema: true, + strictTuples: false +}) + +const schema = fs.readFileSync(argv.s, 'utf8') +const parsedSchema = JSON.parse(schema) + +ajv.addSchema(parsedSchema) + +// This special processing is needed to allow ECLASS integration with +// URL. ECLASS uses IRDI which makes a lot of use of '#' which is +// incompatible with $id definition of JSON-Schema. Workaround +// is to use URL-encoding + +const id = new url.URL(argv.i) +let idFragment = '' +let idPath = '' +if (id.hash !== null) { + idFragment = encodeURIComponent(id.hash) +} +if (id.pathname !== null) { + idPath = id.pathname +} +const idUrl = id.protocol + '//' + id.host + idPath + idFragment + +const data = JSON.parse(fs.readFileSync(argv.d, 'utf8')) + +// Remove all non supported and add all proprietary keywords. +removedKeywords.forEach(kw => ajv.removeKeyword(kw)) +addedKeywords.forEach(kw => ajv.addKeyword({ keyword: kw })) + +if (ajv.validate(idUrl, data)) { + console.log('The Datafile is compliant with Schema') +} else { + console.log('Not Compliant:') + console.log(ajv.errors) +};