Skip to content

Commit

Permalink
add support for dart
Browse files Browse the repository at this point in the history
  • Loading branch information
humblerookie committed Jun 14, 2024
1 parent 34826d6 commit 9165a09
Show file tree
Hide file tree
Showing 9 changed files with 356 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ GEM
rspec-mocks (~> 3.13.0)
rspec-core (3.13.0)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.0)
rspec-expectations (3.13.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.1)
Expand Down
2 changes: 2 additions & 0 deletions lib/arkana.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require_relative "arkana/salt_generator"
require_relative "arkana/swift_code_generator"
require_relative "arkana/kotlin_code_generator"
require_relative "arkana/dart_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 @@ -48,6 +49,7 @@ def self.run(arguments)
generator = case config.current_lang.downcase
when "swift" then SwiftCodeGenerator
when "kotlin" then KotlinCodeGenerator
when "dart" then DartCodeGenerator
else UI.crash("Unknown output lang selected: #{config.current_lang}")
end

Expand Down
44 changes: 44 additions & 0 deletions lib/arkana/dart_code_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

require "erb" unless defined?(Erb)
require "fileutils" unless defined?(FileUtils)
require_relative "helpers/string"

# Responsible for generating Dart source and test files.
module DartCodeGenerator
# Generates Dart code and test files for the given template arguments.
def self.generate(template_arguments:, config:)
dart_sources_dir = File.join("lib", config.result_path.downcase)
dart_tests_dir = File.join("test", config.result_path.downcase)
set_up_dart_interfaces(dart_sources_dir, template_arguments, config)
set_up_dart_classes(dart_sources_dir, dart_tests_dir, template_arguments, config)
end

def self.set_up_dart_interfaces(path, template_arguments, config)
dirname = File.dirname(__FILE__)
sources_dir = path
source_template = File.read("#{dirname}/templates/dart/arkana_protocol.dart.erb")
FileUtils.mkdir_p(path)
render(source_template, template_arguments, File.join(sources_dir, "#{config.namespace.downcase}_environment.dart"))
end

def self.set_up_dart_classes(sources_dir, tests_dir, template_arguments, config)
dirname = File.dirname(__FILE__)
source_template = File.read("#{dirname}/templates/dart/arkana.dart.erb")
tests_template = File.read("#{dirname}/templates/dart/arkana_tests.dart.erb")
FileUtils.mkdir_p(sources_dir)
if config.should_generate_unit_tests
FileUtils.mkdir_p(tests_dir)
end
render(source_template, template_arguments, File.join(sources_dir, "#{config.namespace.downcase}.dart"))
if config.should_generate_unit_tests
render(tests_template, template_arguments, File.join(tests_dir, "#{config.namespace.downcase}_test.dart"))
end
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
28 changes: 28 additions & 0 deletions lib/arkana/helpers/dart_template_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

# Utilities to reduce the amount of boilerplate code in `.dart.erb` template files.
module DartTemplateHelper
def self.dart_type(type)
case type
when :string then "String"
when :boolean then "bool"
when :integer then "int"
else raise "Unknown variable type '#{type}' received."
end
end

def self.dart_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

def self.relative_path_to_source(src)
slash_count = src.count("/")
padding = src.empty? ? 1 : 2
"../" * (slash_count + padding)
end
end
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
# Dart sources Path
@result_path = config.result_path
# 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.
Expand Down
67 changes: 67 additions & 0 deletions lib/arkana/templates/dart/arkana.dart.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<% require "arkana/helpers/string" %>
<% require "arkana/helpers/dart_template_helper" %>
import '<%=(@namespace).downcase%>_environment.dart';

// DO NOT MODIFY
// Automatically generated by Arkana (https://github.com/rogerluan/arkana)
const _salt = [<%= @salt.formatted %>];

class <%= @namespace %> {

static final Global global = Global();

<% for environment in @environments %>
static final <%= @namespace %>Environment <%=environment.camel_case()%> = _<%= environment %>();

<% end %>
static String decode({required List<int> encoded, required List<int> cipher}) {
var index = -1;
final decoded = encoded.map((item) =>
(item ^ cipher[++index % cipher.length]) & 0xff
).toList();
return String.fromCharCodes(decoded);
}

static int decodeInt({required List<int> encoded, required List<int> cipher}) {
return int.parse(decode(encoded: encoded, cipher: cipher));
}

static bool decodeBoolean({required List<int> encoded, required List<int> cipher}) {
return decode(encoded: encoded, cipher: cipher) == 'true';
}

}

class Global {

<% @global_secrets.each_with_index do |secret, index| %>
<%= DartTemplateHelper.dart_type(secret.type) %> get <%= secret.key.camel_case %> {
final encoded = <int>[<%= secret.encoded_value %>];
return <%= @namespace %>.<%= DartTemplateHelper.dart_decode_function(secret.type) %>(encoded: encoded, cipher: _salt);
}
<% unless index == @global_secrets.length - 1 %>

<% end %>
<% end %>

}

