Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: api spec type relationship mapping #1092

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
75 changes: 75 additions & 0 deletions packages/sdk-codegen/src/analyzer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*

MIT License

Copyright (c) 2021 Looker Data Sciences, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

*/

import { TestConfig } from './testUtils'
import { relatedIds, typeFromId, relatedTypes, relations } from './analyzer'

const config = TestConfig()
const apiTestModel = config.apiTestModel

describe('analyzer', () => {
describe('relatedIds', () => {
it('finds related ids', () => {
const dashboard = apiTestModel.types.Dashboard
const actual = relatedIds(dashboard)
expect(actual).toHaveLength(6)
})
})

describe('typeFromId', () => {
it('finds valid types', () => {
let actual = typeFromId(apiTestModel, 'content_favorite_id')
expect(actual?.name).toEqual('ContentFavorite')
actual = typeFromId(apiTestModel, 'dashboard_element_id')
expect(actual?.name).toEqual('DashboardElement')
})
it('returns undefined for invalid types', () => {
let actual = typeFromId(apiTestModel, 'deleter_id')
expect(actual).toBeUndefined()
// Sadly, the corresponding type is `ContentMeta`, breaking the convention
actual = typeFromId(apiTestModel, 'content_metadata_id')
expect(actual?.name).toBeUndefined()
})
})

describe('relatedTypes', () => {
it('finds related types', () => {
const dashboard = apiTestModel.types.Dashboard
const actual = relatedTypes(apiTestModel, dashboard)
// only 9 because `deleter_id` can't be resolved to a type
// and one type is writeable, which shouldn't be included
expect(actual).toHaveLength(9)
})
})

describe('relations', () => {
it('relations handles recursion', () => {
const dashboard = apiTestModel.types.Dashboard
const actual = relations(apiTestModel, dashboard)
expect(actual).toHaveLength(9)
})
})
})
147 changes: 147 additions & 0 deletions packages/sdk-codegen/src/analyzer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*

MIT License

Copyright (c) 2022 Looker Data Sciences, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

*/

import type { IApiModel, IType } from './sdkModels'
import { titleCase, WriteType } from './sdkModels'

/** Foreign key naming convention. Could be modified for other naming conventions */
const FKPattern = /^(\w*)_id$/i

/**
* find all related id properties from a type based on FKPattern
*
* @param type to analyze
*/
export const relatedIds = (type: IType) => {
const result = []
for (const key in type.properties) {
if (FKPattern.test(key)) {
result.push(key)
}
}
return result
}

/**
* Return the type only if it is a custom type from the API spec
*
* Types that are simple (intrinsic) or WriteType are not returned
*
* @param api containing type definitions
* @param name type name to find
*/
const typeDef = (api: IApiModel, name: string): IType | undefined => {
if (name in api.types) {
const type = api.types[name]
if (!(type.intrinsic || type instanceof WriteType)) {
return type
}
}
return undefined
}

/**
* Find a type definition from the name of a property based on FKPattern
* @param api containing types
* @param id name to find
*/
export const typeFromId = (api: IApiModel, id: string) => {
const group = id.match(FKPattern)
if (group) {
return typeDef(api, titleCase(group[1]))
}
return undefined
}

/**
* find all types related to the specified type
*
* Returns the union of complex types referenced in the object and types referenced by FK convention
*
* @param api containing types
* @param type to relate to other types
*/
export const relatedTypes = (api: IApiModel, type: IType) => {
const result: IType[] = []
const ids = relatedIds(type)
ids.forEach((id) => {
const entity = typeFromId(api, id)
if (entity) {
result.push(entity)
}
})
Object(Array.from(type.customTypes)).forEach((t: string) => {
const entity = typeDef(api, t)
if (entity) {
result.push(entity)
}
})
return result
}

export interface ITypeMap {
/** Name of type */
[key: string]: ITypeMap
}

/**
* Get all relations for a type, avoiding recursion
*
* TODO get bread-first population working rather than the depth first with a duplication
* bug for the first key in each collection
*
* @param api containing types
* @param type to relate
* @param seen set of type names already encountered to avoid infinit recursion
*/
export const relations = (
api: IApiModel,
type: IType,
seen: Set<string> = new Set<string>()
) => {
const parent = type.jsonName
if (!seen) {
seen = new Set<string>()
}
if (seen.has(parent)) {
return {} // avoid infinite recursion
}
seen.add(parent)
const kin = relatedTypes(api, type)
const family = {}
kin.forEach((k) => {
family[k.jsonName] = {}
})
kin.forEach((k) => {
const name = k.jsonName
if (!seen.has(name)) {
family[name] = relations(api, k, seen)
}
})
const result: ITypeMap = {}
result[parent] = family
return result
}
2 changes: 1 addition & 1 deletion packages/sdk-codegen/src/sdkModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ export const methodRefs = (api: IApiModel, refs: KeyList): IMethod[] => {
}

/**
* Resolve a list of method keys into an IType[] in alphabetical order by name
* Resolve a list of type keys into an IType[] in alphabetical order by name
* @param api model to use
* @param refs references to models
* @returns Populated method list. Anything not matched is skipped
Expand Down