Skip to content

Commit a6bd3ba

Browse files
committed
feat: api spec type relationship mapping
WIP. Need to fix breadth-first population of `relations()` function
1 parent ae33a7e commit a6bd3ba

File tree

3 files changed

+223
-1
lines changed

3 files changed

+223
-1
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
3+
MIT License
4+
5+
Copyright (c) 2021 Looker Data Sciences, Inc.
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in all
15+
copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
SOFTWARE.
24+
25+
*/
26+
27+
import { TestConfig } from './testUtils'
28+
import { relatedIds, typeFromId, relatedTypes, relations } from './analyzer'
29+
30+
const config = TestConfig()
31+
const apiTestModel = config.apiTestModel
32+
33+
describe('analyzer', () => {
34+
describe('relatedIds', () => {
35+
it('finds related ids', () => {
36+
const dashboard = apiTestModel.types.Dashboard
37+
const actual = relatedIds(dashboard)
38+
expect(actual).toHaveLength(6)
39+
})
40+
})
41+
42+
describe('typeFromId', () => {
43+
it('finds valid types', () => {
44+
let actual = typeFromId(apiTestModel, 'content_favorite_id')
45+
expect(actual?.name).toEqual('ContentFavorite')
46+
actual = typeFromId(apiTestModel, 'dashboard_element_id')
47+
expect(actual?.name).toEqual('DashboardElement')
48+
})
49+
it('returns undefined for invalid types', () => {
50+
let actual = typeFromId(apiTestModel, 'deleter_id')
51+
expect(actual).toBeUndefined()
52+
// Sadly, the corresponding type is `ContentMeta`, breaking the convention
53+
actual = typeFromId(apiTestModel, 'content_metadata_id')
54+
expect(actual?.name).toBeUndefined()
55+
})
56+
})
57+
58+
describe('relatedTypes', () => {
59+
it('finds related types', () => {
60+
const dashboard = apiTestModel.types.Dashboard
61+
const actual = relatedTypes(apiTestModel, dashboard)
62+
// only 9 because `deleter_id` can't be resolved to a type
63+
// and one type is writeable, which shouldn't be included
64+
expect(actual).toHaveLength(9)
65+
})
66+
})
67+
68+
describe('relations', () => {
69+
it('relations handles recursion', () => {
70+
const dashboard = apiTestModel.types.Dashboard
71+
const actual = relations(apiTestModel, dashboard)
72+
expect(actual).toHaveLength(9)
73+
})
74+
})
75+
})

packages/sdk-codegen/src/analyzer.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
3+
MIT License
4+
5+
Copyright (c) 2022 Looker Data Sciences, Inc.
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in all
15+
copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
SOFTWARE.
24+
25+
*/
26+
27+
import type { IApiModel, IType } from './sdkModels'
28+
import { titleCase, WriteType } from './sdkModels'
29+
30+
/** Foreign key naming convention. Could be modified for other naming conventions */
31+
const FKPattern = /^(\w*)_id$/i
32+
33+
/**
34+
* find all related id properties from a type based on FKPattern
35+
*
36+
* @param type to analyze
37+
*/
38+
export const relatedIds = (type: IType) => {
39+
const result = []
40+
for (const key in type.properties) {
41+
if (FKPattern.test(key)) {
42+
result.push(key)
43+
}
44+
}
45+
return result
46+
}
47+
48+
/**
49+
* Return the type only if it is a custom type from the API spec
50+
*
51+
* Types that are simple (intrinsic) or WriteType are not returned
52+
*
53+
* @param api containing type definitions
54+
* @param name type name to find
55+
*/
56+
const typeDef = (api: IApiModel, name: string): IType | undefined => {
57+
if (name in api.types) {
58+
const type = api.types[name]
59+
if (!(type.intrinsic || type instanceof WriteType)) {
60+
return type
61+
}
62+
}
63+
return undefined
64+
}
65+
66+
/**
67+
* Find a type definition from the name of a property based on FKPattern
68+
* @param api containing types
69+
* @param id name to find
70+
*/
71+
export const typeFromId = (api: IApiModel, id: string) => {
72+
const group = id.match(FKPattern)
73+
if (group) {
74+
return typeDef(api, titleCase(group[1]))
75+
}
76+
return undefined
77+
}
78+
79+
/**
80+
* find all types related to the specified type
81+
*
82+
* Returns the union of complex types referenced in the object and types referenced by FK convention
83+
*
84+
* @param api containing types
85+
* @param type to relate to other types
86+
*/
87+
export const relatedTypes = (api: IApiModel, type: IType) => {
88+
const result: IType[] = []
89+
const ids = relatedIds(type)
90+
ids.forEach((id) => {
91+
const entity = typeFromId(api, id)
92+
if (entity) {
93+
result.push(entity)
94+
}
95+
})
96+
Object(Array.from(type.customTypes)).forEach((t: string) => {
97+
const entity = typeDef(api, t)
98+
if (entity) {
99+
result.push(entity)
100+
}
101+
})
102+
return result
103+
}
104+
105+
export interface ITypeMap {
106+
/** Name of type */
107+
[key: string]: ITypeMap
108+
}
109+
110+
/**
111+
* Get all relations for a type, avoiding recursion
112+
*
113+
* TODO get bread-first population working rather than the depth first with a duplication
114+
* bug for the first key in each collection
115+
*
116+
* @param api containing types
117+
* @param type to relate
118+
* @param seen set of type names already encountered to avoid infinit recursion
119+
*/
120+
export const relations = (
121+
api: IApiModel,
122+
type: IType,
123+
seen: Set<string> = new Set<string>()
124+
) => {
125+
const parent = type.jsonName
126+
if (!seen) {
127+
seen = new Set<string>()
128+
}
129+
if (seen.has(parent)) {
130+
return {} // avoid infinite recursion
131+
}
132+
seen.add(parent)
133+
const kin = relatedTypes(api, type)
134+
const family = {}
135+
kin.forEach((k) => {
136+
family[k.jsonName] = {}
137+
})
138+
kin.forEach((k) => {
139+
const name = k.jsonName
140+
if (!seen.has(name)) {
141+
family[name] = relations(api, k, seen)
142+
}
143+
})
144+
const result: ITypeMap = {}
145+
result[parent] = family
146+
return result
147+
}

packages/sdk-codegen/src/sdkModels.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ export const methodRefs = (api: IApiModel, refs: KeyList): IMethod[] => {
283283
}
284284

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

0 commit comments

Comments
 (0)