Skip to content

Commit

Permalink
feat: support either string or numeric JSON values with AnyString (#848)
Browse files Browse the repository at this point in the history
This PR uses an `AnyString` getter/setter for string ids to handle either numeric or string representations of the ID

This allows the SDK to process older Looker API payloads as those types are moved to string from number but the SDK model is updated to use string

Numeric IDs are converted to string
  • Loading branch information
jkaster authored Oct 12, 2021
1 parent bb08f25 commit 9b428f5
Show file tree
Hide file tree
Showing 33 changed files with 5,425 additions and 925 deletions.
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v12
v14
4 changes: 4 additions & 0 deletions packages/sdk-codegen/src/codeGen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ export interface ICodeGen {
*/
sdkPath: string

/** use special handling for a JSON value that can be a string or a number. Introduced for Swift. */
anyString: boolean

/** current version of the Api being generated */
apiVersion: string

Expand Down Expand Up @@ -717,6 +720,7 @@ export interface ICodeGen {

export abstract class CodeGen implements ICodeGen {
willItStream = false
anyString = false
codePath = './'
packagePath = 'looker'
sdkPath = 'sdk'
Expand Down
95 changes: 92 additions & 3 deletions packages/sdk-codegen/src/swift.gen.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,23 +79,27 @@ public enum PermissionType: String, Codable {
})
})

describe('special symbols', () => {
it('generates coding keys', () => {
describe('special handling', () => {
it('generates coding keys for special property names', () => {
const type = apiTestModel.types.HyphenType
const actual = gen.declareType(indent, type)
const expected = `public struct HyphenType: SDKModel {
private enum CodingKeys : String, CodingKey {
case project_name, project_digest = "project-digest", computation_time = "computation time"
case project_name
case project_digest = "project-digest"
case computation_time = "computation time"
}
/**
* A normal variable name (read-only)
*/
public var project_name: String?
/**
* A hyphenated property name (read-only)
*/
public var project_digest: String?
/**
* A spaced out property name (read-only)
*/
Expand All @@ -107,6 +111,88 @@ public enum PermissionType: String, Codable {
self.computation_time = computation_time
}
}`
expect(actual).toEqual(expected)
})

it('optional string ID properties use map to AnyString', () => {
const type = apiTestModel.types.GitConnectionTestResult
const actual = gen.declareType(indent, type)
const expected = `public struct GitConnectionTestResult: SDKModel {
private enum CodingKeys : String, CodingKey {
case can
case _id = "id"
case message
case status
}
/**
* Operations the current user is able to perform on this object (read-only)
*/
public var can: StringDictionary<Bool>?
private var _id: AnyString?
/**
* A short string, uniquely naming this test (read-only)
*/
public var id: String? {
get { _id?.value }
set { _id = newValue.map(AnyString.init) }
}
/**
* Additional data from the test (read-only)
*/
public var message: String?
/**
* Either 'pass' or 'fail' (read-only)
*/
public var status: String?
public init(can: StringDictionary<Bool>? = nil, id: String? = nil, message: String? = nil, status: String? = nil) {
self.can = can
self._id = id.map(AnyString.init)
self.message = message
self.status = status
}
}`
expect(actual).toEqual(expected)
})

it('required string ID properties use map to AnyString', () => {
const type = apiTestModel.types.CreateFolder
const actual = gen.declareType(indent, type)
const expected = `public struct CreateFolder: SDKModel {
private enum CodingKeys : String, CodingKey {
case name
case _parent_id = "parent_id"
}
/**
* Unique Name
*/
public var name: String
private var _parent_id: AnyString
/**
* Id of Parent. If the parent id is null, this is a root-level entry
*/
public var parent_id: String {
get { _parent_id.value }
set { _parent_id = AnyString.init(newValue) }
}
public init(name: String, parent_id: String) {
self.name = name
self._parent_id = AnyString.init(parent_id)
}
public init(_ name: String, _ parent_id: String) {
self.init(name: name, parent_id: parent_id)
}
}`
expect(actual).toEqual(expected)
})
Expand All @@ -121,10 +207,12 @@ public enum PermissionType: String, Codable {
* The complete URL of the Looker UI page to display in the embed context. For example, to display the dashboard with id 34, \`target_url\` would look like: \`https://mycompany.looker.com:9999/dashboards/34\`. \`target_uri\` MUST contain a scheme (HTTPS), domain name, and URL path. Port must be included if it is required to reach the Looker server from browser clients. If the Looker instance is behind a load balancer or other proxy, \`target_uri\` must be the public-facing domain name and port required to reach the Looker instance, not the actual internal network machine name of the Looker instance.
*/
public var target_url: URI
/**
* Number of seconds the SSO embed session will be valid after the embed session is started. Defaults to 300 seconds. Maximum session length accepted is 2592000 seconds (30 days).
*/
public var session_length: Int64?
/**
* When true, the embed session will purge any residual Looker login state (such as in browser cookies) before creating a new login state with the given embed user info. Defaults to true.
*/
Expand All @@ -151,6 +239,7 @@ public enum PermissionType: String, Codable {
* CSS color string
*/
public var color: String?
/**
* Offset in continuous palette (0 to 100)
*/
Expand Down
91 changes: 79 additions & 12 deletions packages/sdk-codegen/src/swift.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import type { IMappedType } from './codeGen'
import { CodeGen, commentBlock } from './codeGen'

export class SwiftGen extends CodeGen {
// Use AnyString JSON parser for number or string ids
anyString = true
codePath = './swift/'
packagePath = 'looker'
itself = 'self'
Expand Down Expand Up @@ -232,6 +234,27 @@ import Foundation
return '' // No end MARK in Swift, and XCode appears to no longer process MARKs anyway
}

/**
* true if this property should use AnyString
* @param property to check
*/
useAnyString(property: IProperty) {
const nameCheck = property.name.toLowerCase()
return (
this.anyString &&
property.type.name.toLowerCase() === 'string' &&
(nameCheck === 'id' || nameCheck.endsWith('_id'))
)
}

/**
* Private version of the name (_ prefix)
* @param name to privatize
*/
privy(name: string) {
return this.reserve('_' + name)
}

declareProperty(indent: string, property: IProperty) {
// const optional = (property.nullable || !property.required) ? '?' : ''
const optional = property.required ? '' : '?'
Expand All @@ -250,11 +273,29 @@ import Foundation
)
}
const type = this.typeMap(property.type)
const specialHandling = this.useAnyString(property)
let munge = ''
let declaration = `${indent}public var ${this.reserve(property.name)}: ${
type.name
}${optional}\n`
if (specialHandling) {
const privy = this.reserve('_' + property.name)
const bump = this.bumper(indent)
const setter = property.required
? 'AnyString.init(newValue)'
: 'newValue.map(AnyString.init)'
munge = `${indent}private var ${privy}: AnyString${optional}\n`
declaration = `${indent}public var ${this.reserve(property.name)}: ${
type.name
}${optional} {
${bump}get { ${privy}${optional}.value }
${bump}set { ${privy} = ${setter} }
${indent}}\n`
}
return (
munge +
this.commentHeader(indent, this.describeProperty(property)) +
`${indent}public var ${this.reserve(property.name)}: ${
type.name
}${optional}`
declaration
)
}

Expand Down Expand Up @@ -298,7 +339,18 @@ import Foundation
const propName = this.reserve(prop.name)
args.push(this.declareConstructorArg('', prop))
posArgs.push(this.declarePositionalArg('', prop))
inits.push(`${bump}${this.it(propName)} = ${propName}`)
if (this.useAnyString(prop)) {
const varName = this.privy(propName)
if (prop.required) {
inits.push(`${bump}${this.it(varName)} = AnyString.init(${propName})`)
} else {
inits.push(
`${bump}${this.it(varName)} = ${propName}.map(AnyString.init)`
)
}
} else {
inits.push(`${bump}${this.it(propName)} = ${propName}`)
}
posInits.push(`${propName}: ${propName}`)
})
const namedInit =
Expand All @@ -314,7 +366,7 @@ import Foundation
`${bump}${this.it('init')}(${posInits.join(', ')})` +
`\n${indent}}\n`
}
return `\n\n${namedInit}\n${posInit}`
return `\n${namedInit}\n${posInit}`
}

declarePositionalArg(indent: string, property: IProperty) {
Expand Down Expand Up @@ -352,7 +404,7 @@ import Foundation
let headComment =
(head ? `${head}\n\n` : '') +
`${method.httpMethod} ${method.endpoint} -> ${type.name}`
let fragment = ''
let fragment
const requestType = this.requestTypeName(method)
const bump = indent + this.indentStr

Expand Down Expand Up @@ -425,16 +477,31 @@ import Foundation
}

codingKeys(indent: string, type: IType) {
if (!type.hasSpecialNeeds) return ''
let special = false

const keys = Object.values(type.properties).map((p) => {
let name = this.reserve(p.name)
let alias = ''
const useIt = this.useAnyString(p)
if (useIt) {
name = this.privy(name)
special = true
alias = p.jsonName
} else if (p.hasSpecialNeeds) {
special = true
alias = p.jsonName
}
return name + (alias ? ` = "${alias}"` : '')
})

if (!special) return ''
const bump = this.bumper(indent)
const bump2 = this.bumper(bump)
const keys = Object.values(type.properties).map(
(p) => p.name + (p.hasSpecialNeeds ? ` = "${p.jsonName}"` : '')
)
const cases = keys.join(`\n${bump2}case `)

return (
`\n${bump}private enum CodingKeys : String, CodingKey {` +
`\n${bump2}case ${keys.join(', ')}` +
`\n${bump2}case ${cases}` +
`\n${bump}}\n`
)
}
Expand Down Expand Up @@ -519,7 +586,7 @@ import Foundation
}

asAny(param: IParameter): Arg {
let castIt = false
let castIt
if (param.type.elementType) {
castIt = true
} else {
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-codegen/src/typescript.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export interface I${this.packageName} extends IAPIMethods {

streamsPrologue(_indent: string): string {
return `
import { Readable } from 'readable-stream'
import type { Readable } from 'readable-stream'
import type { ${this.rtlImports()}IAuthSession, ITransportSettings } from '@looker/sdk-rtl'
import { APIMethods, encodeParam } from '@looker/sdk-rtl'
Expand Down
8 changes: 8 additions & 0 deletions swift/looker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ func testBothPositionalAndNamed() {
}
```

### AnyString usage

Some of the API model structures have a private variable of type `AnyString`. This special type was introduced to handle JSON values that can be either string or numeric.

For Looker API 4.0, all entity ID references are being converted to string (some are currently integer) to prepare for potential scalability changes for entity references.

This special `AnyString` wrapper supports an ID being either numeric or string, so it will work for older Looker releases that still have numeric IDs, and will also work for string IDs.

### More examples

Additional Swift SDK usage examples may be found in the [SDK Examples repository](https://github.com/looker-open-source/sdk-examples/tree/main/swift).
Expand Down
26 changes: 26 additions & 0 deletions swift/looker/Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
/**
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 XCTest

import lookerTests
Expand Down
26 changes: 26 additions & 0 deletions swift/looker/Tests/lookerTests/XCTestManifests.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
/**
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 XCTest

#if !canImport(ObjectiveC)
Expand Down
Loading

0 comments on commit 9b428f5

Please sign in to comment.