Open ashfurrow opened 9 years ago
OK, let's play with a simple example (Emergence's)
Current:
plugin 'cocoapods-keys', {
:project => "Emergence",
:targets => "Artsy Shows",
:keys => [
"ArtsyAPIClientSecret",
"ArtsyAPIClientKey",
"SegmentDevWriteKey",
"SegmentProductionWriteKey"
]
}
Which has 1 "all configs" and 1 "prod/dev".
plugin 'cocoapods-keys', {
:project => "Emergence",
:targets => "Artsy Shows",
:keys => [
"ArtsyAPIClientSecret",
"ArtsyAPIClientKey",
],
:configs => ["production", "dev"],
:config_keys => ["SegmentWriteKey"]
}
Wherein for the keys
it would offer a duplicate API for staging/prod with ArtsyAPIClientKey
, ArtsyAPIClientSecret
, but on SegmentWriteKey
there would be a difference between the two.
Oh I like this even better.
So, I'm starting a new project soon and I'd love to have this from the start.
However, I'd like to propose another DSL:
plugin 'cocoapods-keys', {
:project => "Emergence",
:targets => "Artsy Shows",
:keys => [
"ArtsyAPIClientSecret",
"ArtsyAPIClientKey",
],
:scoped => {
:configs => ["UK", "US", "BR"],
:keys => [
"InstabugToken"
],
:scoped => {
:configs => ["Production", "Staging"],
:keys => [
"ImportantAPIKey"
]
}
}
}
For my use case, it'd be important to have nested configs. In this example, ArtsyAPIClientSecret
and ArtsyAPIClientKey
are always the same, InstabugToken
can have different values on UK
, US
and BR
configs and ImportantAPIKey
would be scoped on UK.Production
, UK.Staging
, US.Production
, etc.
My idea is that if there're any scoped
, the default initializer would be marked as unavailable, and a class method should be used to create a Keys
instance. The number of methods would be the product of config
s of all levels (in this case, 3*2 = 6).
The interface would be something like this:
@interface ScopedKeys : NSObject
- (instancetype)init NS_UNAVAILABLE;
+ (ScopedKeys *)UKProduction;
+ (ScopedKeys *)UKStaging;
+ (ScopedKeys *)USProduction;
+ (ScopedKeys *)USStaging;
+ (ScopedKeys *)BRProduction;
+ (ScopedKeys *)BRStaging;
- (NSString *)artsyAPIClientSecret;
- (NSString *)artsyAPIClientKey;
- (NSString *)instabugToken;
- (NSString *)importantAPIKey;
@end
In terms of implementation, I was thinking about creating a private subclass for each class method and returning them (we'd have a class cluster).
I know this adds (a lot) more complexity, so I'd like to hear other inputs about this before starting working on a PR.
If we could use Swift here, I'd change the DSL such that one could name the scopes
and then we could have the generated code like this:
struct Country<T> {
let UK: T
let BR: T
}
struct Environment<T> {
let Stage: T
let Production: T
}
struct Keys {
static let artsySecret: String = "super secret"
static let artsyKey: String = "super key"
static let instaBug: Country<String> = Country(UK: "tea time!", BR: "caipirinha!")
static let importantKey: Country<Environment<String>> =
Country(
UK: Environment(Stage: "playground", Production: "that's quite serious"),
BR: Environment(Stage: "só de teste", Production: "de verdade")
)
}
And then we could use them like this:
Keys.artsySecret
Keys.artsyKey
Keys.instaBug.BR
Keys.instaBug.UK
Keys.importantKey.BR.Production
Keys.importantKey.BR.Stage
Keys.importantKey.UK.Production
Keys.importantKey.UK.Stage
Or even simpler and better to use:
enum Country { case BR, UK }
enum Environment { case dev, prod }
struct Keys {
init(countryScope: Country, environmentScope: Environment) {
self.keyBar = "keyBar given the scope: \(countryScope) - \(environmentScope)"
self.keyFoo = "keyFoo given the scope: \(countryScope) - \(environmentScope)"
}
let keyFoo: String
let keyBar: String
}
// --- From the client side ---
// This would be somewhere in the app only once
extension Keys {
static var current: Keys {
#if DEBUG
return Keys(countryScope: .BR, environmentScope: .dev)
#else
return Keys(countryScope: .BR, environmentScope: .prod)
#endif
}
}
// And could be used like this in along the project
Keys.current.keyBar
Keys.current.keyFoo
This way, clients can write the branching code once and use it whenever it's necessary without worrying about the scopes, since they had been already injected.
We had this debate originally, but it's done in Objective-C so that you get some security for your keys ( e.g. they won't show up in strings
, or be easily found without attaching a debugger at runtime and pulling it out of a live app )
Moving over to Swift would need to keep that security, which can be done, but would need thinking about 👍
WRT the options, I think that's a great approach - though maybe this might feel better?
plugin 'cocoapods-keys', {
:project => "Emergence",
:targets => "Artsy Shows",
:keys => [
"ArtsyAPIClientSecret",
"ArtsyAPIClientKey",
],
:config => {
:values => ["UK", "US", "BR"],
:keys => [
"InstabugToken"
],
:config => {
:value => ["Production", "Staging"],
:keys => [
"ImportantAPIKey"
]
}
}
}
I think it's better, but I'd change values
to something like options
to make it more obvious that they are exclusive values.
About Swift, I think it's a larger task and it can be done later if that's something that makes sense as there're lots of implications on doing it now (embedding Swift runtime in the clients, supporting multiple Swift versions, making sure the keys are "safe", etc).
However, I think we can borrow @chrisfsampaio's idea to define enums and only create an init with the desired options (instead of several class methods). We can even skip the whole class cluster idea and only change resolveInstanceMethod
to use the enums when choosing an implementation. Does that make sense?
Agreed on all points 👍
I had some time today and yesterday and tried to start this. I found myself taking much time to understand the plugin/trying to not break it, so I decided to do what I thought it was easier: create a plugin that uses cocoapods-keys so I could focus on this problem instead of all complexity together.
Here's the result: https://github.com/marcelofabri/cocoapods-scoped-keys. It's missing lots of tests, but I promise it worked on a project 😅
I also took the naive path sometimes, such as using a separator to store the keys in cocoapods-keys (ImportantAPIKey__Production__UK
for example). We could do better here (because of UX in the commands).
Could someone take a look at that and share their thought? My idea was to create this as an isolated first step and then port these changes to cocoapods-keys, but I need some help on what goes where, etc.
Is this still in Progress? I would be pretty interested in a solution.
@konDeichmann Not as far as I know. You can take a look at the repo that I've shared though: it's a proof of concept that works (on simple setups at least, but I've never used it in production).
I'm sure a PR adding this feature would be more than welcome, so feel free to implement it 👍
I'd be very interested in this feature as well 👀 Managing all these configuration matrices can be quite a lot of work, specially as the number of configs grow I wonder how you ended up implementing this in your project back in 2016 @marcelofabri ? The one with multiple locales and environments
Not meaning to hijack this convo and place some advertisement... but I've created a new tool to achieve this specific goal of scoped keys (and other goals as well). You can check it out here 😊 https://github.com/rogerluan/arkana
This might interest you @ashfurrow @EmilKaczmarski :)
I see code like this a lot – switching to staging or production seems fairly common. What if we built awareness of this pattern into the plugin?
What about something like...
Keys
:Keys.Production
orKeys.Staging
, and each returns aKeys
instance with their appropriate keys.Keys
would be something to use DI with, too.There are obviously questions we'd have to answer about the implementation and such, but I'm curious if this is something worth pursuing?