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

Implement Sync Beta Group Commands #147

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7e9c933
Point SDK source back to AvdLee Master
DechengMa Jun 11, 2020
73ce2cc
update fileContent() -> Data to return FileContent
DechengMa Jun 11, 2020
a9b95c7
Add id to beta group, make group name non-nil
DechengMa Jun 11, 2020
9e564bd
Add delete to processor with some tweaks, create betagroup processor
DechengMa Jun 11, 2020
5226dfc
Remove not found error in ListBetaTesterOperation. make options optional
DechengMa Jun 11, 2020
265826f
Create service function pullBetaGroups
DechengMa Jun 11, 2020
701f95e
Implement basic pull beta groups command
DechengMa Jun 11, 2020
4688745
Create BetaTesterProcessor for rendering CSV
DechengMa Jun 12, 2020
077f789
Wire BetaGroupProcessor up with TesterProcessor
DechengMa Jun 12, 2020
4b74c09
Model.BetaGroup + Hashable, Equatable
DechengMa Jun 12, 2020
94e03c8
Add read() to BetaGroupProcessor
DechengMa Jun 12, 2020
1c50987
Implement UpdateBetaGroupOperation, add update and delete service func
DechengMa Jun 12, 2020
ede1d0f
Create SyncStrategy, SyncResultRenderable, SyncResultRenderer
DechengMa Jun 12, 2020
b7d3cce
Wire up PushBetaGroupCommand with sync funcs and strategies
DechengMa Jun 12, 2020
bc1f122
Reformat files for linting get passed
DechengMa Jun 12, 2020
49e4ecb
Introduce SyncResourceComparator,
DechengMa Jun 14, 2020
aa393dd
Apply new SyncResourceComparator to sync betagroup command
DechengMa Jun 14, 2020
4495c84
Add tests to SyncResourceComparator
DechengMa Jun 14, 2020
1679e6f
PushBetaGroupsCommand syncBetaGroup function code separation
DechengMa Jun 15, 2020
cb7cc95
mark compareIdentity non-optional in ResourceComparable, tweak sync test
DechengMa Jun 15, 2020
0e381c0
Reformat Pull betagroup command desc, remove default command for sync
DechengMa Jul 1, 2020
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct TestFlightBetaGroupCommand: ParsableCommand {
ReadBetaGroupCommand.self,
RemoveTestersFromGroupCommand.self,
AddTestersToGroupCommand.self,
SyncBetaGroupsCommand.self,
],
defaultSubcommand: ListBetaGroupsCommand.self
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2020 Itty Bitty Apps Pty Ltd

import ArgumentParser
import FileSystem

