Skip to content

Commit

Permalink
#53 Add support to Android using Kotlin code generation
Browse files Browse the repository at this point in the history
  • Loading branch information
rogerluan authored Dec 22, 2023
2 parents 9792f59 + 30f6e5a commit 65965d0
Show file tree
Hide file tree
Showing 38 changed files with 1,176 additions and 24 deletions.
27 changes: 27 additions & 0 deletions .github/workflows/kotlin-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Kotlin Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest # Assuming Kotlin projects can be built on Linux
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '11'
distribution: 'adopt'
- name: Cache Gradle packages
uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
restore-keys: |
${{ runner.os }}-gradle-
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true # Runs 'bundle install' and caches installed gems automatically
- name: Generate Kotlin Code & Run Tests
run: bundle exec rake test_kotlin
12 changes: 11 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,18 @@
/spec/reports/
/tmp/

# rspec failure tracking
# Rspec failure tracking
.rspec_status
coverage
*.gem
**/.DS_Store

# Compiled Kotlin/Java class files
*.class

# IntelliJ IDEA files
.idea/

# Gradle files
.gradle/
build/
101 changes: 92 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,29 +37,73 @@

# Requirements

## Android

Your project must be using the Gradle Build Tool.

## iOS

Your project must be using Swift Package Manager or CocoaPods as dependency manager (or both). No support for Carthage.

<sub>Note: this gem was only tested in macOS environments.</sub>

## Android
## Preview

The Java/Kotlin code generator hasn't been implemented yet. Star and "watch" this project and check back in the future, or help us build it [here](https://github.com/rogerluan/arkana/issues/1).
### Kotlin

<img width="200" alt="image" src="https://user-images.githubusercontent.com/8419048/177968055-6774ad8e-2ef3-45ed-9e26-fbe73a783083.png">
<details><summary>Click here to see the Kotlin preview</summary>
<p>

## Preview
The image below shows how the auto-generated file looks like.

<div align="center">
<img src="docs/kotlin-demo.png">
</div>

Usage using the example code above:

<details><summary>Click here to show an image with the preview!</summary>
```kotlin
import com.arkanakeys.MySecrets

// Designed with testability and DI in mind
println(MySecrets.Global.someBooleanSecret)
println(MySecrets.Global.someIntSecret)
println(MySecrets.Global.mySecretAPIKey)

// Simulating environment selection using a random boolean value
val keys = if (Math.random() < 0.5) MySecrets.Dev else MySecrets.Staging
println(keys.serviceKey)
```

</p>
</details>

### Swift

<details><summary>Click here to see the Swift preview</summary>
<p>

The image below shows how the auto-generated file looks like. At the bottom of it you can see how you'll consume the code generated.
The image below shows how the auto-generated file looks like.

<div align="center">
<img src="docs/swift-demo.png">
</div>

Usage using the example code above:

```swift
import ArkanaKeys

// Designed with testability and DI in mind
print(MySecrets.Global().someBooleanSecret)
print(MySecrets.Global().someIntSecret)
print(MySecrets.Global().mySecretAPIKey)

// This is a demo, so we are using Bool.random() to simulate the environment
let keys: MySecretsEnvironmentProtocol = Bool.random() ? MySecrets.Dev() : MySecrets.Staging()
print(keys.serviceKey)
```

</p>
</details>

Expand Down Expand Up @@ -87,18 +131,21 @@ Once you have create your config file, you can run Arkana:

