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

RUMM-2266 Reorganise rum-models-generator package for Session Replay code generation #940

Merged
merged 2 commits into from
Jul 26, 2022
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
35 changes: 30 additions & 5 deletions tools/rum-models-generator/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,45 @@ let package = Package(
.package(name: "Difference", url: "https://github.com/krzysztofzablocki/Difference.git", from: "0.5.0"),
],
targets: [
// CLI wrapper
.target(
name: "rum-models-generator",
dependencies: [
"RUMModelsGeneratorCore",
"CodeGeneration",
"CodeDecoration",
.product(name: "ArgumentParser", package: "swift-argument-parser")
]),
]
),
Comment on lines +14 to +22
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it is no longer rum- models generator, we may want to rename the package. I don't do this, because it has consequences on other files in the repo (all places where we call it from). I didn't want to spend time on it right now, but it's IMO something we might want consider in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's fine keep it for now 👍

.testTarget(
name: "rum-models-generatorTests",
dependencies: ["rum-models-generator",]
),

// Product-agnostic code generator (JSON Schema -> Swift | Objc-interop)
.target(
name: "RUMModelsGeneratorCore",
name: "CodeGeneration",
dependencies: []
),
.testTarget(
name: "rum-models-generator-coreTests",
dependencies: ["RUMModelsGeneratorCore", "Difference"],
name: "CodeGenerationTests",
dependencies: [
"CodeGeneration",
"Difference"
],
resources: [.copy("Fixtures")]
),

// Product-specific code decorators
.target(
name: "CodeDecoration",
dependencies: ["CodeGeneration"]
),
.testTarget(
name: "CodeDecorationTests",
dependencies: [
"CodeDecoration",
"Difference"
]
),
]
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,22 @@
*/

import Foundation
import CodeGeneration

