From 9165a0919032ef5d8eac665828cb2cc9e8390875 Mon Sep 17 00:00:00 2001 From: humblerookie <1428864+humblerookie@users.noreply.github.com> Date: Fri, 14 Jun 2024 12:16:10 +0530 Subject: [PATCH] add support for dart --- Gemfile.lock | 2 +- lib/arkana.rb | 2 + lib/arkana/dart_code_generator.rb | 44 +++++++ lib/arkana/helpers/dart_template_helper.rb | 28 ++++ lib/arkana/models/template_arguments.rb | 2 + lib/arkana/templates/dart/arkana.dart.erb | 67 ++++++++++ .../templates/dart/arkana_protocol.dart.erb | 12 ++ .../templates/dart/arkana_tests.dart.erb | 120 ++++++++++++++++++ spec/dart_code_generator_spec.rb | 80 ++++++++++++ 9 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 lib/arkana/dart_code_generator.rb create mode 100644 lib/arkana/helpers/dart_template_helper.rb create mode 100644 lib/arkana/templates/dart/arkana.dart.erb create mode 100644 lib/arkana/templates/dart/arkana_protocol.dart.erb create mode 100644 lib/arkana/templates/dart/arkana_tests.dart.erb create mode 100644 spec/dart_code_generator_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index f8b2c64..f5226a0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/lib/arkana.rb b/lib/arkana.rb index 589754a..10cb315 100644 --- a/lib/arkana.rb +++ b/lib/arkana.rb @@ -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. @@ -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 diff --git a/lib/arkana/dart_code_generator.rb b/lib/arkana/dart_code_generator.rb new file mode 100644 index 0000000..691d185 --- /dev/null +++ b/lib/arkana/dart_code_generator.rb @@ -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 diff --git a/lib/arkana/helpers/dart_template_helper.rb b/lib/arkana/helpers/dart_template_helper.rb new file mode 100644 index 0000000..c1efca8 --- /dev/null +++ b/lib/arkana/helpers/dart_template_helper.rb @@ -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 diff --git a/lib/arkana/models/template_arguments.rb b/lib/arkana/models/template_arguments.rb index 514c330..9bbd439 100644 --- a/lib/arkana/models/template_arguments.rb +++ b/lib/arkana/models/template_arguments.rb @@ -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. diff --git a/lib/arkana/templates/dart/arkana.dart.erb b/lib/arkana/templates/dart/arkana.dart.erb new file mode 100644 index 0000000..6aac9ba --- /dev/null +++ b/lib/arkana/templates/dart/arkana.dart.erb @@ -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 encoded, required List 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 encoded, required List cipher}) { + return int.parse(decode(encoded: encoded, cipher: cipher)); + } + + static bool decodeBoolean({required List encoded, required List 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 = [<%= 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 = [<%= 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 %> diff --git a/lib/arkana/templates/dart/arkana_protocol.dart.erb b/lib/arkana/templates/dart/arkana_protocol.dart.erb new file mode 100644 index 0000000..a29ff45 --- /dev/null +++ b/lib/arkana/templates/dart/arkana_protocol.dart.erb @@ -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 %> + +} \ No newline at end of file diff --git a/lib/arkana/templates/dart/arkana_tests.dart.erb b/lib/arkana/templates/dart/arkana_tests.dart.erb new file mode 100644 index 0000000..f092fff --- /dev/null +++ b/lib/arkana/templates/dart/arkana_tests.dart.erb @@ -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 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 %> +} + diff --git a/spec/dart_code_generator_spec.rb b/spec/dart_code_generator_spec.rb new file mode 100644 index 0000000..d786b55 --- /dev/null +++ b/spec/dart_code_generator_spec.rb @@ -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