<% @environments.each_with_index do |environment, env_index| %>
class _<%= environment %> implements <%= @namespace %>Environment {

<% environment_protocol_secrets(environment).each_with_index do |secret, secret_index| %>
@override
<%= DartTemplateHelper.dart_type(secret.type) %> get <%= secret.protocol_key.camel_case %> {
final encoded = <int>[<%= secret.encoded_value %>];
return <%= @namespace %>.<%= DartTemplateHelper.dart_decode_function(secret.type) %>(encoded: encoded, cipher: _salt);
}
<% unless secret_index == environment_protocol_secrets(environment).length - 1 %>

<% end %>
<% end %>

}
<% unless env_index == @environments.length - 1 %>

<% end %>
<% end %>
12 changes: 12 additions & 0 deletions lib/arkana/templates/dart/arkana_protocol.dart.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<% require "arkana/helpers/string" %>
<% require "arkana/helpers/dart_template_helper" %>
// DO NOT MODIFY
// Automatically generated by Arkana (https://github.com/rogerluan/arkana)

abstract class <%= @namespace %>Environment {

<% for secret in @environment_secrets.uniq(&:protocol_key) %>
<%=DartTemplateHelper.dart_type(secret.type)%> get <%= secret.protocol_key.camel_case %>;
<% end %>

}
120 changes: 120 additions & 0 deletions lib/arkana/templates/dart/arkana_tests.dart.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<% require "arkana/helpers/string" %>
<% require "arkana/helpers/dart_template_helper" %>
// DO NOT MODIFY
// Automatically generated by Arkana (https://github.com/rogerluan/arkana)


import 'package:test/test.dart';
import '<%=DartTemplateHelper.relative_path_to_source(@result_path.downcase)%>lib/<%= @result_path.downcase%>/<%= @namespace.downcase %>.dart';

void main(){
const List<int> salt = [<%= @salt.formatted %>];

test("decodeRandomHexKey_shouldDecode", () {
<% hex_key = SecureRandom.hex(64) %>
<% secret = generate_test_secret(key: hex_key) %>
const encoded = [<%= secret.encoded_value %>];
expect(<%= @namespace %>.decode(encoded: encoded, cipher: salt), "<%= hex_key %>");
});

test("decodeRandomBase64Key_shouldDecode", () {
<% base64_key = SecureRandom.base64(64) %>
<% secret = generate_test_secret(key: base64_key) %>
const encoded = [<%= secret.encoded_value %>];
expect(<%= @namespace %>.decode(encoded: encoded, cipher: salt), "<%= base64_key %>");
});

test("decodeUUIDKey_shouldDecode", () {
<% uuid_key = SecureRandom.uuid %>
<% secret = generate_test_secret(key: uuid_key) %>
const encoded = [<%= secret.encoded_value %>];
expect(<%= @namespace %>.decode(encoded: encoded, cipher: salt), "<%= uuid_key %>");
});

test("decodeTrueBoolValue_shouldDecode", () {
<% bool_key = "true" %>
<% secret = generate_test_secret(key: bool_key) %>
const encoded = [<%= secret.encoded_value %>];
assert(<%= @namespace %>.decodeBoolean(encoded: encoded, cipher: salt));
});

test("decodeFalseBoolValue_shouldDecode", () {
<% bool_key = "false" %>
<% secret = generate_test_secret(key: bool_key) %>
const encoded = [<%= secret.encoded_value %>];
assert(!<%= @namespace %>.decodeBoolean(encoded: encoded, cipher: salt));
});

test("decodeIntValue_shouldDecode", () {
<% int_key = "42" %>
<% secret = generate_test_secret(key: int_key) %>
const encoded = [<%= secret.encoded_value %>];
expect(<%= @namespace %>.decodeInt(encoded: encoded, cipher: salt), 42);
});

test("decodeIntValueWithLeadingZeroes_shouldDecodeAsString", () {
<% int_with_leading_zeroes_key = "0001" %>
<% secret = generate_test_secret(key: int_with_leading_zeroes_key) %>
const encoded = [<%= secret.encoded_value %>];
expect(<%= @namespace %>.decode(encoded: encoded, cipher: salt), "0001");
});

test("decodeMassiveIntValue_shouldDecodeAsString", () {
<% int_with_massive_number_key = "92233720368547758079223372036854775807" %>
<% secret = generate_test_secret(key: int_with_massive_number_key) %>
const encoded = [<%= secret.encoded_value %>];
expect(<%= @namespace %>.decode(encoded: encoded, cipher: salt), "92233720368547758079223372036854775807");
});

test("decodeNegativeIntValue_shouldDecodeAsString", () {
<% negative_int_key = "-42" %>
<% secret = generate_test_secret(key: negative_int_key) %>
const encoded = [<%= secret.encoded_value %>];
expect(<%= @namespace %>.decode(encoded: encoded, cipher: salt), "-42");
});

test("decodeFloatingPointValue_shouldDecodeAsString", () {
<% float_key = "3.14" %>
<% secret = generate_test_secret(key: float_key) %>
const encoded = [<%= secret.encoded_value %>];
expect(<%= @namespace %>.decode(encoded: encoded, cipher: salt), "3.14");
});

test("encodeAndDecodeValueWithDollarSign_shouldDecode", () {
<% dollar_sign_key = "real_$lim_shady" %>
<% secret = generate_test_secret(key: dollar_sign_key) %>
const encoded = [<%= secret.encoded_value %>];
expect(<%= @namespace %>.decode(encoded: encoded, cipher: salt), "real_\$lim_shady");
});


<% if ENV["ARKANA_RUNNING_CI_INTEGRATION_TESTS"] %>
const globalSecrets = <%= @namespace %>.Global;

test("decodeEnvVarFromDotfile_withDollarSign__andEscaped_andNoQuotes_shouldDecode", () {
expect(globalSecrets.secretWithDollarSignEscapedAndAndNoQuotesKey, "real_\$lim_shady");
});

test("decodeEnvVarFromDotfile_withDollarSign__andEscaped_andDoubleQuotes_shouldDecode", () {
expect(globalSecrets.secretWithDollarSignEscapedAndDoubleQuoteKey, "real_\$lim_shady");
});

test("decodeEnvVarFromDotfile_withDollarSign__andNotEscaped_andSingleQuotes_shouldDecode", () {
expect(globalSecrets.secretWithDollarSignNotEscapedAndSingleQuoteKey, "real_\$lim_shady");
});

test("decodeEnvVarFromDotfile_withDollarSign__andNotEscaped_andDoubleQuotes_shouldDecodeWithUnexpectedValue", () {
expect(globalSecrets.secretWithDollarSignNotEscapedAndDoubleQuotesKey, "real_\$lim_shady");
});

test("test_decodeEnvVarFromDotfile_withDollarSign__andNotEscaped_andNoQuotes_shouldDecodeWithUnexpectedValue", () {
expect(globalSecrets.secretWithDollarSignNotEscapedAndNoQuotesKey, "real_\$lim_shady");
});

test("test_decodeEnvVarFromDotfile_withWeirdCharacters_shouldDecode", () {
expect(globalSecrets.secretWithWeirdCharactersKey, "` ~ ! @ # % ^ & * ( ) _ - + = { [ } } | : ; ' < , > . ? /");
});

<% end %>
}

