Skip to content

Commit

Permalink
Add Embeddable type to store schema info for custom types (#539)
Browse files Browse the repository at this point in the history
* Add Embbeded types support

* Renaming to Embeddable type
  • Loading branch information
lawmicha authored Jun 16, 2020
1 parent dac0b46 commit 7b5611b
Show file tree
Hide file tree
Showing 17 changed files with 436 additions and 19 deletions.
50 changes: 43 additions & 7 deletions Amplify.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions Amplify/Categories/DataStore/Model/Embedded.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// Copyright 2018-2020 Amazon.com,
// Inc. or its affiliates. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

// MARK: - Embeddable

/// A `Embeddable` type can be used in a `Model` as an embedded type. All types embedded in a `Model` as an
/// `embedded(type:)` or `embeddedCollection(of:)` must comform to the `Embeddable` protocol except for Swift's Basic
/// types embedded as a collection. A collection of String can be embedded in the `Model` as
/// `embeddedCollection(of: String.self)` without needing to conform to Embeddable.
public protocol Embeddable: Codable {

/// A reference to the `ModelSchema` associated with this embedded type.
static var schema: ModelSchema { get }
}

extension Embeddable {
public static func defineSchema(name: String? = nil,
attributes: ModelAttribute...,
define: (inout ModelSchemaDefinition) -> Void) -> ModelSchema {
var definition = ModelSchemaDefinition(name: name ?? "",
attributes: attributes)
define(&definition)
return definition.build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -183,4 +183,24 @@ extension ModelField {
return false
}

public var embeddedType: Embeddable.Type? {
switch type {
case .embedded(let type), .embeddedCollection(let type):
if let embeddedType = type as? Embeddable.Type {
return embeddedType
}
return nil
default:
return nil
}
}

public var isEmbeddedType: Bool {
switch type {
case .embedded, .embeddedCollection:
return true
default:
return false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ public enum ModelFieldType {
case timestamp
case bool
case `enum`(type: EnumPersistable.Type)
case customType(_ type: Codable.Type)
case embedded(type: Codable.Type)
case embeddedCollection(of: Codable.Type)
case model(type: Model.Type)
case collection(of: Model.Type)

public var isArray: Bool {
switch self {
case .collection:
case .collection, .embeddedCollection:
return true
default:
return false
Expand Down Expand Up @@ -63,8 +64,8 @@ public enum ModelFieldType {
if let modelType = type as? Model.Type {
return .model(type: modelType)
}
if let codableType = type as? Codable.Type {
return .customType(codableType)
if let embeddedType = type as? Codable.Type {
return .embedded(type: embeddedType)
}
preconditionFailure("Could not create a ModelFieldType from \(String(describing: type)) MetaType")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public struct ConflictResolutionDecorator: ModelBasedGraphQLDocumentDecorator {
/// Append the correct conflict resolution fields for `model` and `pagination` selection sets.
private func addConflictResolution(selectionSet: SelectionSet) {
switch selectionSet.value.fieldType {
case .value:
case .value, .embedded:
break
case .model:
selectionSet.addChild(settingParentOf: .init(value: .init(name: "_version", fieldType: .value)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ extension Model {
// TODO how to handle associations of type "many" (i.e. cascade save)?
// This is not supported right now and might be added as a future feature
break
case .embedded, .embeddedCollection:
if let encodable = value as? Encodable {
let jsonEncoder = JSONEncoder(dateEncodingStrategy: ModelDateFormatting.encodingStrategy)
do {
let data = try jsonEncoder.encode(encodable.eraseToAnyEncodable())
input[name] = try JSONSerialization.jsonObject(with: data)
} catch {
preconditionFailure("Could not turn into json object from \(value)")
}
}
default:
input[name] = value
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public typealias SelectionSet = Tree<SelectionSetField>
public enum SelectionSetFieldType {
case pagination
case model
case embedded
case value
}

Expand All @@ -35,7 +36,11 @@ extension SelectionSet {

func withModelFields(_ fields: [ModelField]) {
fields.forEach { field in
if field.isAssociationOwner, let associatedModel = field.associatedModel {
if field.isEmbeddedType, let embeddedType = field.embeddedType {
let child = SelectionSet(value: .init(name: field.name, fieldType: .embedded))
child.withCodableFields(embeddedType.schema.sortedFields)
self.addChild(settingParentOf: child)
} else if field.isAssociationOwner, let associatedModel = field.associatedModel {
let child = SelectionSet(value: .init(name: field.name, fieldType: .model))
child.withModelFields(associatedModel.schema.graphQLFields)
self.addChild(settingParentOf: child)
Expand All @@ -47,6 +52,19 @@ extension SelectionSet {
addChild(settingParentOf: .init(value: .init(name: "__typename", fieldType: .value)))
}

func withCodableFields(_ fields: [ModelField]) {
fields.forEach { field in
if field.isEmbeddedType, let embeddedType = field.embeddedType {
let child = SelectionSet(value: .init(name: field.name, fieldType: .embedded))
child.withCodableFields(embeddedType.schema.sortedFields)
self.addChild(settingParentOf: child)
} else {
self.addChild(settingParentOf: .init(value: .init(name: field.name, fieldType: .value)))
}
}
addChild(settingParentOf: .init(value: .init(name: "__typename", fieldType: .value)))
}

/// Generate the string value of the `SelectionSet` used in the GraphQL query document
///
/// This method operates on `SelectionSet` with the root node containing a nil `value.name` and expects all inner
Expand All @@ -68,7 +86,7 @@ extension SelectionSet {
let indent = indentSize == 0 ? "" : String(repeating: " ", count: indentSize)

switch value.fieldType {
case .model, .pagination:
case .model, .pagination, .embedded:
if let name = value.name {
result.append(indent + name + " {")
children.forEach { innerSelectionSetField in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public struct ModelMultipleOwner: Model {
model.fields(
.id(),
.field(modelMultipleOwner.content, is: .required, ofType: .string),
.field(modelMultipleOwner.editors, is: .optional, ofType: .customType([String].self))
.field(modelMultipleOwner.editors, is: .optional, ofType: .embeddedCollection(of: String.self))
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// Copyright 2018-2020 Amazon.com,
// Inc. or its affiliates. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import XCTest

@testable import Amplify
@testable import AmplifyTestCommon
@testable import AWSPluginsCore

class GraphQLRequestNonModelTests: XCTestCase {

override func setUp() {
ModelRegistry.register(modelType: Todo.self)
}

override func tearDown() {
ModelRegistry.reset()
}

func testCreateTodoGraphQLRequest() {
let color1 = Color(name: "color1", red: 1, green: 2, blue: 3)
let color2 = Color(name: "color2", red: 12, green: 13, blue: 14)
let category1 = Category(name: "green", color: color1)
let category2 = Category(name: "red", color: color2)
let section = Section(name: "section", number: 1.1)
let todo = Todo(name: "my first todo",
description: "todo description",
categories: [category1, category2],
section: section)
let documentStringValue = """
mutation CreateTodo($input: CreateTodoInput!) {
createTodo(input: $input) {
id
categories {
color {
blue
green
name
red
__typename
}
name
__typename
}
description
name
section {
name
number
__typename
}
stickies
__typename
}
}
"""
let request = GraphQLRequest<Todo>.create(todo)
XCTAssertEqual(documentStringValue, request.document)

guard let variables = request.variables else {
XCTFail("The request doesn't contain variables")
return
}
guard let input = variables["input"] as? [String: Any] else {
XCTFail("The document variables property doesn't contain a valid input")
return
}
XCTAssertEqual(input["id"] as? String, todo.id)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,33 @@ class ModelGraphQLTests: XCTestCase {
XCTAssertTrue(graphQLInput.keys.contains("updatedAt"))
XCTAssertNil(graphQLInput["updatedAt"]!)
}

func testTodoModelToGraphQLInputSuccess() {
let color = Color(name: "red", red: 255, green: 0, blue: 0)
let category = Category(name: "green", color: color)
let todo = Todo(name: "name",
description: "description",
categories: [category],
stickies: ["stickie1"])

let graphQLInput = todo.graphQLInput

XCTAssertEqual(graphQLInput["id"] as? String, todo.id)
XCTAssertEqual(graphQLInput["name"] as? String, todo.name)
XCTAssertEqual(graphQLInput["description"] as? String, todo.description)
guard let categories = graphQLInput["categories"] as? [[String: Any]] else {
XCTFail("Couldn't get array of categories")
return
}
XCTAssertEqual(categories.count, 1)
XCTAssertEqual(categories[0]["name"] as? String, category.name)
guard let expectedColor = categories[0]["color"] as? [String: Any] else {
XCTFail("Couldn't get color in category")
return
}
XCTAssertEqual(expectedColor["name"] as? String, color.name)
XCTAssertEqual(expectedColor["red"] as? Int, color.red)
XCTAssertEqual(expectedColor["green"] as? Int, color.green)
XCTAssertEqual(expectedColor["blue"] as? Int, color.blue)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public struct SQLiteModelValueConverter: ModelValueConverter {
// collections are not converted to SQL Binding since they represent a model association
// and the foreign key lives on the other side of the association
return nil
case .customType:
case .embedded, .embeddedCollection:
if let encodable = value as? Encodable {
return try SQLiteModelValueConverter.toJSON(encodable)
}
Expand Down Expand Up @@ -77,7 +77,7 @@ public struct SQLiteModelValueConverter: ModelValueConverter {
return nil
case .enum:
return value as? String
case .customType:
case .embedded, .embeddedCollection:
if let stringValue = value as? String {
return try SQLiteModelValueConverter.fromJSON(stringValue)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ extension ExampleWithEveryType {
.field(example.boolField, is: .required, ofType: .bool),
.field(example.dateField, is: .required, ofType: .date),
.field(example.enumField, is: .required, ofType: .enum(type: ExampleEnum.self)),
.field(example.nonModelField, is: .required, ofType: .customType(ExampleNonModelType.self)),
.field(example.arrayOfStringsField, is: .required, ofType: .customType([String].self))
.field(example.nonModelField, is: .required, ofType: .embedded(type: ExampleNonModelType.self)),
.field(example.arrayOfStringsField, is: .required, ofType: .embeddedCollection(of: [String].self))
)
}

Expand Down
31 changes: 31 additions & 0 deletions AmplifyTestCommon/Models/NonModel/Category.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// Copyright 2018-2020 Amazon.com,
// Inc. or its affiliates. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

// swiftlint:disable all
import Amplify
import Foundation

public struct Category: Embeddable {
var name: String
var color: Color
}

extension Category {

public enum CodingKeys: CodingKey {
case name
case color
}

public static let keys = CodingKeys.self

public static let schema = defineSchema { embedded in
let category = Category.keys
embedded.fields(.field(category.name, is: .required, ofType: .string),
.field(category.color, is: .required, ofType: .embedded(type: Color.self)))
}
}
36 changes: 36 additions & 0 deletions AmplifyTestCommon/Models/NonModel/Color.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// Copyright 2018-2020 Amazon.com,
// Inc. or its affiliates. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

// swiftlint:disable all
import Amplify
import Foundation

public struct Color: Embeddable {
var name: String
var red: Int
var green: Int
var blue: Int
}

extension Color {
public enum CodingKeys: CodingKey {
case name
case red
case green
case blue
}

public static let keys = CodingKeys.self

public static let schema = defineSchema { embedded in
let color = Color.keys
embedded.fields(.field(color.name, is: .required, ofType: .string),
.field(color.red, is: .required, ofType: .int),
.field(color.green, is: .required, ofType: .int),
.field(color.blue, is: .required, ofType: .int))
}
}
Loading

0 comments on commit 7b5611b

Please sign in to comment.