/// Adjusts naming and structure of generated code for RUM.
public class RUMCodeDecorator: CodeDecorator {
public init() {}

// MARK: - CodeDecorator

public func decorate(code: GeneratedCode) throws -> GeneratedCode {
return GeneratedCode(
swiftTypes: try transform(types: code.swiftTypes)
)
}

// MARK: - Internal

/// Transforms `SwiftTypes` for RUM code generation.
internal class RUMSwiftTypeTransformer {
/// Types which will shared between all input `types`. Sharing means detaching those types from nested declaration
/// and putting them at the root level of the resultant `types` array, so the type can be printed without being nested.
private let sharedTypeNames = [
Expand Down Expand Up @@ -269,3 +282,28 @@ internal class RUMSwiftTypeTransformer {
return SwiftTypeReference(referencedTypeName: name)
}
}

// MARK: - Utilities

extension String {
private var camelCased: String {
guard !isEmpty else {
return ""
}

let words = components(separatedBy: CharacterSet.alphanumerics.inverted)
let first = words.first! // swiftlint:disable:this force_unwrapping
let rest = words.dropFirst().map { $0.uppercasingFirst }
return ([first] + rest).joined(separator: "")
}

/// Uppercases the first character.
var uppercasingFirst: String { prefix(1).uppercased() + dropFirst() }
/// Lowercases the first character.
var lowercasingFirst: String { prefix(1).lowercased() + dropFirst() }

/// "lowerCamelCased" notation.
var lowerCamelCased: String { camelCased.lowercasingFirst }
/// "UpperCamelCased" notation.
var upperCamelCased: String { camelCased.uppercasingFirst }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-2020 Datadog, Inc.
*/

import Foundation
import CodeGeneration

/// Adjusts naming and structure of generated code for Session Replay.
public class SRCodeDecorator: CodeDecorator {
public init() {}

// MARK: - CodeDecorator

public func decorate(code: GeneratedCode) throws -> GeneratedCode {
return code // RUMM-2266 Implement actual decoration
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-2020 Datadog, Inc.
*/

import Foundation

/// Type-safe Swift schema.
public protocol SwiftType {}

/// Swift primitive type.
public protocol SwiftPrimitiveType: SwiftType {}
/// An allowed value of Swift primitive type.
public protocol SwiftPrimitiveValue {}
/// An allowed default value of Swift property.
public protocol SwiftPropertyDefaultValue {}
/// An allowed value of Swift with no obj-c interoperability.
public protocol SwiftPrimitiveNoObjcInteropType: SwiftPrimitiveType {}

Comment on lines +1 to +20
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only changes in this file are internalpublic access modifiers for all pieces of Swift schema. This is to make it visible for external module which performs code decoration.

extension Bool: SwiftPrimitiveValue, SwiftPropertyDefaultValue {}
extension Int: SwiftPrimitiveValue, SwiftPropertyDefaultValue {}
extension Int64: SwiftPrimitiveValue, SwiftPropertyDefaultValue {}
extension String: SwiftPrimitiveValue, SwiftPropertyDefaultValue {}
extension Double: SwiftPrimitiveValue, SwiftPropertyDefaultValue {}

/// Represents `Swift.Codable` - we need to define utility type because it cannot be declared as `extension` to `Codable`.
public struct SwiftCodable: SwiftPrimitiveNoObjcInteropType {}

/// Represents `Swift.Encodable` - we need to define utility type because it cannot be declared as `extension` to `Encodable`.
public struct SwiftEncodable: SwiftPrimitiveNoObjcInteropType {}

public struct SwiftPrimitive<T: SwiftPrimitiveValue>: SwiftPrimitiveType {
public init() {}
}

public struct SwiftArray: SwiftType {
public var element: SwiftType
}

public struct SwiftDictionary: SwiftType {
public let key = SwiftPrimitive<String>()
public var value: SwiftPrimitiveType
}

/// An `enum` with raw-type cases.
public struct SwiftEnum: SwiftType {
public struct Case: SwiftType, SwiftPropertyDefaultValue {
public enum RawValue {
case string(value: String)
case integer(value: Int)
}
public var label: String
public var rawValue: RawValue
}

public var name: String
public var comment: String?
public var cases: [Case]
public var conformance: [SwiftProtocol]
}

/// An `enum` with associated-type cases.
public struct SwiftAssociatedTypeEnum: SwiftType {
public struct Case: SwiftType, SwiftPropertyDefaultValue {
public var label: String
public var associatedType: SwiftType
}

public var name: String
public var comment: String?
public var cases: [Case]
public var conformance: [SwiftProtocol]
}

public struct SwiftStruct: SwiftType {
public struct Property: SwiftType {
/// Mutability levels of a property.
/// From the lowest `immutable` to the highest `mutable`.
public enum Mutability: Int {
case immutable
case mutableInternally
case mutable
}

public enum CodingKey {
/// Static coding key with fixed value.
case `static`(value: String)
/// Dynamic coding key with value determined at runtime.
case `dynamic`

public var isStatic: Bool {
switch self {
case .static: return true
case .dynamic: return false
}
}
}

public var name: String
public var comment: String?
public var type: SwiftType
public var isOptional: Bool
public var mutability: Mutability
public var defaultValue: SwiftPropertyDefaultValue?
public var codingKey: CodingKey
}

public var name: String
public var comment: String?
public var properties: [Property]
public var conformance: [SwiftProtocol]
}

public struct SwiftProtocol: SwiftType {
public var name: String
public var conformance: [SwiftProtocol]

public init(name: String, conformance: [SwiftProtocol]) {
self.name = name
self.conformance = conformance
}
}

/// Reference to any other Swift type.
public struct SwiftTypeReference: SwiftType {
public var referencedTypeName: String

public init(referencedTypeName: String) {
self.referencedTypeName = referencedTypeName
}
}

public let codableProtocol = SwiftProtocol(name: "Codable", conformance: [])

// MARK: - Helpers

public extension SwiftType {
/// The name of this type (or `nil` if this type is unnamed).
var typeName: String? {
let `struct` = self as? SwiftStruct
let `enum` = self as? SwiftEnum
let associatedTypeEnum = self as? SwiftAssociatedTypeEnum
return `struct`?.name ?? `enum`?.name ?? associatedTypeEnum?.name
}
}

// MARK: - Equatable

extension SwiftStruct: Equatable {
public static func == (lhs: SwiftStruct, rhs: SwiftStruct) -> Bool {
return String(describing: lhs) == String(describing: rhs)
}
}

extension SwiftEnum: Equatable {
public static func == (lhs: SwiftEnum, rhs: SwiftEnum) -> Bool {
return String(describing: lhs) == String(describing: rhs)
}
}

public func == (lhs: SwiftType, rhs: SwiftType) -> Bool {
return String(describing: lhs) == String(describing: rhs)
}

public func != (lhs: SwiftType, rhs: SwiftType) -> Bool {
return !(lhs == rhs)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-2020 Datadog, Inc.
*/

import Foundation

/// Generates code from provided JSON schema file.
public struct ModelsGenerator {
public init() {}

/// Generates code from JSON schema file.
/// - Parameter schemaFileURL: the URL of schema file
/// - Returns: schema for generated code.
public func generateCode(from schemaFileURL: URL) throws -> GeneratedCode {
let jsonSchema = try JSONSchemaReader().read(schemaFileURL)
let jsonType = try JSONSchemaToJSONTypeTransformer().transform(jsonSchema: jsonSchema)
let swiftTypes = try JSONToSwiftTypeTransformer().transform(jsonType: jsonType)
return GeneratedCode(swiftTypes: swiftTypes)
}
}

/// Schema describing generated code.
public struct GeneratedCode {
/// An array of Swift schemas describing root constructs in generated code.
public let swiftTypes: [SwiftType]

/// Changes this schema by applying provided decoration.
/// It can be used to adjust naming and structure of generated code.
public func decorate(using decorator: CodeDecorator) throws -> GeneratedCode {
return try decorator.decorate(code: self)
}

/// Renders generated code with provided template.
public func print(using template: OutputTemplate, and printer: CodePrinter) throws -> String {
let codeText = try printer.print(code: self)
return template.render(code: codeText)
}

public init(swiftTypes: [SwiftType]) {
self.swiftTypes = swiftTypes
}
}

/// A type decorating generated code.
/// Decoration can be used to adjust naming and structure in generated code before it is printed.
public protocol CodeDecorator {
func decorate(code: GeneratedCode) throws -> GeneratedCode
}

/// The template for generated code file.
public struct OutputTemplate {
let header: String
let footer: String

/// Initializer.
/// - Parameters:
/// - header: a text block to put before generated code
/// - footer: a text block to put after generated code
public init(header: String, footer: String) {
self.header = header
self.footer = footer
}

func render(code: String) -> String {
return [header, code, footer].joined()
}
}

/// Prints generated code.
public protocol CodePrinter {
func print(code: GeneratedCode) throws -> String
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,24 @@ import Foundation
/// public var integer: NSNumber { foo.integer as NSNumber }
/// }
///
internal class ObjcInteropPrinter: BasePrinter {
/// The prefix used for types exposed to Obj-c.
private let objcTypeNamesPrefix: String

init(objcTypeNamesPrefix: String) {
public class ObjcInteropPrinter: BasePrinter, CodePrinter {
public init(objcTypeNamesPrefix: String) {
self.objcTypeNamesPrefix = objcTypeNamesPrefix
}

// MARK: - CodePrinter

public func print(code: GeneratedCode) throws -> String {
let objcInteropTransformer = SwiftToObjcInteropTypeTransformer()
let objcInteropTypes = try objcInteropTransformer.transform(swiftTypes: code.swiftTypes)
return try print(objcInteropTypes: objcInteropTypes)
}

// MARK: - Internal

/// The prefix used for types exposed to Obj-c.
private let objcTypeNamesPrefix: String

func print(objcInteropTypes: [ObjcInteropType]) throws -> String {
reset()
try objcInteropTypes.forEach { try print(objcInteropType: $0) }
Expand Down
Loading