Skip to content

Commit

Permalink
Merge pull request #950 from IntersectMBO/feat/942-create-govtool-met…
Browse files Browse the repository at this point in the history
…adata-submission-service

feat(942): create govtool metadata submission service
  • Loading branch information
MSzalowski committed May 8, 2024
2 parents 151093d + 0e6b56d commit 18b3c51
Show file tree
Hide file tree
Showing 21 changed files with 4,256 additions and 2 deletions.
2 changes: 2 additions & 0 deletions govtool/packages/submission-tool/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
coverage
node_modules
70 changes: 70 additions & 0 deletions govtool/packages/submission-tool/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Metadata Service Package

🔍 This package provides a set of tools for managing metadata. It includes a `MetadataService` for handling metadata operations and a `MetadataProvider` for providing metadata context to React components.

![Statements](https://img.shields.io/badge/statements-96.33%25-brightgreen.svg?style=flat)
![Branches](https://img.shields.io/badge/branches-80%25-yellow.svg?style=flat)
![Functions](https://img.shields.io/badge/functions-90.9%25-brightgreen.svg?style=flat)
![Lines](https://img.shields.io/badge/lines-96.33%25-brightgreen.svg?style=flat)

## Getting started

First, install the package in your project:

```bash
yarn add metadata-service
```

### MetadataProvider

Wrap your application in the `MetadataProvider`:

```jsx
import { MetadataProvider } from "metadata-service";

function App() {
return (
<MetadataProvider>
<YourComponent />
</MetadataProvider>
);
}
```

Now you can use the `useMetadata` hook to access metadata context in your components:

```jsx
import { useMetadata } from "metadata-service";

function YourComponent() {
const metadata = useMetadata();

// Use the metadata here...

return <div>{/* Your component's JSX... */}</div>;
}
```

### MetadataService

You can also use the `MetadataService` directly to perform metadata operations:

```jsx
import { MetadataService } from "metadata-service";

const metadataService = new MetadataService({
cip: CIP_Reference["0108"],
hashAlgorithm: "blake2b-256",
body: {
title: "My title",
abstract: "My abstract",
motivation: "My motivation",
rationale: "My rationale",
references: [{ label: "some url", uri: "http://some.url" }],
},
});

metadataService.initialize().then((service) => {
// ...use the service here
});
```
6 changes: 6 additions & 0 deletions govtool/packages/submission-tool/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
presets: [
"@babel/preset-env", // Include any other presets you may need
"@babel/preset-react", // Add @babel/preset-react
],
};
1 change: 1 addition & 0 deletions govtool/packages/submission-tool/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./src";
21 changes: 21 additions & 0 deletions govtool/packages/submission-tool/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/

/** @type {import('jest').Config} */
const config = {
clearMocks: true,
collectCoverage: true,
coverageDirectory: "coverage",
coverageProvider: "v8",
coverageReporters: ["json-summary"],
testEnvironment: "jsdom",
preset: "ts-jest",
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
},
};

module.exports = config;
5 changes: 5 additions & 0 deletions govtool/packages/submission-tool/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
require("@testing-library/jest-dom");

const { TextDecoder, TextEncoder } = require("text-encoding");
global.TextDecoder = TextDecoder;
global.TextEncoder = TextEncoder;
30 changes: 30 additions & 0 deletions govtool/packages/submission-tool/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "govtool-submission-tool",
"version": "0.0.1",
"scripts": {
"test": "jest"
},
"dependencies": {
"blakejs": "^1.2.1",
"jsonld": "^8.3.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"zod": "^3.23.6"
},
"devDependencies": {
"@babel/preset-env": "^7.24.5",
"@babel/preset-react": "^7.24.1",
"@testing-library/dom": "^10.1.0",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
"@types/jest": "^29.5.12",
"@types/jsonld": "^1.5.13",
"@types/react": "^18.3.1",
"@types/text-encoding": "^0.0.39",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"text-encoding": "^0.7.0",
"ts-jest": "^29.1.2",
"typescript": "^5.4.5"
}
}
49 changes: 49 additions & 0 deletions govtool/packages/submission-tool/src/consts/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export const CIP_0108_CONTEXT = {
"@language": "en-us",
CIP100:
"https://github.com/cardano-foundation/CIPs/blob/master/CIP-0100/README.md#",
CIP108:
"https://github.com/cardano-foundation/CIPs/blob/master/CIP-0108/README.md#",
hashAlgorithm: "CIP100:hashAlgorithm",
body: {
"@id": "CIP108:body",
"@context": {
references: {
"@id": "CIP108:references",
"@container": "@set" as const,
"@context": {
GovernanceMetadata: "CIP100:GovernanceMetadataReference",
Other: "CIP100:OtherReference",
label: "CIP100:reference-label",
uri: "CIP100:reference-uri",
referenceHash: {
"@id": "CIP108:referenceHash",
"@context": {
hashDigest: "CIP108:hashDigest",
hashAlgorithm: "CIP100:hashAlgorithm",
},
},
},
},
title: "CIP108:title",
abstract: "CIP108:abstract",
motivation: "CIP108:motivation",
rationale: "CIP108:rationale",
},
},
authors: {
"@id": "CIP100:authors",
"@container": "@set" as const,
"@context": {
name: "http://xmlns.com/foaf/0.1/name",
witness: {
"@id": "CIP100:witness",
"@context": {
witnessAlgorithm: "CIP100:witnessAlgorithm",
publicKey: "CIP100:publicKey",
signature: "CIP100:signature",
},
},
},
},
};
1 change: 1 addition & 0 deletions govtool/packages/submission-tool/src/consts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./context";
4 changes: 4 additions & 0 deletions govtool/packages/submission-tool/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./consts";
export * from "./schemas";
export * from "./services";
export * from "./types";
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { render, screen } from "@testing-library/react";
import { MetadataProvider, useMetadata } from "@/providers/MetadataProvider";

describe("MetadataProvider", () => {
it("renders its children", () => {
render(
<MetadataProvider>
<div>Child Component</div>
</MetadataProvider>
);

const childComponent = screen.getByText("Child Component");
expect(childComponent).toBeDefined();
});

it("provides the validate and build functions in the context", () => {
const TestComponent = () => {
const { validate, build } = useMetadata();
expect(typeof validate).toBe("function");
expect(typeof build).toBe("function");

return null;
};

render(
<MetadataProvider>
<TestComponent />
</MetadataProvider>
);
});

it("throws an error when useMetadata is used outside of MetadataProvider", () => {
const TestComponent = () => {
useMetadata();
return null;
};

expect(() => render(<TestComponent />)).toThrow(
"useMetadata must be used within a MetadataProvider"
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
createContext,
useContext,
useMemo,
useCallback,
PropsWithChildren,
} from "react";

import { MetadataService } from "@/services";
import { MetadataConfig } from "@/types";

type MetadataContextValues = {
validate: (data: MetadataConfig) => void;
build: (config: MetadataConfig) => Promise<MetadataService>;
};

const MetadataContext = createContext<MetadataContextValues | null>(null);

/**
* Provides metadata validation and building functionality to its children components.
* @param children - The child components to be wrapped by the MetadataProvider.
*/
export const MetadataProvider = ({ children }: PropsWithChildren) => {
/**
* Validates the metadata configuration.
*
* @param data - The metadata configuration to validate.
* @returns A promise that resolves to the validation result.
*/
const validate = useCallback(
(data: MetadataConfig) => new MetadataService(data).validateMetadata(),
[]
);

/**
* Builds the metadata using the provided configuration.
* @param config The configuration for building the metadata.
* @returns A promise that resolves to the built metadata.
*/
const build = useCallback(
(config: MetadataConfig) => new MetadataService(config).build(),
[]
);

const value = useMemo(() => ({ validate, build }), [validate, build]);

return (
<MetadataContext.Provider value={value}>
{children}
</MetadataContext.Provider>
);
};

/**
* Custom hook that provides access to the metadata context.
* @returns The metadata context.
* @throws {Error} If used outside of a MetadataProvider.
*/
export const useMetadata = () => {
const context = useContext(MetadataContext);
if (!context) {
throw new Error("useMetadata must be used within a MetadataProvider");
}
return context;
};
14 changes: 14 additions & 0 deletions govtool/packages/submission-tool/src/schemas/cipSchemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { z } from "zod";

export const CIP0108ValidationSchema = z.object({
title: z.string().max(80),
abstract: z.string().max(2500),
motivation: z.string(),
rationale: z.string(),
references: z.array(
z.object({
label: z.string(),
uri: z.string().url(),
})
),
});
1 change: 1 addition & 0 deletions govtool/packages/submission-tool/src/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./cipSchemas";
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { CIP_Reference } from "..";
import { MetadataService } from "./MetadataService";

describe("MetadataService", () => {
it("should initialize jsonld and hash", async () => {
// Arrange
const metadataService = new MetadataService({
cip: CIP_Reference["0108"],
hashAlgorithm: "blake2b-256",
body: {
title: "123",
abstract: "My abstract",
motivation: "My motivation",
rationale: "My rationale",
references: [{ label: "some url", uri: "http://some.url" }],
},
});

// Act
await metadataService.initialize();
const jsonld = metadataService.jsonld;
const hash = metadataService.hash;

// Assert
expect(jsonld).toBeDefined();
expect(jsonld).not.toBeNull();
// TODO: Add structure assertions to the jsonld

expect(hash).toBeDefined();
expect(hash).not.toBeNull();
});

it("should fail on body validation", async () => {
try {
new MetadataService({
cip: CIP_Reference["0108"],
hashAlgorithm: "blake2b-256",
body: {
// For the testing purposes
// @ts-expect-error
title: 123,
abstract: "My abstract",
motivation: "My motivation",
rationale: "My rationale",
references: [{ label: "some url", uri: "http://some.url" }],
},
});
} catch (error) {
expect(error).toBeDefined();
expect((error as any).message).toBe("Invalid metadata body");
}
});
});

0 comments on commit 18b3c51

Please sign in to comment.