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 14 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
97edf7c
Add kotlin templates
Nov 6, 2023
9fdbffd
Add base kotlin code generator
Nov 6, 2023
f5bd1d6
Make sources dir and build gradle generation configureable
Nov 7, 2023
1657059
Add missing Kotlin generator rspecs
Dec 5, 2023
325d221
Update template.yml to include Kotlin generator specific values
Dec 5, 2023
1fbe0a9
Generate kotlin unit test when unit tests flag is enabled.
Dec 19, 2023
7c178fb
Update rakefile to add support for kotlin e2e test support.
Dec 19, 2023
4b08ac0
Lint and typo fixes.
Dec 19, 2023
da931ce
Update Kotlin generator rspecs
Dec 19, 2023
b97c65d
Update Readme to reflect Kotlin support.
Dec 19, 2023
20fcfea
Fix generated Kotlin code styling.
Dec 19, 2023
bc72eb1
Switch to using Kotlin jvm toolchain in the generated build.gradle file.
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
49 changes: 43 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ Your project must be using Swift Package Manager or CocoaPods as dependency mana

## Android

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

<img width="200" alt="image" src="https://user-images.githubusercontent.com/8419048/177968055-6774ad8e-2ef3-45ed-9e26-fbe73a783083.png">
Your project must be using the Gradle Build Tool.

## Preview

Expand Down Expand Up @@ -87,18 +85,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 +146,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
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
22 changes: 22 additions & 0 deletions lib/arkana/helpers/kotlin_template_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

# Utilities to reduce the amount of boilerplate code in `.kt.erb` template files.
module KotlinTemplateHelper
def self.kotlin_type(type)
case type
when :string then "String"
when :boolean then "Boolean"
when :integer then "Int"
else raise "Unknown variable type '#{type}' received."
end
end

def self.kotlin_decode_function(type)
case type
when :string then "decode"
when :boolean then "decodeBoolean"
when :integer then "decodeInt"
else raise "Unknown variable type '#{type}' received."
end
end
end
56 changes: 56 additions & 0 deletions lib/arkana/kotlin_code_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

require "erb"
require "fileutils"
require_relative "helpers/string"

# Responsible for generating Kotlin source and test files.
husseinala marked this conversation as resolved.
Show resolved Hide resolved
module KotlinCodeGenerator
# Generates Kotlin code and test files for the given template arguments.
def self.generate(template_arguments:, config:)
kotlin_module_dir = config.result_path
kotlin_sources_dir = File.join(kotlin_module_dir, "src", "main", config.kotlin_sources_path, config.kotlin_package_name.split("."))
kotlin_tests_dir = File.join(kotlin_module_dir, "src", "test", config.kotlin_sources_path, config.kotlin_package_name.split("."))

if config.should_generate_gradle_build_file
set_up_kotlin_module(kotlin_module_dir, template_arguments)
end

set_up_kotlin_interfaces(kotlin_sources_dir, template_arguments, config)
set_up_kotlin_classes(kotlin_sources_dir, kotlin_tests_dir, template_arguments, config)
end

def self.set_up_kotlin_module(path, template_arguments)
dirname = File.dirname(__FILE__)
sources_dir = path
readme_template = File.read("#{dirname}/templates/readme.erb")
source_template = File.read("#{dirname}/templates/kotlin/build.gradle.kts.erb")
FileUtils.mkdir_p(path)
render(readme_template, template_arguments, File.join(path, "README.md"))
render(source_template, template_arguments, File.join(sources_dir, "build.gradle.kts"))
end

def self.set_up_kotlin_interfaces(path, template_arguments, config)
dirname = File.dirname(__FILE__)
sources_dir = path
source_template = File.read("#{dirname}/templates/kotlin/arkana_protocol.kt.erb")
FileUtils.mkdir_p(path)
render(source_template, template_arguments, File.join(sources_dir, "#{config.namespace}Environment.kt"))
end

def self.set_up_kotlin_classes(sources_dir, tests_dir, template_arguments, config)
dirname = File.dirname(__FILE__)
source_template = File.read("#{dirname}/templates/kotlin/arkana.kt.erb")
tests_template = File.read("#{dirname}/templates/kotlin/arkana_tests.kt.erb")
FileUtils.mkdir_p(sources_dir)
FileUtils.mkdir_p(tests_dir) if config.should_generate_unit_tests
render(source_template, template_arguments, File.join(sources_dir, "#{config.namespace}.kt"))
render(tests_template, template_arguments, File.join(tests_dir, "#{config.namespace}Test.kt")) if config.should_generate_unit_tests
end

def self.render(template, template_arguments, destination_file)
renderer = ERB.new(template, trim_mode: ">") # Don't automatically add newlines at the end of each template tag
result = renderer.result(template_arguments.get_binding)
File.write(destination_file, result)
end
end
6 changes: 6 additions & 0 deletions lib/arkana/models/arguments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ class Arguments
attr_reader :flavor
# @returns [Array<string>]
attr_reader :include_environments
# @returns [string]
attr_reader :lang

def initialize
# Default values
@config_filepath = ".arkana.yml"
@dotenv_filepath = ".env" if File.exist?(".env")
@flavor = nil
@include_environments = nil
@lang = "swift"

