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

Add support to Android using Kotlin code generation #53

Merged
merged 26 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
97edf7c
Add kotlin templates
husseinala Nov 6, 2023
9fdbffd
Add base kotlin code generator
husseinala Nov 6, 2023
f5bd1d6
Make sources dir and build gradle generation configureable
husseinala Nov 7, 2023
1657059
Add missing Kotlin generator rspecs
husseinala Dec 5, 2023
325d221
Update template.yml to include Kotlin generator specific values
husseinala Dec 5, 2023
1fbe0a9
Generate kotlin unit test when unit tests flag is enabled.
husseinala Dec 19, 2023
7c178fb
Update rakefile to add support for kotlin e2e test support.
husseinala Dec 19, 2023
4b08ac0
Lint and typo fixes.
husseinala Dec 19, 2023
da931ce
Update Kotlin generator rspecs
husseinala Dec 19, 2023
b97c65d
Update Readme to reflect Kotlin support.
husseinala Dec 19, 2023
20fcfea
Fix generated Kotlin code styling.
husseinala Dec 19, 2023
bc72eb1
Switch to using Kotlin jvm toolchain in the generated build.gradle file.
husseinala Dec 19, 2023
438fe37
Add GHA workflow to run kotlin tests in CI.
rogerluan Dec 20, 2023
8ee65bf
Adjust wording and formatting.
rogerluan Dec 20, 2023
deb5ebf
Wrap keyword around quotes.
rogerluan Dec 22, 2023
e70ba49
Adjust style/formatting of generated kotlin files.
rogerluan Dec 22, 2023
62d53c0
Add kotlin-related patterns to `.gitignore`.
rogerluan Dec 22, 2023
06c274c
Lint `.erb` files.
rogerluan Dec 22, 2023
7a762c1
Move Swift-related templates to `/swift` directory.
rogerluan Dec 22, 2023
1fd1324
Clean up `.gitignore`.
rogerluan Dec 22, 2023
bbea84c
Add missing EOF line break.
rogerluan Dec 22, 2023
92296b6
Update Swift and Kotlin previews and add usage code snippet.
rogerluan Dec 22, 2023
b908222
Organize sections.
rogerluan Dec 22, 2023
977fc4f
Fix lack of line break between extensions in Swift.
rogerluan Dec 22, 2023
f79f887
Remove `*.jar` since we use it in kotlin fixtures.
rogerluan Dec 22, 2023
30f6e5a
Merge branch 'main' into kotlin-support
rogerluan Dec 22, 2023
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
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)
```
rogerluan marked this conversation as resolved.
Show resolved Hide resolved

</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
rogerluan marked this conversation as resolved.
Show resolved Hide resolved

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
Loading