diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index ca2b3b4..fc98237 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -25,7 +25,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v2 with: - node-version: '18' + node-version: '20' - name: Enable Corepack run: | corepack enable @@ -34,10 +34,13 @@ jobs: run: yarn install --no-immutable env: YARN_ENABLE_HARDENED_MODE: 0 + - name: Verify bicep-types Build Output + run: find bicep-types/src/bicep-types/lib | sed -e "s/[^-][^\/]*\// |/g" -e "s/|\([^ ]\)/|-\1/" - name: Build run: yarn run build:all - + - name: Verify bicep-types Build Output + run: find bicep-types/src/bicep-types/lib | sed -e "s/[^-][^\/]*\// |/g" -e "s/|\([^ ]\)/|-\1/" - name: Lint run: yarn run lint:all publish: @@ -61,7 +64,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v2 with: - node-version: '18' + node-version: '20' registry-url: 'https://npm.pkg.github.com' scope: '@radius-project' - name: Enable Corepack @@ -72,7 +75,9 @@ jobs: run: yarn install --no-immutable env: YARN_ENABLE_HARDENED_MODE: 0 - + - name: Verify bicep-types Build Output + run: find bicep-types/src/bicep-types/lib | sed -e "s/[^-][^\/]*\// |/g" -e "s/|\([^ ]\)/|-\1/" + - name: Publish if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} shell: bash diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7e4d373..4defd89 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -24,7 +24,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v2 with: - node-version: '18' + node-version: '20' - name: Enable Corepack run: | corepack enable @@ -33,6 +33,8 @@ jobs: run: yarn install --no-immutable env: YARN_ENABLE_HARDENED_MODE: 0 - + - name: Verify bicep-types Build Output + run: find bicep-types/src/bicep-types/lib | sed -e "s/[^-][^\/]*\// |/g" -e "s/|\([^ ]\)/|-\1/" + - name: Run tests run: yarn run test:all diff --git a/packages/manifest-to-bicep-extension/jest.config.js b/packages/manifest-to-bicep-extension/jest.config.js index 628fa55..e5fbf46 100644 --- a/packages/manifest-to-bicep-extension/jest.config.js +++ b/packages/manifest-to-bicep-extension/jest.config.js @@ -8,5 +8,6 @@ module.exports = { moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], moduleNameMapper: { '^src/(.*)': '/src/$1', + '^bicep-types$': '../../bicep-types/src/bicep-types/', }, } diff --git a/packages/manifest-to-bicep-extension/package.json b/packages/manifest-to-bicep-extension/package.json index e62c454..98a1f6c 100644 --- a/packages/manifest-to-bicep-extension/package.json +++ b/packages/manifest-to-bicep-extension/package.json @@ -33,7 +33,8 @@ "watch": "tsc -w", "publish": "npm publish", "prepublishOnly": "yarn build", - "preinstall": "cd ../../bicep-types/src/bicep-types && npm ci && npm run build", + "preinstall": "echo 'Building bicep-types...'; cd ../../bicep-types/src/bicep-types && npm ci && npm run build", + "postinstall": "echo 'Finished building bicep-types...';", "version": "npm pkg set version=${0}" } } diff --git a/packages/manifest-to-bicep-extension/src/converter.ts b/packages/manifest-to-bicep-extension/src/converter.ts index 7ab6b7e..2997ef1 100644 --- a/packages/manifest-to-bicep-extension/src/converter.ts +++ b/packages/manifest-to-bicep-extension/src/converter.ts @@ -11,7 +11,7 @@ import { writeIndexMarkdown, writeTypesJson, } from 'bicep-types' -import { ResourceProvider, Schema } from './manifest' +import { APIVersion, ResourceProvider, ResourceType, Schema } from './manifest' export function convert(manifest: ResourceProvider): { typesContent: string @@ -19,63 +19,19 @@ export function convert(manifest: ResourceProvider): { documentationContent: string } { const factory = new TypeFactory() - - for (const resourceType of manifest.types) { - for (const apiVersion of resourceType.apiVersions) { - const qualifiedName = `${manifest.name}/${resourceType.name}@${apiVersion.name}` - - const propertyType = factory.addObjectType( - `${resourceType.name}Properties`, - schemaProperties(apiVersion.schema, factory) - ) - - const bodyType = factory.addObjectType(qualifiedName, { - name: { - type: factory.addStringType(), - flags: - ObjectTypePropertyFlags.Required | - ObjectTypePropertyFlags.Identifier, - description: 'The resource name.', - }, - location: { - type: factory.addStringType(), - flags: ObjectTypePropertyFlags.None, - description: 'The resource location.', - }, - properties: { - type: propertyType, - flags: ObjectTypePropertyFlags.Required, - description: 'The resource properties.', - }, - apiVersion: { - type: factory.addStringLiteralType(apiVersion.name), - flags: - ObjectTypePropertyFlags.ReadOnly | - ObjectTypePropertyFlags.DeployTimeConstant, - description: 'The API version.', - }, - type: { - type: factory.addStringLiteralType( - `${manifest.name}/${resourceType.name}` - ), - flags: - ObjectTypePropertyFlags.ReadOnly | - ObjectTypePropertyFlags.DeployTimeConstant, - description: 'The resource type.', - }, - id: { - type: factory.addStringType(), - flags: ObjectTypePropertyFlags.ReadOnly, - description: 'The resource id.', - }, - }) - factory.addResourceType( - qualifiedName, - ScopeType.Unknown, - undefined, - bodyType, - ResourceFlags.None, - {} + for (const [resourceTypeName, resourceType] of Object.entries( + manifest.types + )) { + for (const [apiVersionName, apiVersion] of Object.entries( + resourceType.apiVersions + )) { + addResourceTypeForApiVersion( + manifest, + resourceTypeName, + resourceType, + apiVersionName, + apiVersion, + factory ) } } @@ -99,24 +55,102 @@ export function convert(manifest: ResourceProvider): { } } -function schemaProperties( +export function addResourceTypeForApiVersion( + manifest: ResourceProvider, + resourceTypeName: string, + _: ResourceType, + apiVersionName: string, + apiVersion: APIVersion, + factory: TypeFactory +): TypeReference { + const qualifiedName = `${manifest.name}/${resourceTypeName}@${apiVersionName}` + + const propertyType = factory.addObjectType( + `${resourceTypeName}Properties`, + addObjectProperties(apiVersion.schema, factory) + ) + + const bodyType = factory.addObjectType(qualifiedName, { + name: { + type: factory.addStringType(), + flags: + ObjectTypePropertyFlags.Required | ObjectTypePropertyFlags.Identifier, + description: 'The resource name.', + }, + location: { + type: factory.addStringType(), + flags: ObjectTypePropertyFlags.None, + description: 'The resource location.', + }, + properties: { + type: propertyType, + flags: ObjectTypePropertyFlags.Required, + description: 'The resource properties.', + }, + apiVersion: { + type: factory.addStringLiteralType(apiVersionName), + flags: + ObjectTypePropertyFlags.ReadOnly | + ObjectTypePropertyFlags.DeployTimeConstant, + description: 'The API version.', + }, + type: { + type: factory.addStringLiteralType( + `${manifest.name}/${resourceTypeName}` + ), + flags: + ObjectTypePropertyFlags.ReadOnly | + ObjectTypePropertyFlags.DeployTimeConstant, + description: 'The resource type.', + }, + id: { + type: factory.addStringType(), + flags: ObjectTypePropertyFlags.ReadOnly, + description: 'The resource id.', + }, + }) + return factory.addResourceType( + qualifiedName, + ScopeType.Unknown, + undefined, + bodyType, + ResourceFlags.None, + {} + ) +} + +export function addSchemaType( + schema: Schema, + name: string, + factory: TypeFactory +): TypeReference { + if (schema.type === 'string') { + return factory.addStringType() + } else if (schema.type === 'object') { + return factory.addObjectType(name, addObjectProperties(schema, factory)) + } else { + throw new Error(`Unsupported schema type: ${schema.type}`) + } +} + +export function addObjectProperties( parent: Schema, factory: TypeFactory ): Record { const results: Record = {} for (const [key, value] of Object.entries(parent.properties ?? {})) { - results[key] = addSchemaProperty(parent, key, value, factory) + results[key] = addObjectProperty(parent, key, value, factory) } return results } -function addSchemaProperty( +export function addObjectProperty( parent: Schema, key: string, property: Schema, factory: TypeFactory ): ObjectTypeProperty { - const propertyType = addSchema(property, key, factory) + const propertyType = addSchemaType(property, key, factory) let flags = ObjectTypePropertyFlags.None if (parent.required?.includes(key)) { @@ -132,17 +166,3 @@ function addSchemaProperty( flags: flags, } } - -function addSchema( - schema: Schema, - name: string, - factory: TypeFactory -): TypeReference { - if (schema.type === 'string') { - return factory.addStringType() - } else if (schema.type === 'object') { - return factory.addObjectType(name, schemaProperties(schema, factory)) - } else { - throw new Error(`Unsupported schema type: ${schema.type}`) - } -} diff --git a/packages/manifest-to-bicep-extension/src/manifest.ts b/packages/manifest-to-bicep-extension/src/manifest.ts index bcb0398..577004c 100644 --- a/packages/manifest-to-bicep-extension/src/manifest.ts +++ b/packages/manifest-to-bicep-extension/src/manifest.ts @@ -1,16 +1,16 @@ +import { parse } from 'yaml' + export interface ResourceProvider { name: string - types: ResourceType[] + types: Record } export interface ResourceType { - name: string defaultApiVersion?: string - apiVersions: APIVersion[] + apiVersions: Record } export interface APIVersion { - name: string schema: Schema capabilities?: string[] } @@ -18,7 +18,12 @@ export interface APIVersion { export interface Schema { type: 'string' | 'object' description?: string - properties?: Schema[] + properties?: Record required?: string[] readOnly?: boolean } + +export function parseManifest(input: string): ResourceProvider { + const parsed = parse(input) as ResourceProvider + return parsed +} diff --git a/packages/manifest-to-bicep-extension/src/math.ts b/packages/manifest-to-bicep-extension/src/math.ts deleted file mode 100644 index a39d4ef..0000000 --- a/packages/manifest-to-bicep-extension/src/math.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function add(a: number, b: number): number { - return a + b -} diff --git a/packages/manifest-to-bicep-extension/src/program.ts b/packages/manifest-to-bicep-extension/src/program.ts index cf183be..f1b9212 100644 --- a/packages/manifest-to-bicep-extension/src/program.ts +++ b/packages/manifest-to-bicep-extension/src/program.ts @@ -1,12 +1,12 @@ -import { parse } from 'yaml' import yargs from 'yargs' import { hideBin } from 'yargs/helpers' import fs from 'node:fs' import { convert } from './converter' +import { parseManifest } from './manifest' async function generate(manifest: string, output: string) { const data = fs.readFileSync(manifest, 'utf8') - const parsed = parse(data) + const parsed = parseManifest(data) const converted = convert(parsed) fs.rmSync(`${output}/types.json`, { force: true }) diff --git a/packages/manifest-to-bicep-extension/test/converter.test.ts b/packages/manifest-to-bicep-extension/test/converter.test.ts new file mode 100644 index 0000000..6fdd008 --- /dev/null +++ b/packages/manifest-to-bicep-extension/test/converter.test.ts @@ -0,0 +1,247 @@ +import { beforeEach, describe, expect, it } from '@jest/globals' +import { + addObjectProperties, + addObjectProperty, + addResourceTypeForApiVersion, + addSchemaType, +} from 'src/converter' +import { ResourceProvider, Schema } from 'src/manifest' +import { + ObjectType, + ObjectTypePropertyFlags, + ResourceFlags, + ResourceType, + ScopeType, + TypeBaseKind, + TypeFactory, +} from 'bicep-types' + +describe('addResourceTypeForApiVersion', () => { + let factory: TypeFactory + + beforeEach(() => { + factory = new TypeFactory() + }) + + it('should add a resource type', () => { + const manifest: ResourceProvider = { + name: 'Applications.Test', + types: { + testResources: { + apiVersions: { + '2021-01-01': { + schema: { + type: 'object', + properties: { + a: { + type: 'string', + }, + b: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + } + + const resourceType = manifest.types['testResources'] + const apiVersion = resourceType.apiVersions['2021-01-01'] + + const result = addResourceTypeForApiVersion( + manifest, + 'testResources', + resourceType, + '2021-01-01', + apiVersion, + factory + ) + expect(result).toBeDefined() + + const addedResourceType = factory.types[result.index] as ResourceType + expect(addedResourceType).toBeDefined() + + expect(addedResourceType.name).toBe( + 'Applications.Test/testResources@2021-01-01' + ) + expect(addedResourceType.type).toBe(TypeBaseKind.ResourceType) + expect(addedResourceType.flags).toBe(ResourceFlags.None) + expect(Object.keys(addedResourceType.functions ?? {})).toHaveLength(0) + expect(addedResourceType.scopeType).toBe(ScopeType.Unknown) + expect(addedResourceType.readOnlyScopes).toBeUndefined() + + expect(addedResourceType.body).toBeDefined() + const addedBodyType = factory.types[ + addedResourceType.body.index + ] as ObjectType + expect(addedBodyType).toBeDefined() + + // The body type is predefined (other than .properties) + const expectedProperties = [ + 'name', + 'location', + 'properties', + 'apiVersion', + 'type', + 'id', + ] + expect(Object.keys(addedBodyType.properties).sort()).toEqual( + expectedProperties.sort() + ) + + const addedPropertiesProperty = addedBodyType.properties['properties'] + expect(addedPropertiesProperty).toBeDefined() + + const addedPropertiesType = factory.types[ + addedPropertiesProperty.type.index + ] as ObjectType + expect(addedPropertiesType.properties).toBeDefined() + + expect(addedPropertiesType.properties).toHaveProperty('a') + expect(addedPropertiesType.properties).toHaveProperty('b') + }) +}) + +describe('addSchemaType', () => { + let factory: TypeFactory + + beforeEach(() => { + factory = new TypeFactory() + }) + + it('should add a string type', () => { + const schema: Schema = { + type: 'string', + } + + const result = addSchemaType(schema, 'test', factory) + const added = factory.types[result.index] + expect(added).toBeDefined() + expect(added.type).toBe(TypeBaseKind.StringType) + }) + + it('should add an object type', () => { + const schema: Schema = { + type: 'object', + properties: { + a: { + type: 'string', + }, + b: { + type: 'string', + }, + }, + } + + const result = addSchemaType(schema, 'test', factory) + + const added = factory.types[result.index] as ObjectType + expect(added.type).toBe(TypeBaseKind.ObjectType) + expect(added.properties).toBeDefined() + expect(Object.entries(added.properties)).toHaveLength(2) + expect(added.properties).toHaveProperty('a') + expect(added.properties).toHaveProperty('b') + }) +}) + +describe('addObjectProperties', () => { + let factory: TypeFactory + + beforeEach(() => { + factory = new TypeFactory() + }) + + it('should add each property', () => { + const schema: Schema = { + type: 'object', + properties: { + a: { + type: 'string', + }, + b: { + type: 'string', + }, + }, + } + + const result = addObjectProperties(schema, factory) + expect(Object.entries(result)).toHaveLength(2) + expect(result).toHaveProperty('a') + expect(result).toHaveProperty('b') + }) +}) + +describe('addObjectProperty', () => { + let factory: TypeFactory + + beforeEach(() => { + factory = new TypeFactory() + }) + + it('should add a property', () => { + const parent: Schema = { + type: 'object', + properties: { + // Will be unused + }, + } + + const property: Schema = { + type: 'string', + description: 'cool description', + } + + const result = addObjectProperty(parent, 'a', property, factory) + expect(result.description).toEqual('cool description') + expect(result.flags).toEqual(ObjectTypePropertyFlags.None) + + const added = factory.types[result.type.index] + expect(added).toBeDefined() + }) + + it('should add a readonly property', () => { + const parent: Schema = { + type: 'object', + properties: { + // Will be unused + }, + } + + const property: Schema = { + type: 'string', + description: 'cool description', + readOnly: true, + } + + const result = addObjectProperty(parent, 'a', property, factory) + expect(result.description).toEqual('cool description') + expect(result.flags).toEqual(ObjectTypePropertyFlags.ReadOnly) + + const added = factory.types[result.type.index] + expect(added).toBeDefined() + }) + + it('should add a required property', () => { + const parent: Schema = { + type: 'object', + properties: { + // Will be unused + }, + required: ['a'], + } + + const property: Schema = { + type: 'string', + description: 'cool description', + } + + const result = addObjectProperty(parent, 'a', property, factory) + expect(result.description).toEqual('cool description') + expect(result.flags).toEqual(ObjectTypePropertyFlags.Required) + + const added = factory.types[result.type.index] + expect(added).toBeDefined() + }) +}) diff --git a/packages/manifest-to-bicep-extension/test/manifest.test.ts b/packages/manifest-to-bicep-extension/test/manifest.test.ts new file mode 100644 index 0000000..4b1aa6a --- /dev/null +++ b/packages/manifest-to-bicep-extension/test/manifest.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from '@jest/globals' +import { parseManifest, ResourceProvider } from '../src/manifest' +import * as fs from 'node:fs' + +describe('parseManifest', () => { + it('should parse a manifest file with required fields', () => { + const input = fs.readFileSync(__dirname + '/testdata/valid.yaml', 'utf8') + const result: ResourceProvider = parseManifest(input) + expect(result.name).toBe('MyCompany.Resources') + expect(result.types).toStrictEqual({ + testResources: { + apiVersions: { + '2025-01-01-preview': { + capabilities: ['Recipes'], + schema: {}, + }, + }, + }, + }) + }) +}) diff --git a/packages/manifest-to-bicep-extension/test/math.test.ts b/packages/manifest-to-bicep-extension/test/math.test.ts deleted file mode 100644 index 3ea3744..0000000 --- a/packages/manifest-to-bicep-extension/test/math.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { add } from 'src/math' -import { describe, it, expect } from '@jest/globals' - -describe('add', () => { - it('should add two numbers', () => { - expect(add(1, 2)).toBe(3) - }) -}) diff --git a/packages/manifest-to-bicep-extension/test/testdata/valid.yaml b/packages/manifest-to-bicep-extension/test/testdata/valid.yaml new file mode 100644 index 0000000..add38a4 --- /dev/null +++ b/packages/manifest-to-bicep-extension/test/testdata/valid.yaml @@ -0,0 +1,7 @@ +name: MyCompany.Resources +types: + testResources: + apiVersions: + '2025-01-01-preview': + schema: {} + capabilities: ['Recipes']