Skip to content

Commit

Permalink
fix schemata
Browse files Browse the repository at this point in the history
  • Loading branch information
dskvr committed Jan 8, 2025
1 parent f56afb2 commit 7a14cdc
Show file tree
Hide file tree
Showing 48 changed files with 6,321 additions and 0 deletions.
11 changes: 11 additions & 0 deletions libraries/schemata/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

dist/*
**/dist/*
!**/dist
!**/dist/.gitkeep

**/node_modules
**/packages/**/*.ts
**/src/serialize.ts
**/src/types.ts
**/*.code-workspace
2 changes: 2 additions & 0 deletions libraries/schemata/@/message/filter.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
allOf:
- $ref: "nips/nip-01/messages/filter/schema.yaml"
2 changes: 2 additions & 0 deletions libraries/schemata/@/note.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
allOf:
- $ref: "nips/nip-01/note/schema.yaml"
2 changes: 2 additions & 0 deletions libraries/schemata/@/secp256k1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
allOf:
- $ref: "nips/nip-01/secp256k1/schema.yaml"
2 changes: 2 additions & 0 deletions libraries/schemata/@/tag.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
allOf:
- $ref: "nips/nip-01/tag/schema.yaml"
2 changes: 2 additions & 0 deletions libraries/schemata/@/tag/_A.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
allOf:
- $ref: "nips/nip-01/tag/a/schema.yaml"
2 changes: 2 additions & 0 deletions libraries/schemata/@/tag/_E.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
allOf:
- $ref: "nips/nip-01/tag/e/schema.yaml"
2 changes: 2 additions & 0 deletions libraries/schemata/@/tag/_P.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
allOf:
- $ref: "nips/nip-01/tag/p/schema.yaml"
2 changes: 2 additions & 0 deletions libraries/schemata/@/tag/a.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
allOf:
- $ref: "nips/nip-01/tag/a/schema.yaml"
2 changes: 2 additions & 0 deletions libraries/schemata/@/tag/d.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
allOf:
- $ref: "nips/nip-01/tag/d/schema.yaml"
2 changes: 2 additions & 0 deletions libraries/schemata/@/tag/e.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
allOf:
- $ref: "nips/nip-01/tag/e/schema.yaml"
2 changes: 2 additions & 0 deletions libraries/schemata/@/tag/p.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
allOf:
- $ref: "nips/nip-01/tag/p/schema.yaml"
2 changes: 2 additions & 0 deletions libraries/schemata/@/tag/t.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
allOf:
- $ref: "nips/nip-01/tag/t/schema.yaml"
52 changes: 52 additions & 0 deletions libraries/schemata/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Makefile that converts all *.yaml in nips/ and @ -> dist/,
# rewrites references to absolute paths, then dereferences them.

NIPS_DIR := nips
ALIASES_DIR := @
DIST_DIR := dist

YAMLCONVERT := yaml-convert
REWRITE_SCRIPT := node $(realpath scripts/rewriteRefs.js)
DEREF_SCRIPT := node $(realpath scripts/deref.js)

SCHEMA_YAMLS := $(shell find $(NIPS_DIR) -type f -name "*.yaml")
ALIAS_YAMLS := $(shell find $(ALIASES_DIR) -type f -name "*.yaml")
ALL_YAMLS := $(SCHEMA_YAMLS) $(ALIAS_YAMLS)

JSON_SCHEMAS := $(patsubst %.yaml,$(DIST_DIR)/%.json,$(ALL_YAMLS))

all: convert_json rewrite_refs dereference_json

$(DIST_DIR)/%.json: %.yaml
@echo "Converting $< -> $@"
@mkdir -p $(dir $@)
@$(YAMLCONVERT) $< > $@
@sed -i '' 's/\.yaml/.json/g' $@ || sed -i 's/\.yaml/.json/g' $@

.PHONY: convert_json
convert_json: FORCE $(JSON_SCHEMAS)

.PHONY: rewrite_refs
rewrite_refs: convert_json
@echo "Rewriting references in dist/..."
@cd $(DIST_DIR) && \
for f in $(patsubst $(DIST_DIR)/%,%,$(JSON_SCHEMAS)); do \
echo " -> rewriting $$f"; \
$(REWRITE_SCRIPT) $$f $$f; \
done

.PHONY: dereference_json
dereference_json: rewrite_refs
@echo "Dereferencing JSON schemas in dist/..."
@cd $(DIST_DIR) && \
for f in $(patsubst $(DIST_DIR)/%,%,$(JSON_SCHEMAS)); do \
echo " -> dereferencing $$f"; \
$(DEREF_SCRIPT) $$f $$f; \
done

.PHONY: clean
clean:
@rm -rf $(DIST_DIR)

.PHONY: FORCE
FORCE:
45 changes: 45 additions & 0 deletions libraries/schemata/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# nostrability schemata

A template for simplifying the validation of nostr events, their attriobutes and their respective tags using JSON-Schema standards.

## Usage
1. Download ZIP file (all languages) or include package (js only for now)
2. Validate `.json` schemas against nostr events.

## Use in your own pipline
You shouldn't. You should write a wrapper or use one that already exists. Wrappers **must** use the following typings.
```
type NSchemaResult [ boolean, NSchemaMessage[] ]
interface NSchemaMessage {
level: "info" | "warning" | "error"
message: string
}
```

And provide the following interface:
```
validate(event: NostrEvent): NSchemaResult
validateMany(events: NostrEvent[]): NSchemaResult[]
```

## Contribute

### Setup
1. Fork the repo.
2. `npm/yarn/pnpm install`
3. `npm/yarn/pnpm build`
4. `npm/yarn/pnpm test`

## Writing Schemas
Familiarize yourself with the aliases section and the file structure.
1. Create a new directory for your NIP, and a directory for each kind.
...this is going to be annoying to write, so it should probbaly be automated.

## FS Conventions
This toolkit uses path conventions for build and testing schemata.

`nips/nip-XY/kind-N/schema.yaml`

Kinds are assumed to belong to a NIP, but if you are working with an experimental kind, you won't have a NIP. For these situations, simply place the kind into a nipless "nipless"

`nips/nipless/kind-X`
233 changes: 233 additions & 0 deletions libraries/schemata/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
/**
* build.js
*
* 1) Finds all .json in dist/@ and dist/nips.
* 2) Builds named exports in dist/bundle/schemas.js with custom logic:
* - If parent (or grandparent) is "tag":
* * File named "_A" => "ATagSchema", "_P" => "PTagSchema", etc.
* * Single letter like "a" => "aTagSchema"
* * "schema.json" in "tag/" => "tagSchema"
* - If file is dist/@/note.json => "noteSchema", etc.
* - If file base is "schema" => skip it, to avoid "SchemaSchema".
* - If file base starts with "schema." => remove "schema." => uppercase first letter of remainder.
* - Remove invalid chars (like @, -).
* 3) Outputs dist/bundle/schemas.js + .d.ts, runs esbuild => dist/bundle/schemas.bundle.js
*/