struct PullBetaGroupsCommand: CommonParsableCommand {

static var configuration = CommandConfiguration(
commandName: "pull",
abstract: "Pull down existing beta groups, refreshing local beta group config files"
)

@OptionGroup()
var common: CommonOptions

@Option(
default: "./config/betagroups",
help: "Path to the Folder containing the information about beta groups."
) var outputPath: String

func run() throws {
let service = try makeService()

let betaGroupWithTesters = try service.pullBetaGroups()

try BetaGroupProcessor(path: .folder(path: outputPath))
.write(groupsWithTesters: betaGroupWithTesters)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2020 Itty Bitty Apps Pty Ltd

import ArgumentParser
import FileSystem
import Foundation
import struct Model.BetaGroup

struct PushBetaGroupsCommand: CommonParsableCommand {

static var configuration = CommandConfiguration(
commandName: "push",
abstract: "Push local beta group config files to server, update server beta groups"
)

@OptionGroup()
var common: CommonOptions

@Option(
default: "./config/betagroups",
help: "Path to the Folder containing the information about beta groups. (default: './config/betagroups')"
) var inputPath: String

@Flag(help: "Perform a dry run.")
var dryRun: Bool

func run() throws {
let service = try makeService()

let resourceProcessor = BetaGroupProcessor(path: .folder(path: inputPath))

let serverGroups = try service.pullBetaGroups().map { $0.betaGroup }
let localGroups = try resourceProcessor.read()

let strategies = SyncResourceComparator(
localResources: localGroups,
serverResources: serverGroups
)
.compare()

let renderer = Renderers.SyncResultRenderer<BetaGroup>()

if dryRun {
renderer.render(strategies, isDryRun: true)
} else {
try strategies.forEach { (strategy: SyncStrategy) in
try syncBetaGroup(strategy: strategy, with: service)
renderer.render(strategy, isDryRun: false)
}

let betaGroupWithTesters = try service.pullBetaGroups()

try resourceProcessor.write(groupsWithTesters: betaGroupWithTesters)
}
}

func syncBetaGroup(
strategy: SyncStrategy<BetaGroup>,
with service: AppStoreConnectService
) throws {
switch strategy {
case .create(let group):
_ = try service.createBetaGroup(
appBundleId: group.app.bundleId!,
groupName: group.groupName,
publicLinkEnabled: group.publicLinkEnabled ?? false,
publicLinkLimit: group.publicLinkLimit
)
case .delete(let group):
try service.deleteBetaGroup(with: group.id!)
case .update(let group):
try service.updateBetaGroup(betaGroup: group)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2020 Itty Bitty Apps Pty Ltd

import ArgumentParser

struct SyncBetaGroupsCommand: ParsableCommand {
static var configuration = CommandConfiguration(
commandName: "sync",
abstract: "Sync information about beta groups with provided configuration file.",
subcommands: [
PullBetaGroupsCommand.self,
PushBetaGroupsCommand.self,
]
)
}
2 changes: 1 addition & 1 deletion Sources/AppStoreConnectCLI/Model/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ extension App: TableInfoProvider {

var tableRow: [CustomStringConvertible] {
return [
id,
id ?? "",
bundleId ?? "",
name ?? "",
primaryLocale ?? "",
Expand Down
36 changes: 32 additions & 4 deletions Sources/AppStoreConnectCLI/Model/BetaGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ extension BetaGroup: TableInfoProvider, ResultRenderable {

var tableRow: [CustomStringConvertible] {
[
app.id,
app.id ?? "",
app.bundleId ?? "",
app.name ?? "",
groupName ?? "",
groupName,
isInternal ?? "",
publicLink ?? "",
publicLinkEnabled ?? "",
Expand All @@ -41,13 +41,29 @@ extension BetaGroup: TableInfoProvider, ResultRenderable {
}

extension BetaGroup {
enum Error: LocalizedError {
case invalidName

var errorDescription: String? {
switch self {
case .invalidName:
return "Beta group doesn't have a valid group name."
}
}
}

init(
_ apiApp: AppStoreConnect_Swift_SDK.App,
_ apiBetaGroup: AppStoreConnect_Swift_SDK.BetaGroup
) {
) throws {
guard let groupName = apiBetaGroup.attributes?.name else {
throw Error.invalidName
}

self.init(
app: App(apiApp),
groupName: apiBetaGroup.attributes?.name,
id: apiBetaGroup.id,
groupName: groupName,
isInternal: apiBetaGroup.attributes?.isInternalGroup,
publicLink: apiBetaGroup.attributes?.publicLink,
publicLinkEnabled: apiBetaGroup.attributes?.publicLinkEnabled,
Expand All @@ -57,3 +73,15 @@ extension BetaGroup {
)
}
}

extension BetaGroup: SyncResultRenderable {
var syncResultText: String {
"\(app.bundleId ?? "" )_\(groupName)"
}
}

extension BetaGroup: SyncResourceProcessable {
var compareIdentity: String {
id ?? ""
}
}
39 changes: 39 additions & 0 deletions Sources/AppStoreConnectCLI/Readers and Renderers/Renderers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,42 @@ extension ResultRenderable where Self: TableInfoProvider {
return table.render()
}
}

protocol SyncResultRenderable: Equatable {
var syncResultText: String { get }
}

enum SyncStrategy<T: SyncResultRenderable> {
case delete(T)
case create(T)
case update(T)
}

extension Renderers {

struct SyncResultRenderer<T: SyncResultRenderable> {

func render(_ strategy: [SyncStrategy<T>], isDryRun: Bool) {
strategy.forEach { renderResultText($0, isDryRun) }
}

func render(_ strategy: SyncStrategy<T>, isDryRun: Bool) {
renderResultText(strategy, isDryRun)
}

private func renderResultText(_ strategy: SyncStrategy<T>, _ isDryRun: Bool) {
let resultText: String
switch strategy {
case .create(let input):
resultText = "➕ \(input.syncResultText)"
case .delete(let input):
resultText = "➖ \(input.syncResultText)"
case .update(let input):
resultText = "⬆️ \(input.syncResultText)"
}

print("\(isDryRun ? "" : "✅") \(resultText)")
}
}

}
33 changes: 30 additions & 3 deletions Sources/AppStoreConnectCLI/Services/AppStoreConnectService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ class AppStoreConnectService {
.execute(with: requestor)
.await()

return Model.BetaGroup(app, betaGroup)
return try Model.BetaGroup(app, betaGroup)
}

func createBetaGroup(
Expand All @@ -432,7 +432,7 @@ class AppStoreConnectService {
)

let betaGroupResponse = createBetaGroupOperation.execute(with: requestor)
return try betaGroupResponse.map(Model.BetaGroup.init).await()
return try betaGroupResponse.tryMap(Model.BetaGroup.init).await()
}

func deleteBetaGroup(appBundleId: String, betaGroupName: String) throws {
Expand Down Expand Up @@ -507,7 +507,34 @@ class AppStoreConnectService {
let modifyBetaGroupOperation = ModifyBetaGroupOperation(options: modifyBetaGroupOptions)
let modifiedBetaGroup = try modifyBetaGroupOperation.execute(with: requestor).await()

return Model.BetaGroup(app, modifiedBetaGroup)
return try Model.BetaGroup(app, modifiedBetaGroup)
}

func pullBetaGroups() throws -> [(betaGroup: Model.BetaGroup, testers: [Model.BetaTester])] {
let groupOutputs = try ListBetaGroupsOperation(options: .init(appIds: [], names: [], sort: nil)).execute(with: requestor).await()

return try groupOutputs.map {
let testers = try ListBetaTestersOperation(
options: .init(groupIds: [$0.betaGroup.id])
)
.execute(with: requestor)
.await()
.map(BetaTester.init)

return (try BetaGroup($0.app, $0.betaGroup), testers)
}
}

func updateBetaGroup(betaGroup: Model.BetaGroup) throws {
_ = try UpdateBetaGroupOperation(options: .init(betaGroup: betaGroup))
.execute(with: requestor)
.await()
}

func deleteBetaGroup(with id: String) throws {
try DeleteBetaGroupOperation(options: .init(betaGroupId: id))
.execute(with: requestor)
.await()
}

func readBuild(bundleId: String, buildNumber: String, preReleaseVersion: String) throws -> Model.Build {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,15 @@ import Foundation
struct ListBetaTestersOperation: APIOperation {

struct Options {
let email: String?
let firstName: String?
let lastName: String?
let inviteType: BetaInviteType?
let appIds: [String]?
let groupIds: [String]?
let sort: ListBetaTesters.Sort?
let limit: Int?
let relatedResourcesLimit: Int?
}

enum Error: LocalizedError {
case notFound

var errorDescription: String? {
switch self {
case .notFound:
return "Beta testers with provided filters not found."
}
}
var email: String?
var firstName: String?
var lastName: String?
var inviteType: BetaInviteType?
var appIds: [String]?
var groupIds: [String]?
var sort: ListBetaTesters.Sort?
var limit: Int?
var relatedResourcesLimit: Int?
}

private let options: Options
Expand Down Expand Up @@ -107,13 +96,9 @@ struct ListBetaTestersOperation: APIOperation {
next: $0
)
}
.tryMap { (responses: [BetaTestersResponse]) throws -> Output in
try responses.flatMap { (response: BetaTestersResponse) -> Output in
guard !response.data.isEmpty else {
throw Error.notFound
}

return response.data.map {
.map {
$0.flatMap { (response: BetaTestersResponse) -> Output in
response.data.map {
.init(betaTester: $0, includes: response.included)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2020 Itty Bitty Apps Pty Ltd

import AppStoreConnect_Swift_SDK
import Combine
import Foundation
import struct Model.BetaGroup

struct UpdateBetaGroupOperation: APIOperation {

struct Options {
let betaGroup: BetaGroup
}

private let options: Options

init(options: Options) {
self.options = options
}

func execute(with requestor: EndpointRequestor) throws -> AnyPublisher<BetaGroupResponse, Error> {
let betaGroup = options.betaGroup

let endpoint = APIEndpoint.modify(
betaGroupWithId: betaGroup.id!,
name: betaGroup.groupName,
publicLinkEnabled: betaGroup.publicLinkEnabled,
publicLinkLimit: betaGroup.publicLinkLimit,
publicLinkLimitEnabled: betaGroup.publicLinkLimitEnabled
)

return requestor.request(endpoint).eraseToAnyPublisher()
}

}
Loading