Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(942): create govtool metadata submission service #950

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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");
}
});
});
Loading
Loading