Skip to content

Commit

Permalink
Merge pull request #6 from fortanix/mkrause/241111-upgrade-effect-3.10
Browse files Browse the repository at this point in the history
Upgrade to Effect v3.10 (stable Effect Schema)
  • Loading branch information
mkrause authored Nov 11, 2024
2 parents b447cef + 4d049cb commit 93f35dc
Show file tree
Hide file tree
Showing 13 changed files with 743 additions and 933 deletions.
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@

# openapi-to-effect

Generate [@effect/schema](https://www.npmjs.com/package/@effect/schema) definitions from an [OpenAPI](https://www.openapis.org) document.

Note that `@effect/schema` is currently in pre-stable version, and thus there will likely be breaking changes in the future.
Generate [Effect Schema](https://effect.website/docs/schema/introduction) definitions from an [OpenAPI](https://www.openapis.org) document.

**Features:**

Expand All @@ -20,7 +18,7 @@ Note that `@effect/schema` is currently in pre-stable version, and thus there wi

- We currently only support [OpenAPI v3.1](https://spec.openapis.org/oas/latest.html) documents.
- Only JSON is supported for the OpenAPI document format. For other formats like YAML, run it through a [converter](https://onlineyamltools.com/convert-yaml-to-json) first.
- The input must be a single OpenAPI document. Cross-document [references](https://swagger.io/docs/specification/using-ref/) are not currently supported.
- The input must be a single OpenAPI document. Cross-document [references](https://swagger.io/docs/specification/using-ref) are not currently supported.
- The `$allOf` operator currently only supports schemas of type `object`. Generic intersections are not currently supported.

## Usage
Expand All @@ -31,7 +29,7 @@ This package exposes an `openapi-to-effect` command:
npx openapi-to-effect <command> <args>
```

### Generating `@effect/schema` code with the `gen` command
### Generating Effect Schema code with the `gen` command

The `gen` command takes the path to an OpenAPI v3.1 document (in JSON format), the path to the output directory, and optionally a spec file to configure the output:

Expand Down Expand Up @@ -139,7 +137,7 @@ export default {
**output/example.ts**

```ts
import { Schema as S } from '@effect/schema';
import { Schema as S } from 'effect';

/* Category */

Expand Down
1,595 changes: 706 additions & 889 deletions package-lock.json

Large diffs are not rendered by default.

20 changes: 9 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.8.0",
"license": "MPL-2.0",
"homepage": "https://github.com/fortanix/openapi-to-effect",
"description": "OpenAPI to @effect/schema code generator",
"description": "OpenAPI to Effect Schema code generator",
"author": "Fortanix",
"repository": {
"type": "git",
Expand Down Expand Up @@ -54,24 +54,22 @@
"babel": "// Still needed because tsc doesn't like to emit files with .ts extensions"
},
"devDependencies": {
"typescript": "^5.6.2",
"tsx": "^4.19.1",
"@babel/core": "^7.25.2",
"@babel/cli": "^7.25.6",
"@babel/preset-env": "^7.25.4",
"@babel/preset-typescript": "^7.24.7",
"typescript": "^5.6.3",
"tsx": "^4.19.2",
"@babel/core": "^7.26.0",
"@babel/cli": "^7.25.9",
"@babel/preset-env": "^7.26.0",
"@babel/preset-typescript": "^7.26.0",
"babel-plugin-replace-import-extension": "^1.1.4",
"@types/node": "^22.5.4",
"@types/node": "^22.9.0",
"openapi-types": "^12.1.3"
},
"dependencies": {
"ts-dedent": "^2.2.0",
"immutable-json-patch": "^6.0.1",
"prettier": "^3.3.3",
"prettier-plugin-jsdoc": "^1.3.0",
"effect": "^3.7.3",
"fast-check": "^3.22.0",
"@effect/schema": "^0.72.4"
"effect": "^3.10.14"
},
"peerDependencies": {
"openapi-types": "^12.0.0"
Expand Down
1 change: 0 additions & 1 deletion src/analysis/GraphAnalyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ type Ref = string; // OpenAPI schema reference
type Resolve = (ref: Ref) => OpenApiSchema; // Callback to take a ref and resolve it to the corresponding schema

export const schemaIdFromRef = (ref: Ref): OpenApiSchemaId => {
const matches = ref.match(/^#\/components\/schemas\/.+/);
if (!/^#\/components\/schemas\/.+/.test(ref)) {
throw new Error(`Reference format not supported: ${ref}`);
}
Expand Down
3 changes: 1 addition & 2 deletions src/generation/effSchemGen/moduleGen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ export const generateModule = (schemaId: string, schema: OpenApiSchema): string
return 0;
});
return dedent`
import { pipe, Option } from 'effect';
import { Schema as S } from '@effect/schema';
import { pipe, Option, Schema as S } from 'effect';
${refsSorted.map(ref => {
const refId = schemaIdForRef(ref);
Expand Down
5 changes: 2 additions & 3 deletions src/generation/effSchemGen/moduleGenWithSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const processSchemaWithSpec = (spec: GenSpec.GenerationDefinitionSpec, schema: O
return schema;
};

// Generate an @effect/schema module for the given module as per the given module spec
// Generate an Effect Schema module for the given module as per the given module spec
type GenerateModuleWithSpecOptions = {
// Whether `schema1` is before `schema2` in the generation order
isSchemaBefore: (schema1: OpenApiSchemaId, schema2: OpenApiSchemaId) => boolean,
Expand Down Expand Up @@ -341,8 +341,7 @@ export const generateModuleWithSpec = (
// /* Generated on ${new Date().toISOString()} from ${apiName} version ${apiVersion} */
// Currently leaving this out because it means even simple changes cause a git diff upon regeneration.
return dedent`
import { pipe, Option } from 'effect';
import { Schema as S } from '@effect/schema';
import { pipe, Option, Schema as S } from 'effect';
${codeImports}
Expand Down
8 changes: 4 additions & 4 deletions src/generation/effSchemGen/schemaGen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ export const generateForReferenceObject = (ctx: Context, schema: OpenApi.Referen
return { code, refs: [`./${schemaId}.ts`], comments: GenResultUtil.initComments() };
};

// Generate the @effect/schema code for the given OpenAPI schema
// Generate the Effect Schema code for the given OpenAPI schema
export const generateForSchema = (ctx: Context, schema: OpenApiSchema): GenResult => {
const isNonArraySchemaType = (schema: OpenApiSchema): schema is OpenApi.NonArraySchemaObject => {
return !('$ref' in schema) && (!('type' in schema) || !Array.isArray(schema.type));
Expand All @@ -333,7 +333,7 @@ export const generateForSchema = (ctx: Context, schema: OpenApiSchema): GenResul
return generateForSchema(ctx, schemasHead);
}

// `allOf` supports any type, but `@effect/schema` does not currently support generic intersections. Thus,
// `allOf` supports any type, but Effect Schema does not currently support generic intersections. Thus,
// currently we only support `allOf` if it consists only of object schemas.
// Idea: merge `allOf` schema first, e.g. using https://github.com/mokkabonna/json-schema-merge-allof
const areAllObjects: boolean = schema.allOf.reduce(
Expand Down Expand Up @@ -413,7 +413,7 @@ export const generateForSchema = (ctx: Context, schema: OpenApiSchema): GenResul
schemasResults = schemas.map(schema => generateForSchema(ctx, schema));

// Note: `extend` doesn't quite cover the semantics of `allOf`, since it only accepts objects and
// assumes distinct types. However, @effect/schema has no generic built-in mechanism for this.
// assumes distinct types. However, Effect Schema has no generic built-in mechanism for this.
code = dedent`
pipe(
${schemasResults
Expand Down Expand Up @@ -447,7 +447,7 @@ export const generateForSchema = (ctx: Context, schema: OpenApiSchema): GenResul
} else {
const schemasResults: Array<GenResult> = schemas.map(schema => generateForSchema(ctx, schema));
// Note: `union` doesn't quite cover the semantics of `oneOf`, since `oneOf` must guarantee that exactly
// one schema matches. However, @effect/schema has no easy built-in mechanism for this.
// one schema matches. However, Effect Schema has no easy built-in mechanism for this.
const code = dedent`
S.Union(
${schemasResults
Expand Down
2 changes: 1 addition & 1 deletion src/generation/generationSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export type GenerationHooks = {
};


// @effect/schema
// Effect Schema
export type EffectSchemaId = string;

export type FieldNames = string; // Comma-separated string of field names (identifiers)
Expand Down
9 changes: 4 additions & 5 deletions src/openapiToEffect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,7 @@ export const generateSchemasWithSpec = async (request: GenerateRequestWithSpec):
dedent`
/* Generated on ${new Date().toISOString()} from ${document.info.title} version ${document.info.version} */
import { pipe, Option } from 'effect';
import { Schema as S } from '@effect/schema';
import { pipe, Option, Schema as S } from 'effect';
${bundle.replace(/(^|\n)(\s*)import [^;]+;(?! \/\/ <runtime>)/g, '')}
`,
Expand Down Expand Up @@ -360,8 +359,8 @@ const printUsage = () => {

type ScriptArgs = {
values: {
help: boolean | undefined,
spec: string | undefined,
help?: undefined | boolean,
spec?: undefined | string,
},
positionals: Array<string>,
};
Expand Down Expand Up @@ -442,7 +441,7 @@ export const run = async (argsRaw: Array<string>): Promise<void> => {
return;
}

const argsForCommand = { ...args, positionals: args.positionals.slice(1) };
const argsForCommand: ScriptArgs = { ...args, positionals: args.positionals.slice(1) };
switch (command) {
case 'gen':
await runGenerator(argsForCommand);
Expand Down
4 changes: 2 additions & 2 deletions tests/fixtures/fixture1_spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@

import { dedent } from 'ts-dedent';
import { AST } from '@effect/schema';
import { SchemaAST } from 'effect';

import { type GenerationSpec } from '../../src/generation/generationSpec.ts';
import { GenResultUtil, type GenResult } from '../../src/generation/effSchemGen/genUtil.ts';
import { type OpenAPIV3_1 as OpenAPIV3 } from 'openapi-types';


const parseOptions: AST.ParseOptions = {
const parseOptions: SchemaAST.ParseOptions = {
errors: 'all',
onExcessProperty: 'ignore',
};
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/fixture0.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { exec as execCallback } from 'node:child_process';

import { Schema as S } from '@effect/schema';
import { Schema as S } from 'effect';

import assert from 'node:assert/strict';
import { test } from 'node:test';
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/fixture1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { exec as execCallback } from 'node:child_process';

import { Schema as S } from '@effect/schema';
import { Schema as S } from 'effect';

import assert from 'node:assert/strict';
import { test } from 'node:test';
Expand Down
15 changes: 8 additions & 7 deletions tests/integration/generate_fixture.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,31 @@ cat <<"EOT" | FIXTURE_NAME="${FIXTURE_NAME}" node --import=tsx | npx --silent pr
(async () => {
const fixtureName = process.env.FIXTURE_NAME;
const { dedent } = await import('ts-dedent');
const S = await import('@effect/schema');
const S = await import('effect/Schema');
const FastCheck = await import('effect/FastCheck');
const Arbitrary = await import('effect/Arbitrary');
const Fx = await import(`../project_simulation/generated/${fixtureName}/${fixtureName}.ts`);
console.log(dedent`
import { pipe } from 'effect';
import { Schema as S, AST } from '@effect/schema';
import { pipe, Schema as S, SchemaAST, FastCheck, Arbitrary } from 'effect';
import * as Api from './${fixtureName}.ts';
` + '\n\n');
const opts = { errors: 'all', onExcessProperty: 'ignore' };
console.log(dedent`
const opts: AST.ParseOptions = { errors: 'all', onExcessProperty: 'ignore' };
const opts: SchemaAST.ParseOptions = { errors: 'all', onExcessProperty: 'ignore' };
` + '\n\n');
Object.entries(Fx)
.filter(([name]) => !name.endsWith('Encoded'))
.forEach(([name, Schema]) => {
// Note: using `encodedSchema` here will not produce an input that can be decoded successfully. See:
// https://discord.com/channels/795981131316985866/847382157861060618/threads/1237521922011431014
//const sample = S.FastCheck.sample(S.Arbitrary.make(S.Schema.encodedSchema(Schema)), 1)[0];
//const sample = FastCheck.sample(Arbitrary.make(S.encodedSchema(Schema)), 1)[0];
// Instead, we will sample an instance of the decoded type and then encode that
const sample = S.FastCheck.sample(S.Arbitrary.make(Schema), 1)[0];
const sampleEncoded = S.Schema.encodeSync(Schema, opts)(sample);
const sample = FastCheck.sample(Arbitrary.make(Schema), 1)[0];
const sampleEncoded = S.encodeSync(Schema, opts)(sample);
console.log(dedent`
const sample${name}: Api.${name} = pipe(
Expand Down

0 comments on commit 93f35dc

Please sign in to comment.