80 changes: 80 additions & 0 deletions spec/dart_code_generator_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# frozen_string_literal: true

RSpec.describe DartCodeGenerator do
let(:config) { Config.new(YAML.load_file("spec/fixtures/arkana-fixture.yml")) }
let(:salt) { SaltGenerator.generate }
let(:environment_secrets) do
Encoder.encode!(
keys: config.environment_keys,
salt: salt,
current_flavor: config.current_flavor,
environments: config.environments,
)
end

let(:global_secrets) do
Encoder.encode!(
keys: config.global_secrets,
salt: salt,
current_flavor: config.current_flavor,
environments: config.environments,
)
end

let(:template_arguments) do
TemplateArguments.new(
environment_secrets: environment_secrets,
global_secrets: global_secrets,
config: config,
salt: salt,
)
end

before do
config.all_keys.each do |key|
allow(ENV).to receive(:[]).with(key).and_return("value")
end
allow(ENV).to receive(:[]).with("ARKANA_RUNNING_CI_INTEGRATION_TESTS").and_return(true)
end

after { FileUtils.rm_rf(config.result_path) }

describe ".generate" do
let(:dart_module_dir) { config.result_path }
let(:dart_sources_dir) { File.join("lib", dart_module_dir) }
let(:dart_tests_dir) { File.join("test", dart_module_dir) }

def path(...)
Pathname.new(File.join(...))
end

it "generates all necessary directories and files" do
described_class.generate(template_arguments: template_arguments, config: config)
expect(path(dart_sources_dir, "#{config.namespace.downcase}_environment.dart")).to be_file
expect(path(dart_sources_dir, "#{config.namespace.downcase}.dart")).to be_file
end

context "when 'config.should_generate_unit_tests' is false" do
before do
allow(config).to receive(:should_generate_unit_tests).and_return(false)
described_class.generate(template_arguments: template_arguments, config: config)
end

it "does not generate test folder or files" do
expect(path(dart_tests_dir, "#{config.namespace.downcase}_test.dart")).not_to be_file
expect(Pathname.new(dart_tests_dir)).not_to be_directory
end
end

context "when 'config.should_generate_unit_tests' is true" do
before do
allow(config).to receive(:should_generate_unit_tests).and_return(true)
described_class.generate(template_arguments: template_arguments, config: config)
end

it "generates test folder and files" do
expect(path(dart_tests_dir, "#{config.namespace.downcase}_test.dart")).to be_file
end
end
end
end

0 comments on commit 9165a09

Please sign in to comment.