orta / cocoapods-keys

A key value store for storing per-developer environment and application keys
MIT License
1.55k stars 92 forks source link

Scoped Keys Instances #114

Open ashfurrow opened 9 years ago

ashfurrow commented 9 years ago

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...

There are obviously questions we'd have to answer about the implementation and such, but I'm curious if this is something worth pursuing?

orta commented 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.

ashfurrow commented 9 years ago

Oh I like this even better.

marcelofabri commented 8 years ago

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 configs 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.

chrisfsampaio commented 8 years ago

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
chrisfsampaio commented 8 years ago

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.

orta commented 8 years ago

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 👍

orta commented 8 years ago

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"
      ]
    }
  }
}
marcelofabri commented 8 years ago

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?

orta commented 8 years ago

Agreed on all points 👍

marcelofabri commented 8 years ago

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.

kmeinh commented 7 years ago

Is this still in Progress? I would be pretty interested in a solution.

marcelofabri commented 7 years ago

@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 👍

rogerluan commented 3 years ago

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

rogerluan commented 2 years ago

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 :)