From 9af2e3755fb4f7f987cc6980ed57e15f504295ba Mon Sep 17 00:00:00 2001 From: Joseph Axisa Date: Wed, 14 Apr 2021 21:10:33 +0100 Subject: [PATCH] fix: recursive search of direct type references (#591) * fix: recursive search issue Co-authored-by: John Kaster --- packages/sdk-codegen/src/sdkModels.spec.ts | 17 +-- packages/sdk-codegen/src/sdkModels.ts | 37 ++++- test/openApiRef.json | 168 ++++++++++++--------- 3 files changed, 132 insertions(+), 90 deletions(-) diff --git a/packages/sdk-codegen/src/sdkModels.spec.ts b/packages/sdk-codegen/src/sdkModels.spec.ts index 3c752e4b5..105306039 100644 --- a/packages/sdk-codegen/src/sdkModels.spec.ts +++ b/packages/sdk-codegen/src/sdkModels.spec.ts @@ -291,15 +291,14 @@ describe('sdkModels', () => { } }) - // TODO create a mock spec that has a recursive type, since this no longer does - // it ('detects recursive types', () => { - // const type = apiTestModel.types['LookmlModelExploreField'] - // const actual = type.isRecursive() - // expect(actual).toEqual(true) - // type = apiTestModel.types['CredentialsApi3'] - // actual = type.isRecursive() - // expect(actual).toEqual(false) - // }) + it('detects recursive types', () => { + let type = apiTestModel.types.LookmlModelExploreField + let actual = type.isRecursive() + expect(actual).toEqual(true) + type = apiTestModel.types.CredentialsApi3 + actual = type.isRecursive() + expect(actual).toEqual(false) + }) }) describe('response modes', () => { diff --git a/packages/sdk-codegen/src/sdkModels.ts b/packages/sdk-codegen/src/sdkModels.ts index 0efab7c31..b8f7a4cc2 100644 --- a/packages/sdk-codegen/src/sdkModels.ts +++ b/packages/sdk-codegen/src/sdkModels.ts @@ -1608,10 +1608,13 @@ export class Type implements IType { return false let result = rx.test(this.searchString(criteria)) if (!result && Type.isPropSearch(criteria)) { - for (const [, p] of Object.entries(this.properties)) { - if (p.search(rx, criteria)) { - result = true - break + for (const [, prop] of Object.entries(this.properties)) { + if (this.name !== prop.type.name) { + /** Avoiding recursion */ + if (prop.search(rx, criteria)) { + result = true + break + } } } } @@ -1630,7 +1633,10 @@ export class Type implements IType { } if (criteria.has(SearchCriterion.property)) { Object.entries(this.properties).forEach(([, prop]) => { - result += prop.searchString(criteria) + if (this.name !== prop.type.name) { + /** Avoiding recursion */ + result += prop.searchString(criteria) + } }) } return result @@ -1915,7 +1921,7 @@ export class ApiModel implements ISymbolTable, IApiModel { return new ApiModel(spec) } - private static isModelSearch(criteria: SearchCriteria): boolean { + private static isMethodSearch(criteria: SearchCriteria): boolean { return ( criteria.has(SearchCriterion.method) || criteria.has(SearchCriterion.argument) || @@ -1979,7 +1985,7 @@ export class ApiModel implements ISymbolTable, IApiModel { return result } - if (ApiModel.isModelSearch(criteria)) { + if (ApiModel.isMethodSearch(criteria)) { Object.entries(this.methods).forEach(([, method]) => { if (method.search(rx, criteria)) { methodCount++ @@ -2029,6 +2035,21 @@ export class ApiModel implements ISymbolTable, IApiModel { typeName?: string, methodName?: string ): IType { + const getRef = (schema: OAS.SchemaObject | OAS.ReferenceObject) => { + const ref = schema.$ref + let result = this.refs[ref] + + if (!result) { + /** This must be recursive */ + const parts: string[] = ref.split('/') + const name = parts[parts.length - 1] + const t = new Type(schema, name) + this.refs[ref] = t + result = t + } + return result + } + if (typeof schema === 'string') { if (schema.indexOf('/requestBodies/') < 0) return this.types[schema.substr(schema.lastIndexOf('/') + 1)] @@ -2042,7 +2063,7 @@ export class ApiModel implements ISymbolTable, IApiModel { if (ref) return this.resolveType(ref, style, typeName, methodName) } } else if (OAS.isReferenceObject(schema)) { - return this.refs[schema.$ref] + return getRef(schema) } else if (schema.type) { if (schema.type === 'integer' && schema.format === 'int64') { return this.types.int64 diff --git a/test/openApiRef.json b/test/openApiRef.json index 4c8e1ee0f..f66cfbb80 100644 --- a/test/openApiRef.json +++ b/test/openApiRef.json @@ -30042,287 +30042,309 @@ "align": { "type": "string", "readOnly": true, - "enum": ["left", "right"], + "x-looker-values": ["left", "right"], "description": "The appropriate horizontal text alignment the values of this field should be displayed in. Valid values are: \"left\", \"right\".", - "nullable": false + "x-looker-nullable": false }, "can_filter": { "type": "boolean", "readOnly": true, "description": "Whether it's possible to filter on this field.", - "nullable": false + "x-looker-nullable": false }, "category": { "type": "string", "readOnly": true, - "enum": ["parameter", "filter", "measure", "dimension"], + "x-looker-values": ["parameter", "filter", "measure", "dimension"], "description": "Field category Valid values are: \"parameter\", \"filter\", \"measure\", \"dimension\".", - "nullable": true + "x-looker-nullable": true }, "default_filter_value": { "type": "string", "readOnly": true, "description": "The default value that this field uses when filtering. Null if there is no default value.", - "nullable": true + "x-looker-nullable": true }, "description": { "type": "string", "readOnly": true, "description": "Description", - "nullable": true + "x-looker-nullable": true }, - "enumerations": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LookmlModelExploreFieldEnumeration" - }, + "dimension_group": { + "type": "string", "readOnly": true, - "description": "An array enumerating all the possible values that this field can contain. When null, there is no limit to the set of possible values this field can contain.", - "nullable": true + "description": "Dimension group if this field is part of a dimension group. If not, this will be null.", + "x-looker-nullable": true }, "error": { "type": "string", "readOnly": true, "description": "An error message indicating a problem with the definition of this field. If there are no errors, this will be null.", - "nullable": true + "x-looker-nullable": true }, "field_group_label": { "type": "string", "readOnly": true, "description": "A label creating a grouping of fields. All fields with this label should be presented together when displayed in a UI.", - "nullable": true + "x-looker-nullable": true }, "field_group_variant": { "type": "string", "readOnly": true, "description": "When presented in a field group via field_group_label, a shorter name of the field to be displayed in that context.", - "nullable": true + "x-looker-nullable": true }, "fill_style": { "type": "string", "readOnly": true, - "enum": ["enumeration", "range"], + "x-looker-values": ["enumeration", "range"], "description": "The style of dimension fill that is possible for this field. Null if no dimension fill is possible. Valid values are: \"enumeration\", \"range\".", - "nullable": true + "x-looker-nullable": true }, "fiscal_month_offset": { "type": "integer", "format": "int64", "readOnly": true, "description": "An offset (in months) from the calendar start month to the fiscal start month defined in the LookML model this field belongs to.", - "nullable": false + "x-looker-nullable": false }, "has_allowed_values": { "type": "boolean", "readOnly": true, "description": "Whether this field has a set of allowed_values specified in LookML.", - "nullable": false + "x-looker-nullable": false }, "hidden": { "type": "boolean", "readOnly": true, "description": "Whether this field should be hidden from the user interface.", - "nullable": false + "x-looker-nullable": false }, "is_filter": { "type": "boolean", "readOnly": true, "description": "Whether this field is a filter.", - "nullable": false + "x-looker-nullable": false }, "is_fiscal": { "type": "boolean", "readOnly": true, "description": "Whether this field represents a fiscal time value.", - "nullable": false + "x-looker-nullable": false }, "is_numeric": { "type": "boolean", "readOnly": true, "description": "Whether this field is of a type that represents a numeric value.", - "nullable": false + "x-looker-nullable": false }, "is_timeframe": { "type": "boolean", "readOnly": true, "description": "Whether this field is of a type that represents a time value.", - "nullable": false + "x-looker-nullable": false + }, + "is_nested": { + "type": "boolean", + "readOnly": true, + "description": "Whether this field is a nested!", + "x-looker-undocumented": true, + "x-looker-nullable": false + }, + "can_nest": { + "type": "boolean", + "readOnly": true, + "description": "Whether this field can actually be nested!", + "x-looker-undocumented": true, + "x-looker-nullable": false + }, + "nest_dimension": { + "$ref": "#/definitions/LookmlModelExploreField", + "readOnly": true, + "description": "Nest dimension for testing type recursion", + "x-looker-undocumented": true, + "x-looker-nullable": true }, "can_time_filter": { "type": "boolean", "readOnly": true, "description": "Whether this field can be time filtered.", - "nullable": false + "x-looker-nullable": false }, "time_interval": { - "$ref": "#/components/schemas/LookmlModelExploreFieldTimeInterval" + "$ref": "#/definitions/LookmlModelExploreFieldTimeInterval", + "readOnly": true, + "description": "Details on the time interval this field represents, if it is_timeframe.", + "x-looker-nullable": true }, "label": { "type": "string", "readOnly": true, "description": "Fully-qualified human-readable label of the field.", - "nullable": false + "x-looker-nullable": false }, "label_from_parameter": { "type": "string", "readOnly": true, "description": "The name of the parameter that will provide a parameterized label for this field, if available in the current context.", - "nullable": true + "x-looker-nullable": true }, "label_short": { "type": "string", "readOnly": true, "description": "The human-readable label of the field, without the view label.", - "nullable": false + "x-looker-nullable": false }, "lookml_link": { "type": "string", "readOnly": true, "description": "A URL linking to the definition of this field in the LookML IDE.", - "nullable": true + "x-looker-nullable": true }, "map_layer": { - "$ref": "#/components/schemas/LookmlModelExploreFieldMapLayer" + "$ref": "#/definitions/LookmlModelExploreFieldMapLayer", + "readOnly": true, + "description": "If applicable, a map layer this field is associated with.", + "x-looker-nullable": true }, "measure": { "type": "boolean", "readOnly": true, "description": "Whether this field is a measure.", - "nullable": false + "x-looker-nullable": false }, "name": { "type": "string", "readOnly": true, "description": "Fully-qualified name of the field.", - "nullable": false + "x-looker-nullable": false }, "strict_value_format": { "type": "boolean", "readOnly": true, "description": "If yes, the field will not be localized with the user attribute number_format. Defaults to no", - "nullable": false + "x-looker-nullable": false }, "parameter": { "type": "boolean", "readOnly": true, "description": "Whether this field is a parameter.", - "nullable": false + "x-looker-nullable": false }, "permanent": { "type": "boolean", "readOnly": true, "description": "Whether this field can be removed from a query.", - "nullable": true + "x-looker-nullable": true }, "primary_key": { "type": "boolean", "readOnly": true, "description": "Whether or not the field represents a primary key.", - "nullable": false + "x-looker-nullable": false }, "project_name": { "type": "string", "readOnly": true, "description": "The name of the project this field is defined in.", - "nullable": true + "x-looker-nullable": true }, "requires_refresh_on_sort": { "type": "boolean", "readOnly": true, "description": "When true, it's not possible to re-sort this field's values without re-running the SQL query, due to database logic that affects the sort.", - "nullable": false + "x-looker-nullable": false }, "scope": { "type": "string", "readOnly": true, "description": "The LookML scope this field belongs to. The scope is typically the field's view.", - "nullable": false + "x-looker-nullable": false }, "sortable": { "type": "boolean", "readOnly": true, "description": "Whether this field can be sorted.", - "nullable": false + "x-looker-nullable": false }, "source_file": { "type": "string", "readOnly": true, "description": "The path portion of source_file_path.", - "nullable": false + "x-looker-nullable": false }, "source_file_path": { "type": "string", "readOnly": true, "description": "The fully-qualified path of the project file this field is defined in.", - "nullable": false + "x-looker-nullable": false }, "sql": { "type": "string", "readOnly": true, "description": "SQL expression as defined in the LookML model. The SQL syntax shown here is a representation intended for auditability, and is not neccessarily an exact match for what will ultimately be run in the database. It may contain special LookML syntax or annotations that are not valid SQL. This will be null if the current user does not have the see_lookml permission for the field's model.", - "nullable": true + "x-looker-nullable": true }, "sql_case": { "type": "array", - "items": { - "$ref": "#/components/schemas/LookmlModelExploreFieldSqlCase" - }, + "items": { "$ref": "#/definitions/LookmlModelExploreFieldSqlCase" }, "readOnly": true, "description": "An array of conditions and values that make up a SQL Case expression, as defined in the LookML model. The SQL syntax shown here is a representation intended for auditability, and is not neccessarily an exact match for what will ultimately be run in the database. It may contain special LookML syntax or annotations that are not valid SQL. This will be null if the current user does not have the see_lookml permission for the field's model.", - "nullable": true + "x-looker-nullable": true }, "filters": { "type": "array", "items": { - "$ref": "#/components/schemas/LookmlModelExploreFieldMeasureFilters" + "$ref": "#/definitions/LookmlModelExploreFieldMeasureFilters" }, "readOnly": true, "description": "Array of filter conditions defined for the measure in LookML.", - "nullable": true + "x-looker-nullable": true }, "suggest_dimension": { "type": "string", "readOnly": true, "description": "The name of the dimension to base suggest queries from.", - "nullable": false + "x-looker-nullable": false }, "suggest_explore": { "type": "string", "readOnly": true, "description": "The name of the explore to base suggest queries from.", - "nullable": false + "x-looker-nullable": false }, "suggestable": { "type": "boolean", "readOnly": true, "description": "Whether or not suggestions are possible for this field.", - "nullable": false + "x-looker-nullable": false }, "suggestions": { "type": "array", "items": { "type": "string" }, "readOnly": true, "description": "If available, a list of suggestions for this field. For most fields, a suggest query is a more appropriate way to get an up-to-date list of suggestions. Or use enumerations to list all the possible values.", - "nullable": true + "x-looker-nullable": true }, "tags": { "type": "array", "items": { "type": "string" }, "readOnly": true, "description": "An array of arbitrary string tags provided in the model for this field.", - "nullable": false + "x-looker-nullable": false }, "type": { "type": "string", "readOnly": true, "description": "The LookML type of the field.", - "nullable": false + "x-looker-nullable": false }, "user_attribute_filter_types": { "type": "array", "items": { "type": "string" }, "readOnly": true, - "enum": [ + "x-looker-values": [ "advanced_filter_string", "advanced_filter_number", "advanced_filter_datetime", @@ -30334,36 +30356,36 @@ "zipcode" ], "description": "An array of user attribute types that are allowed to be used in filters on this field. Valid values are: \"advanced_filter_string\", \"advanced_filter_number\", \"advanced_filter_datetime\", \"string\", \"number\", \"datetime\", \"relative_url\", \"yesno\", \"zipcode\".", - "nullable": false + "x-looker-nullable": false }, "value_format": { "type": "string", "readOnly": true, "description": "If specified, the LookML value format string for formatting values of this field.", - "nullable": true + "x-looker-nullable": true }, "view": { "type": "string", "readOnly": true, "description": "The name of the view this field belongs to.", - "nullable": false + "x-looker-nullable": false }, "view_label": { "type": "string", "readOnly": true, "description": "The human-readable label of the view the field belongs to.", - "nullable": false + "x-looker-nullable": false }, "dynamic": { "type": "boolean", "readOnly": true, "description": "Whether this field was specified in \"dynamic_fields\" and is not part of the model.", - "nullable": false + "x-looker-nullable": false }, "week_start_day": { "type": "string", "readOnly": true, - "enum": [ + "x-looker-values": [ "monday", "tuesday", "wednesday", @@ -30373,14 +30395,20 @@ "sunday" ], "description": "The name of the starting day of the week. Valid values are: \"monday\", \"tuesday\", \"wednesday\", \"thursday\", \"friday\", \"saturday\", \"sunday\".", - "nullable": false + "x-looker-nullable": false }, "times_used": { "type": "integer", "format": "int64", "readOnly": true, "description": "The number of times this field has been used in queries", - "nullable": false + "x-looker-nullable": false + }, + "original_view": { + "type": "string", + "readOnly": true, + "description": "The name of the view this field is defined in. This will be different than \"view\" when the view has been joined via a different name using the \"from\" parameter.", + "x-looker-nullable": false } }, "x-looker-status": "stable" @@ -33589,12 +33617,6 @@ "description": "True for persistent derived table support", "nullable": false }, - "turtles": { - "type": "boolean", - "readOnly": true, - "description": "True for turtles support", - "nullable": false - }, "percentile": { "type": "boolean", "readOnly": true,