```sh
Usage: arkana [options]
-c /path/to/your/.arkana.yml, Path to your config file. Defaults to '.arkana.yml'
-c /path/to/your/.arkana.yml, Path to your config file. Defaults to '.arkana.yml'
--config-filepath
-e /path/to/your/.env, Path to your dotenv file. Defaults to '.env' if one exists.
-e /path/to/your/.env, Path to your dotenv file. Defaults to '.env' if one exists.
--dotenv-filepath
-f, --flavor FrostedFlakes Flavors are useful, for instance, when generating secrets for white-label projects. See the README for more information
-i debug,release, Optionally pass the environments that you want Arkana to generate secrets for. Useful if you only want to build a certain environment, e.g. just Debug in local machines, while only building Staging and Release in CI. Separate the keys using a comma, without spaces. When omitted, Arkana generate secrets for all environments.
--include-environments
-l, --lang kotlin Language to produce keys for, e.g. kotlin, swift. Defaults to 'swift'. See the README for more information
```
Note that you have to prepend `bundle exec` before `arkana` if you manage your dependencies via bundler, as recommended.
Arkana only has one command, which parses your config file and env vars, generating all the code needed. Arkana should always be run before attempting to build your project, to make sure the files exist _and_ are up-to-date (according to the current config file). This means you might need to add the Arkana run command in your CI/CD scripts, _fastlane_, Xcode Build Phases, or something similar.
## Importing Arkana into your project
## Importing Arkana into your iOS project
Once the Arkana has been run, its files will be created according to the `package_manager` setting defined in your config file, so update that setting according to your project needs.
Expand Down Expand Up @@ -145,12 +192,48 @@ After adding its dependency, you should be able to `import ArkanaKeys` (or the `
We recommend you to add your ArkanaKeys directory to your `.gitignore` since it's an auto-generated code that will change every time you run Arkana (since its salt gets generated on each run). For more information, see [How does it work?](#how-does-it-work)
## Importing Arkana into your Android project
When importing Arkana into your project, you have two options: generating its files within a new Gradle module created by Arkana, or adding them to an existing module. The choice depends on the settings in your config file, so ensure these are updated to reflect your project's requirements.
### Creating a New Arkana Gradle Module
To generate a new Gradle module containing Arkana files, follow these steps:
1. In your config file, set the `result_path` to the desired name for the new Arkana module.
2. Update your project's `settings.gradle` file to include this newly created Arkana module.
### Adding Arkana to an Existing Gradle Module
If you prefer to add Arkana files to an existing Gradle module, follow these steps:
1. Adjust the `result_path` in your config file to specify the existing Gradle module where you want to include the Arkana files.
2. Change `should_generate_gradle_build_file` to `false`. This prevents the overwriting of your existing module's `build.gradle` file.
### Automating Arkana Execution During Gradle Sync
For automatic execution of Arkana during Gradle sync, modify your `settings.gradle` file by adding the following code:
```kotlin
exec {
commandLine("arkana", "--lang", "kotlin")
}
```
## Options
### `--help`
Will display a list of the available options.
### `--lang`
Usage: `--lang kotlin`
Indicates the language to produce keys for, e.g. kotlin, swift.
Defaults to `swift`.
### `--config-file-path`
Usage: `--config-file-path /path/to/your/.arkana.yml`
Expand Down
10 changes: 10 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ task :test_swift do
FileUtils.rm_rf("tests")
end

desc "Generates Kotlin source code and run its unit tests."
task :test_kotlin do
FileUtils.copy_entry("spec/fixtures/kotlin", "tests")
sh("ARKANA_RUNNING_CI_INTEGRATION_TESTS=true bin/arkana --lang kotlin --config-filepath spec/fixtures/kotlin-tests.yml --dotenv-filepath spec/fixtures/.env.fruitloops --include-environments dev,staging")
Dir.chdir("tests") do
sh("./gradlew test")
end
FileUtils.rm_rf("tests")
end

desc "Sets lib version to the semantic version given, and push it to remote."
task :bump, [:v] do |_t, args|
version = args[:v] || raise("A version is required. Pass it like `rake bump[1.2.3]`")
Expand Down
58 changes: 58 additions & 0 deletions docs/demo-used-to-gen-image.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// DO NOT MODIFY
// Automatically generated by Arkana (https://github.com/rogerluan/arkana)
package com.arkanakeys

object MySecrets {
private val salt = listOf(0x3f, 0xb6, 0x2, 0x3, 0xbf, 0x54, 0xf5, 0x67, 0x95, 0xc, 0x56, 0x47, 0x87, 0x55, 0x60, 0x74, 0x6, 0x77, 0x8b, 0xd6, 0x88, 0x41, 0x99, 0xe2, 0x97, 0x92, 0x9f, 0x68, 0x7d, 0x6c, 0x39, 0x64, 0xca, 0x98, 0xe7, 0x8d, 0xe8, 0x9e, 0x1f, 0xe5, 0xad, 0x45, 0x32, 0xac, 0xc5, 0xe1, 0xf6, 0x4f, 0x67, 0xcc, 0x6a, 0xee, 0x66, 0xac, 0x80, 0xea, 0x78, 0x1b, 0xd6, 0x78, 0x4, 0x97, 0xfa, 0xcc)

internal fun decode(encoded: List<Int>, cipher: List<Int>): String {
val decoded = encoded.mapIndexed { index, item ->
(item xor cipher[(index % cipher.size)]).toByte()
}.toByteArray()
return decoded.toString(Charsets.UTF_8)
}

internal fun decodeInt(encoded: List<Int>, cipher: List<Int>): Int {
return decode(encoded = encoded, cipher = cipher).toInt()
}

internal fun decodeBoolean(encoded: List<Int>, cipher: List<Int>): Boolean {
return decode(encoded = encoded, cipher = cipher).toBoolean()
}

object Global {
val someBooleanSecret: Boolean
get() {
val encoded = listOf(0x4b, 0xc4, 0x77, 0x66)
return decodeBoolean(encoded = encoded, cipher = salt)
}

val someIntSecret: Int
get() {
val encoded = listOf(0xb, 0x84)
return decodeInt(encoded = encoded, cipher = salt)
}

val mySecretAPIKey: String
get() {
val encoded = listOf(0x6, 0x84, 0x30, 0x30, 0x8c, 0x63, 0xc7, 0x57, 0xa6, 0x3a, 0x6e, 0x72, 0xb3, 0x62, 0x57, 0x41, 0x3e, 0x47, 0xbc, 0xef, 0xba, 0x73, 0xaa, 0xd1, 0xa0, 0xa0, 0xaf, 0x5b, 0x4b, 0x54, 0xc, 0x50, 0xfd, 0xaf, 0xd2, 0xb5, 0xd8, 0xa9)
return decode(encoded = encoded, cipher = salt)
}
}

object Dev : MySecretsEnvironment {
override val serviceKey: String
get() {
val encoded = listOf(0x4b, 0xde, 0x6b, 0x70, 0x9f, 0x30, 0x90, 0x11, 0xb5, 0x67, 0x33, 0x3e, 0xa7, 0x3c, 0x13, 0x54, 0x75, 0x12, 0xe8, 0xa4, 0xed, 0x35)
return decode(encoded = encoded, cipher = salt)
}
}

object Staging : MySecretsEnvironment {
override val serviceKey: String
get() {
val encoded = listOf(0x4b, 0xde, 0x6b, 0x70, 0x9f, 0x27, 0x81, 0x6, 0xf2, 0x65, 0x38, 0x20, 0xa7, 0x3e, 0x5, 0xd, 0x26, 0x1e, 0xf8, 0xf6, 0xfb, 0x24, 0xfa, 0x90, 0xf2, 0xe6)
return decode(encoded = encoded, cipher = salt)
}
}
}
86 changes: 86 additions & 0 deletions docs/demo-used-to-gen-image.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// DO NOT MODIFY
// Automatically generated by Arkana (https://github.com/rogerluan/arkana)

import Foundation
import MySecretsInterfaces

public enum MySecrets {
@inline(__always)
fileprivate static let salt: [UInt8] = [
0x47, 0x7c, 0xe3, 0x7f, 0x80, 0xa8, 0x41, 0x6c, 0x2, 0xc3, 0x7f, 0x3f, 0x63, 0xd9, 0xb6, 0x57, 0x3a, 0x33, 0x98, 0xed, 0xfa, 0x71, 0xcb, 0x9a, 0x55, 0x23, 0x52, 0x1c, 0x31, 0xc0, 0x74, 0xd4, 0x7e, 0x6e, 0xf5, 0xca, 0xbc, 0x25, 0x69, 0xcd, 0x9, 0, 0xcb, 0x70, 0xd3, 0x5e, 0xa5, 0x92, 0xe7, 0x6b, 0x2, 0x90, 0x29, 0xc2, 0x44, 0xc8, 0x5a, 0xc2, 0xc0, 0xc7, 0x42, 0x30, 0x40, 0xa3
]

static func decode(encoded: [UInt8], cipher: [UInt8]) -> String {
return String(decoding: encoded.enumerated().map { offset, element in
element ^ cipher[offset % cipher.count]
}, as: UTF8.self)
}

static func decode(encoded: [UInt8], cipher: [UInt8]) -> Bool {
let stringValue: String = Self.decode(encoded: encoded, cipher: cipher)
return Bool(stringValue)!
}

static func decode(encoded: [UInt8], cipher: [UInt8]) -> Int {
let stringValue: String = Self.decode(encoded: encoded, cipher: cipher)
return Int(stringValue)!
}
}

public extension MySecrets {
struct Global: MySecretsGlobalProtocol {
public init() {}

@inline(__always)
public lazy var someBooleanSecret: Bool = {
let encoded: [UInt8] = [
0x33, 0xe, 0x96, 0x1a
]
return MySecrets.decode(encoded: encoded, cipher: MySecrets.salt)
}()

@inline(__always)
public lazy var someIntSecret: Int = {
let encoded: [UInt8] = [
0x73, 0x4e
]
return MySecrets.decode(encoded: encoded, cipher: MySecrets.salt)
}()

@inline(__always)
public lazy var mySecretAPIKey: String = {
let encoded: [UInt8] = [
0x7e, 0x4e, 0xd1, 0x4c, 0xb3, 0x9f, 0x73, 0x5c, 0x31, 0xf5, 0x47, 0xa, 0x57, 0xee, 0x81, 0x62, 0x2, 0x3, 0xaf, 0xd4, 0xc8, 0x43, 0xf8, 0xa9, 0x62, 0x11, 0x62, 0x2f, 0x7, 0xf8, 0x41, 0xe0, 0x49, 0x59, 0xc0, 0xf2, 0x8c, 0x12
]
return MySecrets.decode(encoded: encoded, cipher: MySecrets.salt)
}()
}
}

public extension MySecrets {
struct Dev: MySecretsEnvironmentProtocol {
public init() {}

@inline(__always)
public lazy var serviceKey: String = {
let encoded: [UInt8] = [
0x33, 0x14, 0x8a, 0xc, 0xa0, 0xcc, 0x24, 0x1a, 0x22, 0xa8, 0x1a, 0x46, 0x43, 0xb0, 0xc5, 0x77, 0x49, 0x56, 0xfb, 0x9f, 0x9f, 0x5
]
return MySecrets.decode(encoded: encoded, cipher: MySecrets.salt)
}()
}
}

public extension MySecrets {
struct Staging: MySecretsEnvironmentProtocol {
public init() {}

@inline(__always)
public lazy var serviceKey: String = {
let encoded: [UInt8] = [
0x33, 0x14, 0x8a, 0xc, 0xa0, 0xdb, 0x35, 0xd, 0x65, 0xaa, 0x11, 0x58, 0x43, 0xb2, 0xd3, 0x2e, 0x1a, 0x5a, 0xeb, 0xcd, 0x89, 0x14, 0xa8, 0xe8, 0x30, 0x57
]
return MySecrets.decode(encoded: encoded, cipher: MySecrets.salt)
}()
}
}
Binary file added docs/kotlin-demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/swift-demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 9 additions & 1 deletion lib/arkana.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
require_relative "arkana/models/template_arguments"
require_relative "arkana/salt_generator"
require_relative "arkana/swift_code_generator"
require_relative "arkana/kotlin_code_generator"
require_relative "arkana/version"

# Top-level namespace for Arkana's execution entry point. When ran from CLI, `Arkana.run` is what is invoked.
Expand Down Expand Up @@ -43,7 +44,14 @@ def self.run(arguments)
config: config,
salt: salt,
)
SwiftCodeGenerator.generate(

generator = case config.current_lang.downcase
when "swift" then SwiftCodeGenerator
when "kotlin" then KotlinCodeGenerator
else UI.crash("Unknown output lang selected: #{config.current_lang}")
end

generator.method(:generate).call(
template_arguments: template_arguments,
config: config,
)
Expand Down
1 change: 1 addition & 0 deletions lib/arkana/config_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def self.parse(arguments)
config = Config.new(yaml)
config.include_environments(arguments.include_environments)
config.current_flavor = arguments.flavor
config.current_lang = arguments.lang
config.dotenv_filepath = arguments.dotenv_filepath
UI.warn("Dotenv file was specified but couldn't be found at '#{config.dotenv_filepath}'") if config.dotenv_filepath && !File.exist?(config.dotenv_filepath)
config
Expand Down
Loading

0 comments on commit 65965d0

Please sign in to comment.