Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
# For more details, visit the project page:
# https://github.com/github/gitignore

# Codegen
CODEGEN.md

# rspec failure tracking
.rspec_status

Expand Down
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Prettier exceptions

*.hbs
CODEGEN.md
1 change: 1 addition & 0 deletions codegen/content/CODEGEN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Codegen
1 change: 1 addition & 0 deletions codegen/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default null
26 changes: 26 additions & 0 deletions codegen/layouts/client.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true
{{#if importActionAttempt}}

require "seam/helpers/action_attempt"
{{/if}}

module Seam
module Clients
class {{className}}
def initialize(client:, defaults:)
@client = client
@defaults = defaults
end
{{#each childClients}}

def {{namespace}}
@{{namespace}} ||= Seam::Clients::{{clientName}}.new(client: @client, defaults: @defaults)
end
{{/each}}
{{#each methods}}

{{> client-method}}
{{/each}}
end
end
end
1 change: 1 addition & 0 deletions codegen/layouts/default.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{contents}}
8 changes: 8 additions & 0 deletions codegen/layouts/imports.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

{{#each customImports}}
{{this}}
{{/each}}
{{#each requires}}
require_relative "{{this}}"
{{/each}}
17 changes: 17 additions & 0 deletions codegen/layouts/partials/client-method.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
def {{name}}{{#if hasSignature}}({{signatureParams}}){{/if}}
{{#if usesRes}}res = {{/if}}@client.post("{{path}}"{{#if hasParams}}, {{bodyParams}}.compact{{/if}})
{{#if isResource}}

Seam::Resources::{{returnResource}}.load_from_response(res.body["{{returnPath}}"])
{{/if}}
{{#if isPoll}}

wait_for_action_attempt = wait_for_action_attempt.nil? ? @defaults.wait_for_action_attempt : wait_for_action_attempt

Helpers::ActionAttempt.decide_and_wait(Seam::Resources::ActionAttempt.load_from_response(res.body["{{returnPath}}"]), @client, wait_for_action_attempt)
{{/if}}
{{#if isNil}}

nil
{{/if}}
end
22 changes: 22 additions & 0 deletions codegen/layouts/resource.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Seam
module Resources
class {{className}} < BaseResource
attr_accessor {{attrAccessors}}
{{#if hasDateAccessors}}

date_accessor {{dateAccessors}}
{{/if}}
{{#if hasSupportModules}}

{{#if includeErrorsSupport}}
include Seam::Resources::ResourceErrorsSupport
{{/if}}
{{#if includeWarningsSupport}}
include Seam::Resources::ResourceWarningsSupport
{{/if}}
{{/if}}
end
end
end
32 changes: 32 additions & 0 deletions codegen/layouts/routes.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

module Seam
module Routes
{{#each routeClients}}
def {{name}}
@{{name}} ||= Seam::Clients::{{className}}.new(client: @client, defaults: @defaults)
end

{{/each}}
# @deprecated Please use {#devices.unmanaged} instead.
def unmanaged_devices
warn "[DEPRECATION] 'unmanaged_devices' is deprecated. Please use 'devices.unmanaged' instead."

@unmanaged_devices ||= Seam::Clients::DevicesUnmanaged.new(client: @client, defaults: @defaults)
end

# @deprecated Please use {#access_codes.unmanaged} instead.
def unmanaged_access_codes
warn "[DEPRECATION] 'unmanaged_access_codes' is deprecated. Please use 'access_codes.unmanaged' instead."

@unmanaged_access_codes ||= Seam::Clients::AccessCodesUnmanaged.new(client: @client, defaults: @defaults)
end

private

def initialize_routes(client:, defaults:)
@client = client
@defaults = defaults
end
end
end
5 changes: 5 additions & 0 deletions codegen/lib/custom-resource-name-conversions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Ported from @seamapi/nextlove-sdk-generator lib/custom-resource-name-conversions.ts.

export function convertCustomResourceName(responseType: string): string {
return responseType === 'event' ? 'seam_event' : responseType
}
23 changes: 23 additions & 0 deletions codegen/lib/endpoint-rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// TEMPORARY: Verbatim port of @seamapi/nextlove-sdk-generator
// lib/endpoint-rules.ts. These lists only preserve legacy generated output:
// the ignored paths reproduce the previous endpoint filtering, and the
// deprecated action attempt list keeps those endpoints returning None.
// TODO: Delete this file once generated output is allowed to change; filter
// on blueprint undocumented flags instead and let the deprecated endpoints
// return their real response types.

export const endpointsReturningDeprecatedActionAttempt = [
'/access_codes/delete',
'/access_codes/unmanaged/delete',
'/access_codes/update',
'/noise_sensors/noise_thresholds/delete',
'/noise_sensors/noise_thresholds/update',
'/thermostats/climate_setting_schedules/update',
]

export const ignoredEndpointPaths = [
'/health',
'/health/get_health',
'/health/get_service_health',
'/health/service/[service_name]',
]
1 change: 1 addition & 0 deletions codegen/lib/handlebars-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const identity = (x: unknown): unknown => x
7 changes: 7 additions & 0 deletions codegen/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { handlebarsHelpers } from '@seamapi/smith'

import * as customHelpers from './handlebars-helpers.js'

export const helpers = { ...handlebarsHelpers, ...customHelpers }

export * from './routes.js'
93 changes: 93 additions & 0 deletions codegen/lib/layouts/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Builds the template context for client files
// (lib/seam/routes/clients/{snake_name}.rb).
// Mirrors the output of the nextlove RubyClient#serialize.

import { endpointsReturningDeprecatedActionAttempt } from '../endpoint-rules.js'
import {
type ClientMethod,
type ClientModel,
sortClientMethodParameters,
} from '../ruby-client.js'

export interface ClientMethodLayoutContext {
name: string
hasSignature: boolean
signatureParams: string
hasParams: boolean
bodyParams: string
path: string
usesRes: boolean
isResource: boolean
isPoll: boolean
isNil: boolean
returnResource: string
returnPath: string
}

export interface ClientLayoutContext {
className: string
importActionAttempt: boolean
childClients: Array<{ namespace: string; clientName: string }>
methods: ClientMethodLayoutContext[]
}

const getMethodLayoutContext = (
method: ClientMethod,
): ClientMethodLayoutContext => {
const { methodName, path, parameters, returnResource, returnPath } = method

const hasReturnValue = returnResource != null && returnPath !== ''
const returnsDeprecatedActionAttempt =
endpointsReturningDeprecatedActionAttempt.includes(path)
const canPollActionAttempt =
returnPath === 'action_attempt' && !returnsDeprecatedActionAttempt
const hasParams = parameters.length > 0

const sortedParameters = sortClientMethodParameters(parameters)

const signatureParams = sortedParameters
.map((p) => `${p.name}${p.required ?? false ? ':' : ': nil'}`)
.concat(canPollActionAttempt ? ['wait_for_action_attempt: nil'] : [])
.join(', ')

const bodyParams = `{${sortedParameters
.map((p) => `${p.name}: ${p.name}`)
.join(', ')}}`

// These three branches mirror the nextlove serializer and are independent by
// design, not mutually exclusive: the resource line renders when there is a
// return value that is not polled, and the nil line renders when there is no
// return value or the endpoint returns a deprecated action attempt.
const isResource = hasReturnValue && !canPollActionAttempt
const isPoll = canPollActionAttempt
const isNil = !hasReturnValue || returnsDeprecatedActionAttempt

return {
name: methodName,
hasSignature: signatureParams.length > 0,
signatureParams,
hasParams,
bodyParams,
path,
usesRes: isResource || isPoll,
isResource,
isPoll,
isNil,
returnResource: returnResource ?? '',
returnPath,
}
}

export const setClientLayoutContext = (
cls: ClientModel,
): ClientLayoutContext => ({
className: cls.name,
importActionAttempt: cls.methods.some(
({ returnResource }) => returnResource === 'ActionAttempt',
),
childClients: cls.childClientIdentifiers.map((i) => ({
namespace: i.namespace,
clientName: i.clientName,
})),
methods: cls.methods.map(getMethodLayoutContext),
})
16 changes: 16 additions & 0 deletions codegen/lib/layouts/imports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Builds the template context for the index files
// (lib/seam/routes/{clients,resources}/index.rb).
// Mirrors the output of the nextlove get-entity-imports-template.ts.

export interface ImportsLayoutContext {
customImports: string[]
requires: string[]
}

export const setImportsLayoutContext = (
entityNames: string[],
customImports: string[],
): ImportsLayoutContext => ({
customImports,
requires: entityNames,
})
55 changes: 55 additions & 0 deletions codegen/lib/layouts/resource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Builds the template context for resource files
// (lib/seam/routes/resources/{snake_name}.rb).
// Mirrors the output of the nextlove resource.rb.template.ts.

import { pascalCase } from 'change-case'

import { convertCustomResourceName } from '../custom-resource-name-conversions.js'
import { flattenObjSchema } from '../openapi/flatten-obj-schema.js'
import type { ObjSchema } from '../openapi/types.js'

export interface ResourceLayoutContext {
className: string
attrAccessors: string
hasDateAccessors: boolean
dateAccessors: string
hasSupportModules: boolean
includeErrorsSupport: boolean
includeWarningsSupport: boolean
}

export const setResourceLayoutContext = (
snakeName: string,
schema: ObjSchema,
): ResourceLayoutContext => {
// TODO: Use resource properties from @seamapi/blueprint once generated output
// is allowed to change. Blueprint omits some schemas, reorders others, and
// collapses integer to number, so the raw OpenAPI schema is used here to keep
// the output identical.
const properties = Object.entries(flattenObjSchema(schema).properties).map(
([name, propertySchema]) => ({
name,
isDateTime:
'format' in propertySchema && propertySchema.format === 'date-time',
}),
)

const attrs = properties.filter((p) => !p.isDateTime).map((p) => p.name)
const dateAttrs = properties.filter((p) => p.isDateTime).map((p) => p.name)
const noErrorWarningAttrs = attrs.filter(
(attr) => attr !== 'errors' && attr !== 'warnings',
)

const includeErrorsSupport = attrs.includes('errors')
const includeWarningsSupport = attrs.includes('warnings')

return {
className: pascalCase(convertCustomResourceName(snakeName)),
attrAccessors: noErrorWarningAttrs.map((attr) => `:${attr}`).join(', '),
hasDateAccessors: dateAttrs.length > 0,
dateAccessors: dateAttrs.map((attr) => `:${attr}`).join(', '),
hasSupportModules: includeErrorsSupport || includeWarningsSupport,
includeErrorsSupport,
includeWarningsSupport,
}
}
17 changes: 17 additions & 0 deletions codegen/lib/layouts/routes-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Builds the template context for lib/seam/routes/routes.rb.
// Mirrors the output of the nextlove routes.rb.template.ts.

import { pascalCase } from 'change-case'

export interface RoutesFileLayoutContext {
routeClients: Array<{ name: string; className: string }>
}

export const setRoutesFileLayoutContext = (
resourceClientNames: string[],
): RoutesFileLayoutContext => ({
routeClients: resourceClientNames.map((name) => ({
name,
className: pascalCase(name),
})),
})
Loading
Loading