diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 989eedd7..d60317c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,7 @@ jobs: tool: - ux - uml2ts + - zod2uml steps: - uses: actions/checkout@v4 - uses: ./.github/actions/build-docker diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 34fdc645..3d91bda9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,6 +18,7 @@ jobs: tool: - ux - uml2ts + - zod2uml steps: - uses: actions/checkout@v4 - uses: ./.github/actions/build-docker @@ -34,6 +35,7 @@ jobs: matrix: tool: - uml2ts + - zod2uml target: - bun-linux-x64 - bun-linux-arm64 diff --git a/Makefile b/Makefile index bb6cd175..39c4a311 100644 --- a/Makefile +++ b/Makefile @@ -31,10 +31,14 @@ else TEST_FLAGS := --github-output --race --trace endif -build: generate bin/ux bin/devops .make/buf_build packages/tdl/dist packages/ts/dist +build: generate .make/buf_build build_go build_ts + +build_go: bin/ux bin/devops +build_ts: bin/uml2ts bin/zod2uml packages/tdl/dist packages/ts/dist + test: .make/go_test .make/ts_test generate: ${GO_PB_SRC} -docker: .make/docker_ux .make/docker_uml2ts +docker: .make/docker_ux .make/docker_uml2ts .make/docker_zod2uml format: .make/dprint .make/go_fmt .make/buf_format lint: .make/buf_lint .make/go_lint tidy: go.sum @@ -68,6 +72,9 @@ bin/ux: $(shell $(DEVOPS) list --go --exclude-tests) bin/uml2ts: $(shell $(DEVOPS) list --ts --exclude-tests) bun build --cwd packages/uml2ts index.ts --compile --outfile ${WORKING_DIR}/$@ +bin/zod2uml: $(shell $(DEVOPS) list --ts --exclude-tests) + bun build --cwd packages/zod2uml index.ts --compile --outfile ${WORKING_DIR}/$@ + bin/devops: $(shell $(DEVOPS) list --go --exclude-tests) go -C cmd/devops build -o ${WORKING_DIR}/$@ @@ -103,6 +110,10 @@ go.sum: go.mod ${GO_SRC} docker build -f docker/uml2ts/Dockerfile -t uml2ts ${WORKING_DIR} @touch $@ +.make/docker_zod2uml: ${TS_SRC} $(wildcard docker/zod2uml/*) + docker build -f docker/zod2uml/Dockerfile -t zod2uml ${WORKING_DIR} + @touch $@ + .make/go_test: ${GO_SRC} | bin/ginkgo bin/ux bin/uml2ts $(GINKGO) run ${TEST_FLAGS} $(sort $(dir $?)) @touch $@ diff --git a/bun.lockb b/bun.lockb index d26b45ef..537901f0 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docker/uml2ts/Dockerfile b/docker/uml2ts/Dockerfile index 0b110ce9..2e96368b 100644 --- a/docker/uml2ts/Dockerfile +++ b/docker/uml2ts/Dockerfile @@ -12,6 +12,8 @@ COPY package.json . COPY packages/tdl/package.json packages/tdl/ COPY packages/ts/package.json packages/ts/ COPY packages/uml2ts/package.json packages/uml2ts/ +COPY packages/zod/package.json packages/zod/ +COPY packages/zod2uml/package.json packages/zod2uml/ COPY bun.lockb . RUN bun install --frozen-lockfile --production diff --git a/docker/zod2uml/.dockerignore b/docker/zod2uml/.dockerignore new file mode 100644 index 00000000..34d728dd --- /dev/null +++ b/docker/zod2uml/.dockerignore @@ -0,0 +1,22 @@ +node_modules +Dockerfile* +docker-compose* +.dockerignore +.git +.gitignore +README.md +LICENSE +.vscode +Makefile +.env +.editorconfig +.idea +coverage* +.config +.github +.make +cli +docker +pkg +proto +src diff --git a/docker/zod2uml/Dockerfile b/docker/zod2uml/Dockerfile new file mode 100644 index 00000000..328f8c72 --- /dev/null +++ b/docker/zod2uml/Dockerfile @@ -0,0 +1,39 @@ +# syntax=docker/dockerfile:1 +FROM --platform=$BUILDPLATFORM oven/bun:1.1.34 AS base +ARG BUILDPLATFORM +WORKDIR /build + +FROM --platform=$BUILDPLATFORM base AS install + +RUN mkdir -p gen/proto/{es,ts} packages/{ts,uml,uml2ts} + +# Need everything because the lockfile references everything +COPY package.json . +COPY packages/tdl/package.json packages/tdl/ +COPY packages/ts/package.json packages/ts/ +COPY packages/uml2ts/package.json packages/uml2ts/ +COPY packages/zod/package.json packages/zod/ +COPY packages/zod2uml/package.json packages/zod2uml/ +COPY bun.lockb . + +RUN bun install --frozen-lockfile --production + +FROM --platform=$BUILDPLATFORM install AS build +COPY --from=install /build/node_modules . + +COPY packages/tdl/ packages/tdl/ +COPY packages/zod/ packages/zod/ +COPY packages/zod2uml/ . + +RUN bun build \ + --compile \ + --minify \ + --sourcemap ./index.ts \ + --outfile ./dist/zod2uml + +FROM --platform=$BUILDPLATFORM ubuntu:noble-20241015 AS test +COPY --from=build /build/dist/zod2uml . + +FROM --platform=$BUILDPLATFORM oven/bun:1.1.34-distroless +COPY --from=build /build/dist/zod2uml /bin/ +ENTRYPOINT ["/bin/zod2uml"] diff --git a/go.sum b/go.sum index 41131327..569f9b1f 100644 --- a/go.sum +++ b/go.sum @@ -179,12 +179,8 @@ github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 h1:vkHw5I/plNdTr435 github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231/go.mod h1:murToZ2N9hNJzewjHBgfFdXhZKjY3z5cYC1VXk+lbFE= github.com/pulumi/esc v0.10.0 h1:jzBKzkLVW0mePeanDRfqSQoCJ5yrkux0jIwAkUxpRKE= github.com/pulumi/esc v0.10.0/go.mod h1:2Bfa+FWj/xl8CKqRTWbWgDX0SOD4opdQgvYSURTGK2c= -github.com/pulumi/pulumi/pkg/v3 v3.138.0 h1:a+MMvCrvsju4YFVYEwPBtcW7XqsEIV3B+FMblisxEkM= -github.com/pulumi/pulumi/pkg/v3 v3.138.0/go.mod h1:xpaeNeKmM2KLafWwm8TlvJGbWtwEwlrK88U6FvXucpY= github.com/pulumi/pulumi/pkg/v3 v3.139.0 h1:vWwAvEL0US8Z6emggo+OI/ZducMq9m9LKCJbEDbERvU= github.com/pulumi/pulumi/pkg/v3 v3.139.0/go.mod h1:LtEg3PKYzbFnAKWFOENGRwGRNBpv16xKtbK+7esiSHU= -github.com/pulumi/pulumi/sdk/v3 v3.138.0 h1:1feN0YU1dHnbNw+cHaenmx3AgU0DEiKQbvjxaGQuShk= -github.com/pulumi/pulumi/sdk/v3 v3.138.0/go.mod h1:PvKsX88co8XuwuPdzolMvew5lZV+4JmZfkeSjj7A6dI= github.com/pulumi/pulumi/sdk/v3 v3.139.0 h1:oBGP58b2Yw1HbPA3LHO/jHmOaVqFSEjw5BXd36ZbPLw= github.com/pulumi/pulumi/sdk/v3 v3.139.0/go.mod h1:PvKsX88co8XuwuPdzolMvew5lZV+4JmZfkeSjj7A6dI= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/packages/zod/index.ts b/packages/zod/index.ts new file mode 100644 index 00000000..1f0e8d72 --- /dev/null +++ b/packages/zod/index.ts @@ -0,0 +1,19 @@ +import type { Field, Spec, Type } from '@unmango/tdl/v1alpha1/tdl'; +import { create } from '@unmango/tdl/spec'; +import { ZodObject, ZodType, type ZodSchema } from 'zod'; + +type Schema = Record; + +export function parse(schema: Schema): Spec { + if (schema instanceof ZodObject) { + return create({ + name: schema._def.description, + }); + } + + return create(); +} + +export function parseObject(name: string, schema: ZodType): Spec { + return create({ name }); +} diff --git a/packages/zod/package.json b/packages/zod/package.json new file mode 100644 index 00000000..eb6c2bd3 --- /dev/null +++ b/packages/zod/package.json @@ -0,0 +1,15 @@ +{ + "name": "@unmango/2zod", + "module": "index.ts", + "type": "module", + "dependencies": { + "@unmango/tdl": "workspace:*", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/packages/zod/testdata/zod.ts b/packages/zod/testdata/zod.ts new file mode 100644 index 00000000..f02e110b --- /dev/null +++ b/packages/zod/testdata/zod.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +const schema = z.object({ + thing: z.string(), +}).describe('test'); + +export default schema; diff --git a/packages/zod/tsconfig.json b/packages/zod/tsconfig.json new file mode 100644 index 00000000..238655f2 --- /dev/null +++ b/packages/zod/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/packages/zod2uml/index.ts b/packages/zod2uml/index.ts new file mode 100644 index 00000000..99727a77 --- /dev/null +++ b/packages/zod2uml/index.ts @@ -0,0 +1,51 @@ +import { parse, parseObject } from '@unmango/2zod'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { z, type ZodTypeAny } from 'zod'; + +if (process.argv.length !== 3) { + console.log('path to zod schema definition script is required'); + process.exit(1); +} + +const schemaPath = process.argv[2]; +if (!await fs.exists(schemaPath)) { + console.log('not found: %s', schemaPath); + process.exit(1); +} + +const module = z.record(z.string(), z.unknown()); +const exports = module.parse(await import(schemaPath)); + +if (exports.default) { + if (!isZod(exports.default)) { + console.log('default export was not a supported zod type'); + process.exit(1); + } + + const ext = path.extname(schemaPath); + const name = path.basename(schemaPath, ext); + const spec = parseObject(name, exports.default); + console.log(JSON.stringify(spec)); +} else { + const schemas: Record = {}; + for (const [k, v] of Object.entries(exports)) { + if (!isZod(v)) { + console.log('exported member %s was not a supported zod type'); + process.exit(1); + } + + schemas[k] = v; + } + + const spec = parse(schemas); + console.log(JSON.stringify(spec)); +} + +function isZod(x: unknown): x is ZodTypeAny { + if (!x?.constructor.name) { + return false; + } + + return ['ZodObject'].includes(x.constructor.name); +} diff --git a/packages/zod2uml/package.json b/packages/zod2uml/package.json new file mode 100644 index 00000000..e8d6db3f --- /dev/null +++ b/packages/zod2uml/package.json @@ -0,0 +1,16 @@ +{ + "name": "zod2uml", + "license": "GPL-3.0-only", + "module": "index.ts", + "type": "module", + "dependencies": { + "@unmango/2zod": "workspace:*", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/packages/zod2uml/tsconfig.json b/packages/zod2uml/tsconfig.json new file mode 100644 index 00000000..238655f2 --- /dev/null +++ b/packages/zod2uml/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}