Skip to content

Commit

Permalink
Feature: Ability to add schema to Validator in-memory (#17)
Browse files Browse the repository at this point in the history
Signed-off-by: Michiel Mulders <[email protected]>
Signed-off-by: Iliya Savov <[email protected]>
Co-authored-by: isavov <[email protected]>
  • Loading branch information
michielmulders and isavov authored Jun 14, 2023
1 parent 83f679b commit 5c91932
Show file tree
Hide file tree
Showing 10 changed files with 219 additions and 53 deletions.
62 changes: 53 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

This package includes all sorts of tooling for the Hedera NFT ecosystem, including:

1. **Token metadata validation:** Verify your metadata against the [token metadata JSON schema V2](https://github.com/hashgraph/hedera-improvement-proposal/blob/main/HIP/hip-412.md) for NFTs, which returns errors and warnings against the standard.
1. **Token metadata validation:** Verify your metadata against the [token metadata JSON schema V2](https://github.com/hashgraph/hedera-improvement-proposal/blob/main/HIP/hip-412.md) for NFTs, which returns errors and warnings against the standard. You can also define your own token metadata standard and add it to the package to use this schema for validation.
2. **Local metadata validator:** Verify a local folder containing multiple JSON metadata files against the standard before publishing the NFT collection on the Hedera network.
3. **Risk score calculation:** Calculate a risk score for an NFT collection from the token information or by passing a token ID of an NFT on the Hedera testnet or mainnet.
4. **Rarity score calculation:** Calculate the rarity scores for a local folder containing multiple JSON metadata files for an NFT collection.
Expand Down Expand Up @@ -39,13 +39,13 @@ Install the package:
npm i -s @hashgraph/nft-utilities
```

Import the package into your project. You can import the `validator` function and the default schema version for token metadata with `defaultVersion`.
Import the package into your project. You can import the `Validator` class and the default schema version for token metadata with `defaultVersion`.

```js
const { validator, defaultVersion } = require('@hashgraph/nft-utilities');
const { Validator, defaultVersion } = require('@hashgraph/nft-utilities');
```

You can use the `validator` like below.
You can use the `Validator` like below.
1. The first parameter is the JSON object you want to verify against a JSON schema
2. The second parameter is the version of the token metadata JSON schema against which you want to validate your metadata instance. The default value is `2.0.0` (V2). In the future, new functionality might be added, releasing new version numbers.

Expand All @@ -58,7 +58,8 @@ const metadata = {
};
const version = '2.0.0';

const issues = validator(metadata, version);
const validator = new Validator();
const issues = validator.validate(metadata, version);
```

### Interface
Expand Down Expand Up @@ -111,6 +112,49 @@ See: **[/examples/token-metadata-validator](https://github.com/hashgraph/hedera-

### Add custom schema versions

#### Method 1: Use Validator constructor to pass custom schemas

The easiest approach to adding new schemas is using the constructor of the `Validator` class. It accepts an array of JSON objects, each containing a JSON schema and tag for the schema. The tag is used to refer to the schema when validating metadata instances.

Therefore, each tag needs to be unqiue. The following tags can't be used as they are already occupied:

- `1.0.0` -> Refers to token metadata JSON schema V1 (HIP10)
- `2.0.0` -> Refers to token metadata JSON schema V2 (HIP412)

You can add your custom schema like this:

```js
const { Validator } = require('@hashgraph/nft-utilities');

// Define your schema
const customSchema = {
"title": "Token Metadata",
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"description": "Identifies the asset to which this token represents."
}
},
"required": ["name"]
}

// Create Validator instance with custom schema labeled "custom-v1"
const validator = new Validator([{ schemaObject: customSchema, tag: "custom-v1" }]);

// Verify metadata against custom schema
const results = validator.validate(metadataInstance, "custom-v1");
console.log(results);
```

**Examples:** See: [/examples/token-metadata-calculation](https://github.com/hashgraph/hedera-nft-utilities/tree/main/examples/token-metadata-calculation/custom-schema-valid-metadata.js)


#### Method 2: Rebuilding package

> ⚠️ Warning: **This approach requires you to rebuild the package.**
You can add custom JSON schemas to the `/schemas` folder.

You can then add the version to the `schemaMap` in `/schema/index.js` using the following code:
Expand All @@ -128,16 +172,16 @@ When you've added your schema to the map, you can validate against your schema v

### Add custom validation rules

Set custom validation rules by importing new validators from the `/validators` folder into the `index.js` file. You can then add them to the `validator()` function. Stick to the `issues` format of errors and warnings (see section "Issues format" for the detailed description).
Set custom validation rules by importing new validators from the `/validators` folder into the `index.js` file. You can then add them to the `validate()` function. Stick to the `issues` format of errors and warnings (see section "Issues format" for the detailed description).

```js
const { myCustomValidator, schemaValidator } = require("./validators");

const validator = (instance, schemaVersion = defaultVersion) => {
const validate = (instance, schemaVersion = defaultSchemaVersion) => {
let errors = [];
let warnings = [];

const schema = getSchema(schemaVersion)
const schema = this.getSchema(schemaVersion)

// When errors against the schema are found, you don't want to continue verifying the NFT
// Warnings don't matter because they only contain "additional property" warnings that don't break the other validators
Expand Down Expand Up @@ -186,7 +230,7 @@ The `localValidation` expects an absolute path to your metadata files to verify
localValidation("/Users/projects/nft/files");
```

This package uses the `validator` function explained in the [previous section](#token-metadata-validator).
This package uses the `Validator` class explained in the [previous section](#token-metadata-validator).

### Interface

Expand Down
72 changes: 72 additions & 0 deletions examples/token-metadata-validator/custom-schema-valid-metadata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*-
*
* Hedera NFT Utilities
*
* Copyright (C) 2023 Hedera Hashgraph, LLC
*
* 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.
*
*/
const { Validator, defaultVersion } = require('../..');

function main() {
// Define your JSON schema
const customSchema = {
"title": "Token Metadata",
"type": "object",
"additionalProperties": false,
"properties": {
"version": {
"type": "string",
"description": "Semantic version for the metadata JSON format."
},
"name": {
"type": "string",
"description": "Identifies the asset to which this token represents."
}
},
"required": [
"version",
"name"
]
}

// Create Validator instance with custom schema
const validator = new Validator([{ schemaObject: customSchema, tag: "custom-v1" }]);

// Define metadata
const metadataInstance = {
"version": "v3.0.0",
"name": "HANGRY BARBOON #2343",
"image": "ipfs://QmaHVnnp7qAmGADa3tQfWVNxxZDRmTL5r6jKrAo16mSd5y/2343.png"
}

// Verify metadata against custom schema
const results = validator.validate(metadataInstance, "custom-v1");
console.log(results);

/* Output:
{
errors: [],
warnings: [
{
type: 'schema',
msg: "is not allowed to have the additional property 'image'",
path: 'instance'
}
]
}
*/
}

main();
5 changes: 3 additions & 2 deletions examples/token-metadata-validator/invalid-errors-metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* limitations under the License.
*
*/
const { validator, defaultVersion } = require('../..');
const { Validator, defaultVersion } = require('../..');

function main() {
const metadataInstance = {
Expand All @@ -37,7 +37,8 @@ function main() {
]
}

const results = validator(metadataInstance, defaultVersion);
const validator = new Validator();
const results = validator.validate(metadataInstance, defaultVersion);
console.log(results);

/* Output:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* limitations under the License.
*
*/
const { validator, defaultVersion } = require('../..');
const { Validator, defaultVersion } = require('../..');

function main() {
const metadataInstance = {
Expand All @@ -38,7 +38,8 @@ function main() {
"myAdditionalProperty": "Additional properties should be included in properties"
}

const results = validator(metadataInstance, defaultVersion);
const validator = new Validator();
const results = validator.validate(metadataInstance, defaultVersion);
console.log(results);

/* Output:
Expand Down
5 changes: 3 additions & 2 deletions examples/token-metadata-validator/valid-metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* limitations under the License.
*
*/
const { validator, defaultVersion } = require('../..');
const { Validator, defaultVersion } = require('../..');

function main() {
const metadataInstance = {
Expand All @@ -37,7 +37,8 @@ function main() {
]
}

const results = validator(metadataInstance); // by default: verifies metadata against [email protected]
const validator = new Validator();
const results = validator.validate(metadataInstance); // by default: verifies metadata against [email protected]
console.log(results);

/* Output:
Expand Down
6 changes: 3 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* limitations under the License.
*
*/
const { validator, defaultVersion } = require('./validator');
const { Validator, defaultSchemaVersion } = require('./validator');
const { localValidation } = require('./local-validation');
const {
defaultWeights, defaultRiskLevels,
Expand All @@ -27,8 +27,8 @@ const { calculateRarity } = require('./rarity');

module.exports = {
// validation
validator,
defaultVersion,
Validator,
defaultSchemaVersion,

// local validation
localValidation,
Expand Down
5 changes: 3 additions & 2 deletions local-validation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@
* limitations under the License.
*
*/
const { validator } = require('../validator/index');
const { Validator } = require('../validator/index');
const { readFiles, getJSONFilesForDir } = require('../helpers/files');

const validateFiles = (files) => {
let validationResults = {};
const validator = new Validator();

files.forEach(file => {
const result = validator(file.filedata);
const result = validator.validate(file.filedata);
validationResults[file.filename] = result;
});

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hashgraph/nft-utilities",
"version": "1.2.1",
"version": "2.0.0",
"description": "NFT Utilities for Hedera Hashgraph",
"main": "index.js",
"scripts": {
Expand Down
21 changes: 13 additions & 8 deletions tests/validator/validator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@
* limitations under the License.
*
*/
const { validator } = require('../../validator/index');
const { defaultVersion } = require('../../validator/schemas');
const { Validator, defaultSchemaVersion } = require('../../validator/index');
const validMetadata = require('./data/valid-HIP412.json');

describe("Validator function tests", () => {
describe("Schema version tests", () => {
test("it should not return errors for a valid metadata JSON using default schema", () => {
// Arrange
const validator = new Validator();
let metadata = JSON.parse(JSON.stringify(validMetadata));

// Act
const schemaProblems = validator(metadata, defaultVersion);
const schemaProblems = validator.validate(metadata, defaultSchemaVersion);

// Assert
expect(Array.isArray(schemaProblems.errors)).toBe(true);
Expand All @@ -39,10 +39,11 @@ describe("Validator function tests", () => {

test("it should not return errors for a valid metadata JSON using schema version v1.0.0", () => {
// Arrange
const validator = new Validator();
let metadata = JSON.parse(JSON.stringify(validMetadata));

// Act
const schemaProblems = validator(metadata, "1.0.0");
const schemaProblems = validator.validate(metadata, "1.0.0");

// Assert
expect(schemaProblems.warnings.length).toBe(0);
Expand All @@ -51,10 +52,11 @@ describe("Validator function tests", () => {

test("it should not return errors for a valid metadata JSON not passing a schema version (using default version)", () => {
// Arrange
const validator = new Validator();
let metadata = JSON.parse(JSON.stringify(validMetadata));

// Act
const schemaProblems = validator(metadata);
const schemaProblems = validator.validate(metadata);

// Assert
expect(schemaProblems.warnings.length).toBe(0);
Expand All @@ -65,6 +67,7 @@ describe("Validator function tests", () => {
describe("Validator errors", () => {
test("it should only return schema errors when the metadata contains schema errors and also other types of errors like attribute and localization", () => {
// Arrange
const validator = new Validator();
let metadataCopy = JSON.parse(JSON.stringify(validMetadata));
let metadata = {
// missing name, image, and type for [email protected]
Expand All @@ -78,7 +81,7 @@ describe("Validator function tests", () => {
}

// Act
const validationResults = validator(metadata, defaultVersion);
const validationResults = validator.validate(metadata, defaultSchemaVersion);

// Assert
expect(validationResults.errors.length).toBe(3);
Expand All @@ -89,6 +92,7 @@ describe("Validator function tests", () => {

test("it should return all types of errors when there are no schema errors", () => {
// Arrange
const validator = new Validator();
let metadataCopy = JSON.parse(JSON.stringify(validMetadata));
let metadata = {
name: "myname",
Expand All @@ -104,7 +108,7 @@ describe("Validator function tests", () => {
}

// Act
const validationResults = validator(metadata, defaultVersion);
const validationResults = validator.validate(metadata, defaultSchemaVersion);

// Assert
expect(validationResults.errors.length).toBe(1);
Expand All @@ -113,6 +117,7 @@ describe("Validator function tests", () => {

test("it should return all types of errors even when there are additional property warnings and no schema errors", () => {
// Arrange
const validator = new Validator();
let metadataCopy = JSON.parse(JSON.stringify(validMetadata));
let metadata = {
name: "myname",
Expand All @@ -128,7 +133,7 @@ describe("Validator function tests", () => {
}

// Act
const validationResults = validator(metadata, defaultVersion);
const validationResults = validator.validate(metadata, defaultSchemaVersion);

// Assert
expect(validationResults.warnings.length).toBe(1);
Expand Down
Loading

0 comments on commit 5c91932

Please sign in to comment.