import esbuild from 'esbuild';
import {
readdirSync,
statSync,
writeFileSync,
mkdirSync,
existsSync
} from 'fs';
import { resolve, join, basename, dirname } from 'path';

const aliasDir = resolve('dist/@');
const nipsDir = resolve('dist/nips');
const bundleDir = resolve('dist/bundle');

function getAllJsonFiles(dir) {
if (!existsSync(dir)) return [];
const entries = readdirSync(dir, { withFileTypes: true });
return entries.flatMap((ent) => {
const full = join(dir, ent.name);
if (ent.isDirectory()) {
return getAllJsonFiles(full);
}
if (ent.name.endsWith('.json')) {
return [full];
}
return [];
});
}

/**
* Remove invalid chars like "@", "-", etc. so we produce a valid JS identifier.
*/
function sanitize(str) {
return str.replace(/[^a-zA-Z0-9]/g, '');
}

/**
* "kind-3" => "kind3", "client-req" => "clientReq"
*/
function camelCaseHyphens(str) {
const parts = str.split('-').filter(Boolean);
if (!parts.length) return '';
const [first, ...rest] = parts;
const lowered = first.toLowerCase();
const appended = rest
.map(s => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase())
.join('');
return lowered + appended;
}

/**
* If file base is exactly "schema", we remove it (avoid "SchemaSchema").
* If it starts with "schema.", remove "schema." => uppercase the remainder's first letter.
*/
function processBaseName(baseName) {
if (baseName === 'schema') {
return '';
}
if (baseName.startsWith('schema.')) {
const after = baseName.slice('schema.'.length);
return after.charAt(0).toUpperCase() + after.slice(1);
}
return baseName;
}