OptionParser.new do |opt|
opt.on("-c", "--config-filepath /path/to/your/.arkana.yml", "Path to your config file. Defaults to '.arkana.yml'") do |o|
Expand All @@ -33,6 +36,9 @@ def initialize
opt.on("-i", "--include-environments 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 ommitted, Arkana generate secrets for all environments.") do |o|
@include_environments = o.split(",")
end
opt.on("-l", "--lang kotlin", "Language to produce keys for, e.g. kotlin, swift. Defaults to swift. See the README for more information") do |o|
@lang = o
end
end.parse!
end
end
15 changes: 15 additions & 0 deletions lib/arkana/models/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ class Config
attr_reader :pod_name
# @returns [string]
attr_reader :result_path
# @returns [string]
attr_reader :kotlin_package_name
# @returns [string]
attr_reader :kotlin_sources_path
# @returns [string[]]
attr_reader :flavors
# @returns [string]
Expand All @@ -26,11 +30,17 @@ class Config
attr_reader :package_manager
# @returns [boolean]
attr_reader :should_cocoapods_cross_import_modules
# @returns [boolean]
attr_reader :should_generate_gradle_build_file
# @returns [int]
attr_reader :kotlin_jvm_toolchain_version

# @returns [string]
attr_accessor :current_flavor
# @returns [string]
attr_accessor :dotenv_filepath
# @returns [string]
attr_accessor :current_lang

# rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
def initialize(yaml)
Expand All @@ -42,13 +52,18 @@ def initialize(yaml)
@import_name = yaml["import_name"] || default_name
@pod_name = yaml["pod_name"] || default_name
@result_path = yaml["result_path"] || default_name
@kotlin_package_name = yaml["kotlin_package_name"] || "com.arkanakeys"
@kotlin_sources_path = yaml["kotlin_sources_path"] || "kotlin"
@flavors = yaml["flavors"] || []
@swift_declaration_strategy = yaml["swift_declaration_strategy"] || "let"
@should_generate_unit_tests = yaml["should_generate_unit_tests"]
@should_generate_unit_tests = true if @should_generate_unit_tests.nil?
@package_manager = yaml["package_manager"] || "spm"
@should_cocoapods_cross_import_modules = yaml["should_cocoapods_cross_import_modules"]
@should_cocoapods_cross_import_modules = true if @should_cocoapods_cross_import_modules.nil?
@should_generate_gradle_build_file = yaml["should_generate_gradle_build_file"]
@should_generate_gradle_build_file = true if @should_generate_gradle_build_file.nil?
@kotlin_jvm_toolchain_version = yaml["kotlin_jvm_toolchain_version"] || 11
end
# rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity

Expand Down
4 changes: 4 additions & 0 deletions lib/arkana/models/template_arguments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ def initialize(environment_secrets:, global_secrets:, config:, salt:)
@pod_name = config.pod_name
# The top level namespace in which the keys will be generated. Often an enum.
@namespace = config.namespace
# Name of the kotlin package to be used for the generated code.
@kotlin_package_name = config.kotlin_package_name
# The kotlin JVM toolchain JDK version to be used in the generated build.gradle file.
@kotlin_jvm_toolchain_version = config.kotlin_jvm_toolchain_version
# The property declaration strategy declared in the config file.
@swift_declaration_strategy = config.swift_declaration_strategy
# Whether unit tests should be generated.
Expand Down
53 changes: 53 additions & 0 deletions lib/arkana/templates/kotlin/arkana.kt.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<% require 'arkana/helpers/string' %>
<% require 'arkana/helpers/kotlin_template_helper' %>
<% # TODO: Sort these import statements alphabetically %>
// DO NOT MODIFY
// Automatically generated by Arkana (https://github.com/rogerluan/arkana)
package <%= @kotlin_package_name %>


object <%= @namespace %> {
private val salt = listOf(<%= @salt.formatted %>)

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 {
<% for secret in @global_secrets %>
val <%= secret.key.camel_case %>: <%= KotlinTemplateHelper.kotlin_type(secret.type) %>
rogerluan marked this conversation as resolved.
Show resolved Hide resolved

get() {
val encoded = listOf(<%= secret.encoded_value %>)

return <%= KotlinTemplateHelper.kotlin_decode_function(secret.type) %>(encoded = encoded, cipher = salt)
}
<% end %>
}

<% for environment in @environments %>
object <%= environment %> : <%= @namespace %>Environment {
<% for secret in environment_protocol_secrets(environment) %>
override val <%= secret.protocol_key.camel_case %>: <%= KotlinTemplateHelper.kotlin_type(secret.type) %>

get() {
val encoded = listOf(<%= secret.encoded_value %>)

return <%= KotlinTemplateHelper.kotlin_decode_function(secret.type) %>(encoded = encoded, cipher = salt)
}
<% end %>
}
<% end %>
}
13 changes: 13 additions & 0 deletions lib/arkana/templates/kotlin/arkana_protocol.kt.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<% require 'arkana/helpers/string' %>
<% require 'arkana/helpers/kotlin_template_helper' %>
// DO NOT MODIFY
// Automatically generated by Arkana (https://github.com/rogerluan/arkana)
package <%= @kotlin_package_name %>


interface <%= @namespace %>Environment {
<% for secret in @environment_secrets.uniq(&:protocol_key) %>
val <%= secret.protocol_key.camel_case %>: <%= KotlinTemplateHelper.kotlin_type(secret.type) %>

<% end %>
}
Loading
Loading