Skip to content

Commit

Permalink
feat: basic templated type support (#76)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomerAberbach authored Dec 26, 2024
1 parent 20a5c5c commit 6fab1ca
Show file tree
Hide file tree
Showing 10 changed files with 507 additions and 4 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"@alloy-js/core": "^0.3.0",
"@alloy-js/typescript": "^0.3.0",
"@rtsao/scc": "^1.1.0",
"camelcase": "^8.0.0",
"keyalesce": "^2.2.0",
"lfi": "^3.8.0"
},
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions src/arbitrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ export type Arbitrary =
| IntersectionArbitrary
| ReferenceArbitrary
| RecursiveReferenceArbitrary
| FunctionDeclarationArbitrary
| ParameterReferenceArbitrary
| FunctionCallArbitrary

export const neverArbitrary = (): NeverArbitrary => memoize({ type: `never` })

Expand Down Expand Up @@ -190,6 +193,36 @@ export type RecursiveReferenceArbitrary = {
deref: () => ReferenceArbitrary
}

export const functionDeclarationArbitrary = (
options: Omit<FunctionDeclarationArbitrary, `type`>,
): FunctionDeclarationArbitrary =>
memoize({ ...options, type: `function-declaration` })

export type FunctionDeclarationArbitrary = {
type: `function-declaration`
parameters: string[]
arbitrary: Arbitrary
}

export const parameterReferenceArbitrary = (
name: string,
): ParameterReferenceArbitrary => memoize({ type: `parameter-reference`, name })

export type ParameterReferenceArbitrary = {
type: `parameter-reference`
name: string
}

export const functionCallArbitrary = (
options: Omit<FunctionCallArbitrary, `type`>,
): FunctionCallArbitrary => memoize({ ...options, type: `function-call` })

export type FunctionCallArbitrary = {
type: `function-call`
name: string
args: Arbitrary[]
}

const memoize = <A extends Arbitrary>(arbitrary: A): A => {
const arbitraryKey = getArbitraryKey(arbitrary)
let cachedArbitrary = arbitraryCache.get(arbitraryKey)
Expand Down Expand Up @@ -258,6 +291,16 @@ const getArbitraryKey = (arbitrary: Arbitrary): ArbitraryKey => {
])
case `recursive-reference`:
return keyalesce([arbitrary.type, arbitrary.deref])
case `function-declaration`:
return keyalesce([
arbitrary.type,
...arbitrary.parameters,
arbitrary.arbitrary,
])
case `parameter-reference`:
return keyalesce([arbitrary.type, arbitrary.name])
case `function-call`:
return keyalesce([arbitrary.type, arbitrary.name, ...arbitrary.args])
}
}

Expand Down
58 changes: 58 additions & 0 deletions src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import type {
ConstantArbitrary,
DictionaryArbitrary,
EnumArbitrary,
FunctionCallArbitrary,
FunctionDeclarationArbitrary,
IntersectionArbitrary,
NumberArbitrary,
RecordArbitrary,
Expand Down Expand Up @@ -322,6 +324,20 @@ const ArbitraryDefinition = ({
})
case `recursive-reference`:
return RecursiveReferenceArbitrary({ arbitrary: arbitrary.deref() })
case `function-declaration`:
return FunctionDeclarationArbitrary({
arbitrary,
sharedArbitraries,
currentStronglyConnectedArbitraries,
})
case `parameter-reference`:
return arbitrary.name
case `function-call`:
return FunctionCallArbitrary({
arbitrary,
sharedArbitraries,
currentStronglyConnectedArbitraries,
})
}
}

Expand Down Expand Up @@ -638,6 +654,48 @@ const RecursiveReferenceArbitrary = ({
arbitrary: ReferenceArbitrary
}): Child => code`tie(${StringLiteral({ string: arbitrary.name })})`

const FunctionDeclarationArbitrary = ({
arbitrary,
sharedArbitraries,
currentStronglyConnectedArbitraries,
}: {
arbitrary: FunctionDeclarationArbitrary
sharedArbitraries: SharedArbitraries
currentStronglyConnectedArbitraries: Set<ReferenceArbitrary>
}): Child => code`
${
arbitrary.parameters.length === 1
? arbitrary.parameters[0]
: code`(${Commas({ values: arbitrary.parameters, oneLine: true })})`
} =>
${Arbitrary({
arbitrary: arbitrary.arbitrary,
sharedArbitraries,
currentStronglyConnectedArbitraries,
})}
`

const FunctionCallArbitrary = ({
arbitrary,
sharedArbitraries,
currentStronglyConnectedArbitraries,
}: {
arbitrary: FunctionCallArbitrary
sharedArbitraries: SharedArbitraries
currentStronglyConnectedArbitraries: Set<ReferenceArbitrary>
}): Child =>
CallExpression({
name: arbitrary.name,
args: arbitrary.args.map(arg =>
Arbitrary({
arbitrary: arg,
sharedArbitraries,
currentStronglyConnectedArbitraries,
}),
),
oneLine: true,
})