/**
* If the folder path includes "tag", we do special logic:
* - If file is "_A" => "ATagSchema"
* - If file is "p" => "pTagSchema"
* - If file is "schema" => "tagSchema"
*/
function handleTagCase(dirParts, baseName) {
const lastDir = dirParts[dirParts.length - 1] || '';
const secondLast = dirParts[dirParts.length - 2] || '';

if (lastDir === 'tag' && baseName === 'schema') {
return 'tagSchema';
}
if (secondLast === 'tag') {
if (baseName === 'schema') {
let name = lastDir;
if (name.startsWith('_') && name.length > 1) {
name = name.charAt(1).toUpperCase() + name.slice(2);
}
return name + 'TagSchema';
} else {
return '';
}
}
if (lastDir === 'tag' && baseName) {
if (baseName.startsWith('_') && baseName.length > 1) {
const letter = baseName.charAt(1).toUpperCase() + baseName.slice(2);
return letter + 'TagSchema';
}
return baseName + 'TagSchema';
}

return '';
}

/**
* Generate exports for dist/nips
*/
function generateNipsExports(filePath) {
const distRoot = resolve('dist');
const relativePath = filePath.replace(distRoot, '').replace(/^[\\/]/, '');

const baseName = basename(filePath, '.json');
const processed = processBaseName(baseName);

const dirParts = dirname(filePath).split(/[/\\]/).filter(Boolean);

const tagResult = handleTagCase(dirParts, baseName);
if (tagResult) {
return { exportName: sanitize(tagResult), relativePath };
}

const parent = dirParts[dirParts.length - 1] || '';
const parentCased = camelCaseHyphens(parent);
let combined = parentCased + processed;
if (!combined) {
combined = 'Unnamed';
}
combined += 'Schema';

return { exportName: sanitize(combined), relativePath };
}

/**
* For dist/@:
* If we have direct parent '@' => e.g. dist/@/note.json => "noteSchema" in lowercase
* If we have a "tag" subfolder => handleTagCase
*/
function generateAliasExports(filePath) {
const distRoot = resolve('dist');
const relativePath = filePath.replace(distRoot, '').replace(/^[\\/]/, '');

const baseName = basename(filePath, '.json');
const processed = processBaseName(baseName);

const dirParts = dirname(filePath).split(/[/\\]/).filter(Boolean);

const last = dirParts[dirParts.length - 1] || '';
if (last === '@') {
const final = processed.toLowerCase() + 'Schema';
return { exportName: sanitize(final), relativePath };
}

const tagResult = handleTagCase(dirParts, baseName);
if (tagResult) {
return { exportName: sanitize(tagResult), relativePath };
}

const parent = dirParts[dirParts.length - 1] || '';
const parentCased = camelCaseHyphens(parent);
let combined = parentCased + processed;
if (!combined) {
combined = 'Unnamed';
}
combined += 'Schema';

return { exportName: sanitize(combined), relativePath };
}

/**
* If two exports share the same name, keep the first (nips).
*/
function prioritizeExports(exports) {
const seen = new Set();
return exports.filter(({ exportName }) => {
if (seen.has(exportName)) return false;
seen.add(exportName);
return true;
});
}

function main() {
const aliasFiles = getAllJsonFiles(aliasDir);
const nipsFiles = getAllJsonFiles(nipsDir);

const aliasExports = aliasFiles.map(generateAliasExports);
const nipsExports = nipsFiles.map(generateNipsExports);

const combined = prioritizeExports([...nipsExports, ...aliasExports]);

const exportLines = combined.map(({ exportName, relativePath }) =>
`export { default as ${exportName} } from '../${relativePath}';`
);

const typeLines = combined.map(({ exportName }) =>
`declare const ${exportName}: unknown;\nexport { ${exportName} };`
);

if (!existsSync(bundleDir)) {
mkdirSync(bundleDir, { recursive: true });
}

const schemasFile = join(bundleDir, 'schemas.js');
const dtsFile = join(bundleDir, 'schemas.d.ts');

writeFileSync(schemasFile, exportLines.join('\n'));
writeFileSync(dtsFile, typeLines.join('\n'));

esbuild.build({
entryPoints: [schemasFile],
outfile: join(bundleDir, 'schemas.bundle.js'),
format: 'esm',
platform: 'node',
target: 'es2020',
sourcemap: true,
minify: true,
})
.then(() => console.log('Schemas bundled successfully!'))
.catch(() => process.exit(1));
}

main();
5 changes: 5 additions & 0 deletions libraries/schemata/nips/nip-01/kind-1/schema.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
$id: "https://schemata.nostr.watch/note/kind/1"
$schema: http://json-schema.org/draft-07/schema#
title: kind1
allOf:
- $ref: "@/note.yaml"
Loading

0 comments on commit 7a14cdc

Please sign in to comment.