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 5 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
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.downcase}")
husseinala marked this conversation as resolved.
Show resolved Hide resolved
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.'"
husseinala marked this conversation as resolved.
Show resolved Hide resolved
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.'"
husseinala marked this conversation as resolved.
Show resolved Hide resolved
end
end
end
54 changes: 54 additions & 0 deletions lib/arkana/kotlin_code_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 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, 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, 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(path, template_arguments, config)
dirname = File.dirname(__FILE__)
sources_dir = path
source_template = File.read("#{dirname}/templates/kotlin/arkana.kt.erb")
FileUtils.mkdir_p(path)
FileUtils.mkdir_p(sources_dir)
render(source_template, template_arguments, File.join(sources_dir, "#{config.namespace}.kt"))
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, for e.g. kotlin. See the README for more information") do |o|
husseinala marked this conversation as resolved.
Show resolved Hide resolved
@lang = o
end
end.parse!
end
end
12 changes: 12 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,15 @@ 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 [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 +50,17 @@ 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"] || "src/main/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?
end
# rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity

Expand Down
2 changes: 2 additions & 0 deletions lib/arkana/models/template_arguments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ 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 property declaration strategy declared in the config file.
@swift_declaration_strategy = config.swift_declaration_strategy
# Whether unit tests should be generated.
Expand Down
52 changes: 52 additions & 0 deletions lib/arkana/templates/kotlin/arkana.kt.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<% 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 %>)

private 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)
}

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

private 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) %>
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 %>

}
12 changes: 12 additions & 0 deletions lib/arkana/templates/kotlin/build.gradle.kts.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// DO NOT MODIFY
// Automatically generated by Arkana (https://github.com/rogerluan/arkana)

plugins {
id("java-library")
id("kotlin")
}

java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
husseinala marked this conversation as resolved.
Show resolved Hide resolved
}
husseinala marked this conversation as resolved.
Show resolved Hide resolved
16 changes: 16 additions & 0 deletions spec/arkana_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,21 @@
expect { described_class.run(arguments) }.not_to raise_error
end
end
context "when kotlin is specified as the language" do
let(:lang) { "kotlin" }

before do
ARGV << "--lang" << lang

config.all_keys.each do |key|
allow(ENV).to receive(:[]).with(key).and_return("lorem ipsum")
end
end

it "calls KotlinCodeGenerator.generate" do
expect(KotlinCodeGenerator).to receive(:generate)
described_class.run(arguments)
end
end
end
end
18 changes: 18 additions & 0 deletions spec/config_parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@
end
end

describe "#current_lang" do
describe "when language is specified in arguments" do
let(:lang) { "kotlin" }

before { ARGV << "--lang" << lang }

it "is the same as the language specified" do
expect(subject.current_lang).to eq lang
end
end

describe "when flavor is not specified in arguments" do
it "is nil" do
expect(subject.current_flavor).to be_nil
end
end
end

describe "#dotenv_filepath" do
context "when dotenv_filepath is specified" do
context "when it exists" do
Expand Down
Loading
Loading