const ArrayExpression = ({ values }: { values: Child[] }): Child =>
code`[${ayJoin(values, { joiner: `, ` })}]`

Expand Down
70 changes: 67 additions & 3 deletions src/convert.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert from 'node:assert'
import { getDoc } from '@typespec/compiler'
import { getDoc, isTemplateDeclaration } from '@typespec/compiler'
import type {
BooleanLiteral,
Enum,
Expand All @@ -13,6 +13,8 @@ import type {
Program,
Scalar,
StringLiteral,
TemplateParameter,
TemplatedType,
Tuple,
Type,
Union,
Expand All @@ -33,6 +35,7 @@ import {
values,
} from 'lfi'
import keyalesce from 'keyalesce'
import camelcase from 'camelcase'
import {
anythingArbitrary,
arrayArbitrary,
Expand All @@ -42,9 +45,12 @@ import {
constantArbitrary,
dictionaryArbitrary,
enumArbitrary,
functionCallArbitrary,
functionDeclarationArbitrary,
intersectionArbitrary,
neverArbitrary,
numberArbitrary,
parameterReferenceArbitrary,
recordArbitrary,
recursiveReferenceArbitrary,
referenceArbitrary,
Expand All @@ -59,6 +65,7 @@ import type {
ArrayArbitrary,
ConstantArbitrary,
DictionaryArbitrary,
ParameterReferenceArbitrary,
RecordArbitrary,
StringArbitrary,
} from './arbitrary.ts'
Expand Down Expand Up @@ -141,6 +148,28 @@ const convertType = (
return arbitrary
}),
)

if (
isTemplatedType(type) &&
type.templateMapper &&
!isTypeSpecNamespace(type.namespace)
) {
return functionCallArbitrary({
name: String(type.name),
// eslint-disable-next-line array-callback-return
args: type.templateMapper.args.map(arg => {
switch (arg.entityKind) {
case `Type`:
return convertType(program, arg, constraints)
case `Value`:
return convertValue(arg)
case `Indeterminate`:
throw new Error(`Unhandled entity: ${arg.entityKind}`)
}
}),
})
}

switch (type.kind) {
case `Intrinsic`:
arbitrary = convertIntrinsic(type)
Expand Down Expand Up @@ -171,12 +200,14 @@ const convertType = (
case `ModelProperty`:
arbitrary = convertType(program, type.type, constraints)
break
case `TemplateParameter`:
arbitrary = convertTemplateParameter(type)
break
case `Operation`:
throw new Error(`Unhandled type: ${type.kind}`)
case `ScalarConstructor`:
case `EnumMember`:
case `UnionVariant`:
case `TemplateParameter`:
case `Namespace`:
case `Decorator`:
case `Function`:
Expand All @@ -185,7 +216,21 @@ const convertType = (
case `Projection`:
case `StringTemplate`:
case `StringTemplateSpan`:
throw new Error(`Unreachable`)
throw new Error(`Unreachable: ${type.kind}`)
}

if (
isTemplatedType(type) &&
isTemplateDeclaration(type) &&
!isTypeSpecNamespace(type.namespace)
) {
assert(arbitrary.type === `reference`)
arbitrary.arbitrary = functionDeclarationArbitrary({
parameters: type.node.templateParameters.map(parameter =>
camelcase(parameter.symbol.name),
),
arbitrary: arbitrary.arbitrary,
})
}

arbitrary = normalizeArbitrary(arbitrary)
Expand All @@ -194,6 +239,20 @@ const convertType = (
return arbitrary
}

const isTemplatedType = (type: Type): type is TemplatedType => {
// eslint-disable-next-line typescript/switch-exhaustiveness-check
switch (type.kind) {
case `Scalar`:
case `Union`:
case `Model`:
case `Interface`:
case `Operation`:
return true
default:
return false
}
}

const typeToArbitrary = new Map<TypeKey, Arbitrary>()
type TypeKey = ReturnType<typeof keyalesce>

Expand Down Expand Up @@ -560,6 +619,11 @@ const toJsValue = (value: Value): unknown => {
}
}

const convertTemplateParameter = (
templateParameter: TemplateParameter,
): ParameterReferenceArbitrary =>
parameterReferenceArbitrary(camelcase(templateParameter.node.symbol.name))

const isTypeSpecNamespace = (namespace?: Namespace): boolean =>
namespace?.name === `TypeSpec`

Expand Down
4 changes: 4 additions & 0 deletions src/dependency-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ const getDirectArbitraryDependencies = (arbitrary: Arbitrary): Arbitrary[] => {
case `url`:
case `bytes`:
case `enum`:
case `function-declaration`:
case `parameter-reference`:
return []
case `array`:
return [arbitrary.value]
Expand All @@ -131,5 +133,7 @@ const getDirectArbitraryDependencies = (arbitrary: Arbitrary): Arbitrary[] => {
return [arbitrary.arbitrary]
case `recursive-reference`:
return [arbitrary.deref()]
case `function-call`:
return arbitrary.args
}
}
Loading

0 comments on commit 6fab1ca

Please sign in to comment.