Bloombox/Swift

View on GitHub
Sources/Client/PlatformClient.swift

Summary

Maintainability
A
0 mins
Test Coverage
/**
* Copyright 2019, Momentum Ideas, Co. All rights reserved.
* Source and object computer code contained herein is the private intellectual
* property of Momentum Ideas Co., a Delaware Corporation. Use of this
* code in source form requires permission in writing before use or the
* assembly, distribution, or publishing of derivative works, for commercial
* purposes or any other purpose, from a duly authorized officer of Momentum
* Ideas Co.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/

import Foundation
import SwiftGRPC
import OpenCannabis


// Types & Enums

/// Ping callback specification. Accepts a gRPC call result, and a platform service status, which indicates the status
/// of the server-side systems that responded to the ping. Two parameters are given and no return value is expected.
///
/// - Parameters:
///    - `CallResult: gRPC call result, carrying the status of the call.
///    - `PlatformServiceStatus?`: If the call succeeded, the self-reported status of the responder server systems.
public typealias PlatformPingCallback = (CallResult, PlatformServiceStatus?) -> ()

/// Health check callback specification. Accepts a simple gRPC call result, and no response, because health checks
/// simply provide an empty success as a response in the case of an all-clear. Just one parameter is given and no return
/// value is expected.
///
/// - Parameters:
///    - `CallResult`: gRPC call result, carrying the status of the call.
public typealias HealthcheckCallback = (CallResult) -> ()

/// Domain resolution callback specification. Accepts a gRPC call result, and a domain resolution response, if info
/// could be resolved for the given partner and location account codes. Two parameters are given and no return value is
/// expected.
///
/// - Parameters:
///    - `CallResult`: gRPC call result, carrying the status of the call.
///    - `ResolveDomains.Response?`: Domain resolution response, if information could be resolved.
public typealias ResolveCallback = (CallResult, ResolveDomains.Response?) -> ()

/// Domain configuration callback specification. Accepts a gRPC call result, and a domain info manifest, if domain info
/// could be resolved for the given partner and location account codes. Two parameters are given and no return value is
/// expected.
///
/// - Parameters:
///    - `CallResult`: gRPC call result, carrying the status of the call.
///    - `DomainInfo.Response?`: Domain info manifest for the given partner/location info, if available.
public typealias DomainInfoCallback = (CallResult, DomainInfo.Response?) -> ()

/// Brand information callback specification. Accepts a gRPC call result, and a set of branding info for a given partner
/// and location account code set. Two parameters are given and no return value is expected.
///
/// - Parameters:
///    - `CallResult`: gRPC call result, carrying the status of the call.
///    - `BrandInfo.Response?`: Brand information manifest for the given partner/location info, if available.
public typealias BrandCallback = (CallResult, BrandInfo.Response?) -> ()


/// Enumerates code-level errors in the platform client. Client errors occur before a given request is transmitted to
/// remote services.
public enum PlatformClientError: Error {
  /// The provided API key was found to be invalid.
  case invalidApiKey

  /// The provided partner account code was found to be invalid, or none could be resolved.
  case invalidPartnerCode

  /// The provided location account code was found to be invalid, or none could be resolved.
  case invalidLocationCode

  /// The engine failed to base64 encode a given request or request value.
  case cannotBase64Encode
}


/// Provides functionality for the Platform API, which supports general configuration and information support for
/// cannabis retail partner locations. Domains are resolved through the Platform Service to their respective partner and
/// location account codes, or domain info can be resolved for a given partner/location pair. Other utility methods are
/// supported through the Platform Service including healthchecks, pings, and others.
public final class PlatformClient: RemoteService {
  /// API name. Always 'platform'.
  let name = "platform"

  /// Active API version. Currently 'v1'.
  let version = "v1"

  // MARK: Internals

  /// Client-wide settings.
  internal let settings: Bloombox.Settings

  /// Platform service.
  internal var svc: PlatformService?

  /// Library-internal initializer.
  ///
  /// - Parameter settings: Client-wide settings to apply.
  internal init(settings: Bloombox.Settings) {
    self.settings = settings
  }

  /// Platform service. Retrieve an implementation of the platform service, capable of resolving and providing basic,
  /// system and account configuration info.
  ///
  /// - Parameter apiKey: API Key to use.
  /// - Returns: Prepared Platform API service class.
  /// - Throws: `POSClientError` if the API key is not able to be resolved.
  internal func service(_ apiKey: APIKey) throws -> PlatformService {
    if let s = self.svc {
      return s
    }
    let svc = RPCServiceFactory<PlatformService>.factory(
      forService: Transport.config.services.platform,
      withSettings: self.settings)

    try svc.metadata.add(key: "x-api-key", value: apiKey)
    self.svc = svc
    return svc
  }

  /// Resolve partner and location context, throwing an error if it cannot be figured out. Partner and location context
  /// is required for all partner calls, along with an API key.
  ///
  /// - Parameter partner: Partner account code to use. If unspecified, library defaults will be used.
  /// - Parameter location: Location account code to use. If unspecified, library defaults will be used.
  /// - Parameter apiKey: API key to identify ourselves with. If unspecified, library defaults will be used.
  /// - Returns: Tuple of `(partner, location, apiKey)`.
  /// - Throws: `MenuClientError` codes if details cannot be resolved.
  private func resolveContext(_ partner: PartnerCode? = nil,
                              _ location: LocationCode? = nil,
                              _ apiKey: APIKey? = nil) throws -> (partner: PartnerCode,
                                                                  location: LocationCode,
                                                                  apiKey: APIKey) {
    let partnerCode: PartnerCode? = partner ?? settings.partner
    let locationCode: LocationCode? = location ?? settings.location
    let apiKey: APIKey? = apiKey ?? settings.apiKey

    guard apiKey != nil else {
      throw PlatformClientError.invalidApiKey
    }

    // validate partner and location codes
    guard partnerCode != nil, locationCode != nil else {
      // throw error: we require a partner or location code from somewhere
      if partnerCode == nil {
        throw PlatformClientError.invalidPartnerCode
      }
      throw PlatformClientError.invalidLocationCode
    }
    return (partner: partnerCode!, location: locationCode!, apiKey: apiKey!)
  }

  // MARK: - Public API -

  // MARK: Method: Healthcheck

  ///
  ///
  @discardableResult
  public func healthcheck() throws -> Empty {
    return try self.service(self.resolveContext().apiKey).health(Empty())
  }

  ///
  ///
  @discardableResult
  public func healthcheck(_ callback: @escaping HealthcheckCallback) throws -> HealthcheckCall {
    return try self.service(self.resolveContext().apiKey).health(Empty()) { (_, result) in
      callback(result)
    }
  }

  // MARK: Method: Resolve

  ///
  ///
  public func resolve(encodedDomain domain: String,
                      forAPIKey apiKey: APIKey? = nil) throws -> ResolveDomains.Response {
    return try self.service(self.resolveContext(apiKey).apiKey)
      .resolve(ResolveDomains.Request.with { req in
        req.origin = domain
      })
  }

  ///
  ///
  @discardableResult
  public func resolve(encodedDomain domain: String,
                      forAPIKey apiKey: APIKey? = nil,
                      _ callback: @escaping ResolveCallback) throws -> ResolveDomainsCall {
    return try self.service(self.resolveContext(apiKey).apiKey)
      .resolve(ResolveDomains.Request.with { req in
        req.origin = domain
      }) { resp, result in
        // async copy
        callback(result, resp)
    }
  }

  ///
  ///
  public func resolve(domain: String) throws -> ResolveDomains.Response {
    if let encoded = domain.data(using: .utf8)?
                           .base64EncodedString()
                           .replacingOccurrences(of: "=", with: "") {
      // encoded domain
      return try self.resolve(encodedDomain: encoded)
    }
    throw PlatformClientError.cannotBase64Encode
  }

  ///
  ///
  @discardableResult
  public func resolve(domain: String, _ callback: @escaping ResolveCallback) throws -> ResolveDomainsCall {
    if let encoded = domain.data(using: .utf8)?
                           .base64EncodedString()
                           .replacingOccurrences(of: "=", with: "") {
      return try self.resolve(encodedDomain: encoded, callback)
    }
    throw PlatformClientError.cannotBase64Encode
  }

  // MARK: Method: Domain Info

  ///
  ///
  public func domains(forPartner partner: PartnerCode? = nil,
                      withLocation location: LocationCode? = nil,
                      andAPIKey apiKey: APIKey? = nil) throws -> DomainInfo.Response {
    let ctx = try self.resolveContext(partner, location, apiKey)

    return try self.service(ctx.apiKey).domains(DomainInfo.Request.with { req in
        req.partnerID = ctx.partner
        req.locationID = ctx.location
      })
  }

  ///
  ///
  @discardableResult
  public func domains(forPartner partner: PartnerCode? = nil,
                      withLocation location: LocationCode? = nil,
                      andAPIKey apiKey: APIKey? = nil,
                      _ callback: @escaping DomainInfoCallback) throws -> DomainInfoCall {
    let ctx = try self.resolveContext(partner, location, apiKey)

    return try self.service(ctx.apiKey).domains(DomainInfo.Request.with { req in
      req.partnerID = ctx.partner
      req.locationID = ctx.location
    }) { resp, result in
      // handle callback
      callback(result, resp)
    }
  }

  // MARK: Method: Brand Info

  ///
  ///
  public func brand(forPartner partner: PartnerCode? = nil,
                    withLocation location: LocationCode? = nil,
                    andAPIKey apiKey: APIKey? = nil) throws -> BrandInfo.Response {
    let ctx = try self.resolveContext(partner, location, apiKey)

    return try self.service(ctx.apiKey).brand(BrandInfo.Request.with { req in
      req.partner = ctx.partner
      req.location = ctx.location
    })
  }

  ///
  ///
  @discardableResult
  public func brand(forPartner partner: PartnerCode? = nil,
                    withLocation location: LocationCode? = nil,
                    andAPIKey apiKey: APIKey? = nil,
                    _ callback: @escaping BrandCallback) throws -> BrandInfoCall {
    let ctx = try self.resolveContext(partner, location, apiKey)

    return try self.service(ctx.apiKey).brand(BrandInfo.Request.with { req in
      req.partner = ctx.partner
      req.location = ctx.location
    }) { resp, result in
      // handle callback
      callback(result, resp)
    }
  }

}