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 for flutter using dart generator #68

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions .github/workflows/dart-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Dart Tests
on:
push:
branches:
- main
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: dart-lang/setup-dart@v1
with:
sdk: 3.4.0
- name: Cache Dart dependencies
uses: actions/cache@v2
with:
path: ~/.pub-cache
key: ${{ runner.os }}-dart-${{ hashFiles('**/pubspec.lock') }}
restore-keys: |
${{ runner.os }}-dart-
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true # Runs 'bundle install' and caches installed gems automatically
- name: Generate Dart Code & Run Tests
run: bundle exec rake test_dart
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
14 changes: 14 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ task :test_kotlin do
end
end

desc "Generates dart source code and run its unit tests."
task :test_dart do
config_file = File.absolute_path("spec/fixtures/dart-tests.yml")
dotenv_file = File.absolute_path("spec/fixtures/.env.fruitloops")
directory_to_copy = File.absolute_path("spec/fixtures/dart")
with_temp_dir do |temp_dir|
puts "Current working directory: #{temp_dir}"
FileUtils.copy_entry(directory_to_copy, "tests")
Dir.chdir("tests")
sh("ARKANA_RUNNING_CI_INTEGRATION_TESTS=true && arkana --lang dart --config-filepath #{config_file} --dotenv-filepath #{dotenv_file} --include-environments dev,staging")
sh("dart test")
end
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
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
43 changes: 43 additions & 0 deletions lib/arkana/dart_code_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 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"))
return unless config.should_generate_unit_tests
render(tests_template, template_arguments, File.join(tests_dir, "#{config.namespace.downcase}_test.dart"))
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 %>
}

Loading
Loading