Skip to content
Closed
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
3 changes: 3 additions & 0 deletions .swiftpm/xcode/xcshareddata/xcschemes/AWSAPIPlugin.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
ReferencedContainer = "container:">
</BuildableReference>
<SkippedTests>
<Test
Identifier = "AWSRESTOperationTests/testGetReturnsOperation()">
</Test>
<Test
Identifier = "AWSRESTOperationTests/testRESTOperationConstructURLFailure()">
</Test>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ extension APICategory: Resettable {
log.verbose("Resetting ModelRegistry and ModelListDecoderRegistry")
ModelRegistry.reset()
ModelListDecoderRegistry.reset()
ModelProviderRegistry.reset()
log.verbose("Resetting ModelRegistry and ModelListDecoderRegistry: finished")

isConfigured = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ extension DataStoreCategory: Resettable {
log.verbose("Resetting ModelRegistry and ModelListDecoderRegistry")
ModelRegistry.reset()
ModelListDecoderRegistry.reset()
ModelProviderRegistry.reset()
log.verbose("Resetting ModelRegistry and ModelListDecoderRegistry: finished")

isConfigured = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ public protocol ModelListDecoder {
static func makeListProvider<ModelType: Model>(
modelType: ModelType.Type, decoder: Decoder) throws -> AnyModelListProvider<ModelType>
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
//

import Foundation
import Combine

/// Empty protocol used as a marker to detect when the type is a `List`
///
Expand All @@ -23,7 +22,7 @@ public protocol ModelListMarker { }
/// application making any change to these `public` types should be backward compatible, otherwise it will be a breaking
/// change.
public enum ModelListProviderState<Element: Model> {
case notLoaded
case notLoaded(associatedId: String, associatedField: String)
case loaded([Element])
}

Expand Down Expand Up @@ -94,3 +93,12 @@ public extension ModelListProvider {
AnyModelListProvider(provider: self)
}
}

// MARK - AnyModelProvider


public extension ModelProvider {
func eraseToAnyModelProvider() -> AnyModelProvider<Element> {
AnyModelProvider(provider: self)
}
}
46 changes: 46 additions & 0 deletions Amplify/Categories/DataStore/Model/Internal/ModelProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

public protocol LazyModelMarker {
associatedtype Element: Model

var element: Element? { get }
}

public struct AnyModelProvider<Element: Model>: ModelProvider {

private let loadAsync: () async throws -> Element?
private let getStateClosure: () -> ModelProviderState<Element>

public init<Provider: ModelProvider>(provider: Provider) where Provider.Element == Self.Element {
self.loadAsync = provider.load
self.getStateClosure = provider.getState
}
public func load() async throws -> Element? {
try await loadAsync()
}

public func getState() -> ModelProviderState<Element> {
getStateClosure()
}
}

public protocol ModelProvider {
associatedtype Element: Model

func load() async throws -> Element?

func getState() -> ModelProviderState<Element>

}

public enum ModelProviderState<Element: Model> {
case notLoaded(identifiers: [String: String])
case loaded(Element?)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

// MARK: ModelProviderRegistry

public struct ModelProviderRegistry {
public static var decoders = AtomicValue(initialValue: [ModelProviderDecoder.Type]())

/// Register a decoder during plugin configuration time, to allow runtime retrievals of list providers.
public static func registerDecoder(_ decoder: ModelProviderDecoder.Type) {
decoders.append(decoder)
}
}

extension ModelProviderRegistry {
static func reset() {
decoders.set([ModelProviderDecoder.Type]())
}
}

public protocol ModelProviderDecoder {
static func shouldDecode<ModelType: Model>(modelType: ModelType.Type, decoder: Decoder) -> Bool
static func makeModelProvider<ModelType: Model>(
modelType: ModelType.Type, decoder: Decoder) throws -> AnyModelProvider<ModelType>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

// MARK: - DefaultModelProvider

public struct DefaultModelProvider<Element: Model>: ModelProvider {

let element: Element?
public init(element: Element? = nil) {
self.element = element
}

public func load() async throws -> Element? {
return element
}

public func getState() -> ModelProviderState<Element> {
return .loaded(element)
}

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

import Foundation
import Combine

public class LazyModel<Element: Model>: Codable, LazyModelMarker {

/// Represents the data state of the `LazyModel`.
enum LoadedState {
case notLoaded(identifiers: [String: String])
case loaded(Element?)
}
var loadedState: LoadedState

/// The provider for fulfilling list behaviors
let modelProvider: AnyModelProvider<Element>

public internal(set) var element: Element? {
get {
switch loadedState {
case .notLoaded:
return nil
case .loaded(let element):
return element
}
}
set {
switch loadedState {
case .loaded:
Amplify.log.error("""
There is an attempt to set an already lazy model. The existing data will not be overwritten
""")
return
case .notLoaded:
loadedState = .loaded(newValue)
}
}
}

public init(modelProvider: AnyModelProvider<Element>) {
self.modelProvider = modelProvider
switch self.modelProvider.getState() {
case .loaded(let element):
self.loadedState = .loaded(element)
case .notLoaded(let identifiers):
self.loadedState = .notLoaded(identifiers: identifiers)
}
}

public convenience init(element: Element? = nil) {
let modelProvider = DefaultModelProvider(element: element).eraseToAnyModelProvider()
self.init(modelProvider: modelProvider)
}

required convenience public init(from decoder: Decoder) throws {
for modelDecoder in ModelProviderRegistry.decoders.get() {
if modelDecoder.shouldDecode(modelType: Element.self, decoder: decoder) {
let modelProvider = try modelDecoder.makeModelProvider(modelType: Element.self, decoder: decoder)
self.init(modelProvider: modelProvider)
return
}
}
let json = try JSONValue(from: decoder)
if case .object = json {
let element = try Element(from: decoder)
self.init(element: element)
} else {
self.init()
}
}


public func encode(to encoder: Encoder) throws {
switch loadedState {
case .notLoaded(let identifiers):
var container = encoder.singleValueContainer()
try container.encode(identifiers)
case .loaded(let element):
try element.encode(to: encoder)
}
}

// MARK: - APIs

public func get() async throws -> Element? {
switch loadedState {
case .notLoaded:
let element = try await modelProvider.load()
self.loadedState = .loaded(element)
return element
case .loaded(let element):
return element
}
}

public func require() async throws -> Element {
switch loadedState {
case .notLoaded:
guard let element = try await modelProvider.load() else {
throw CoreError.operation("Expected required element not found", "", nil)
}
self.loadedState = .loaded(element)
return element
case .loaded(let element):
guard let element = element else {
throw CoreError.operation("Expected required element not found", "", nil)
}
return element
}
}
}
5 changes: 5 additions & 0 deletions Amplify/Core/Error/CoreError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public enum CoreError {

/// A related operation performed on `List` resulted in an error.
case listOperation(ErrorDescription, RecoverySuggestion, Error? = nil)

case operation(ErrorDescription, RecoverySuggestion, Error? = nil)

/// A client side validation error occured.
case clientValidation(ErrorDescription, RecoverySuggestion, Error? = nil)
Expand All @@ -19,6 +21,7 @@ extension CoreError: AmplifyError {
public var errorDescription: ErrorDescription {
switch self {
case .listOperation(let errorDescription, _, _),
.operation(let errorDescription, _, _),
.clientValidation(let errorDescription, _, _):
return errorDescription
}
Expand All @@ -27,6 +30,7 @@ extension CoreError: AmplifyError {
public var recoverySuggestion: RecoverySuggestion {
switch self {
case .listOperation(_, let recoverySuggestion, _),
.operation(_, let recoverySuggestion, _),
.clientValidation(_, let recoverySuggestion, _):
return recoverySuggestion
}
Expand All @@ -35,6 +39,7 @@ extension CoreError: AmplifyError {
public var underlyingError: Error? {
switch self {
case .listOperation(_, _, let underlyingError),
.operation(_, _, let underlyingError),
.clientValidation(_, _, let underlyingError):
return underlyingError
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ final public class AWSAPIPlugin: NSObject, APICategoryPlugin, APICategoryGraphQL

modelRegistration?.registerModels(registry: ModelRegistry.self)
ModelListDecoderRegistry.registerDecoder(AppSyncListDecoder.self)
ModelProviderRegistry.registerDecoder(AppSyncModelDecoder.self)
let sessionFactory = sessionFactory
?? URLSessionFactory.makeDefault()
self.session = sessionFactory.makeSession(withDelegate: self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,41 @@ public struct AppSyncListDecoder: ModelListDecoder {
return nil
}
}


public struct AppSyncModelDecoder: ModelProviderDecoder {
public static func shouldDecode<ModelType: Model>(modelType: ModelType.Type, decoder: Decoder) -> Bool {
if (try? AppSyncModelIdentifierMetadata(from: decoder)) != nil {
return true
}

if (try? ModelType(from: decoder)) != nil {
return true
}

return false
}

public static func makeModelProvider<ModelType: Model>(modelType: ModelType.Type,
decoder: Decoder) throws -> AnyModelProvider<ModelType> {
if let appSyncModelProvider = try makeAppSyncModelProvider(modelType: modelType, decoder: decoder) {
return appSyncModelProvider.eraseToAnyModelProvider()
}

return DefaultModelProvider<ModelType>().eraseToAnyModelProvider()
}

static func makeAppSyncModelProvider<ModelType: Model>(modelType: ModelType.Type,
decoder: Decoder) throws -> AppSyncModelProvider<ModelType>? {
if let model = try? ModelType.init(from: decoder) {
return AppSyncModelProvider(model: model)
} else if let metadata = try? AppSyncModelIdentifierMetadata.init(from: decoder) {
return AppSyncModelProvider<ModelType>(metadata: metadata)
}
let json = try JSONValue(from: decoder)
let message = "AppSyncModelProvider could not be created from \(String(describing: json))"
Amplify.DataStore.log.error(message)
assertionFailure(message)
return nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,20 @@ public struct AppSyncListPayload: Codable {
return nil
}
}

// MARK - AppSyncModelPayload


public struct AppSyncModelPayload: Codable {

let graphQLData: JSONValue
let apiName: String?

public init(graphQLData: JSONValue,
apiName: String?) {
self.apiName = apiName
self.graphQLData = graphQLData
}


}
Loading