remocon 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.gitignore +313 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +70 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +13 -0
  8. data/Gemfile +7 -0
  9. data/Gemfile.lock +72 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +91 -0
  12. data/Rakefile +6 -0
  13. data/bin/console +15 -0
  14. data/bin/get_access_token +11 -0
  15. data/bin/get_access_token.py +18 -0
  16. data/bin/put_by_curl +13 -0
  17. data/bin/setup +8 -0
  18. data/cmd/remocon +6 -0
  19. data/lib/remocon.rb +47 -0
  20. data/lib/remocon/cli.rb +41 -0
  21. data/lib/remocon/command/create_command.rb +48 -0
  22. data/lib/remocon/command/lib/interpreter_helper.rb +44 -0
  23. data/lib/remocon/command/pull_command.rb +66 -0
  24. data/lib/remocon/command/push_command.rb +119 -0
  25. data/lib/remocon/command/validate_command.rb +38 -0
  26. data/lib/remocon/dumper/condition_file_dumper.rb +14 -0
  27. data/lib/remocon/dumper/parameter_file_dumper.rb +22 -0
  28. data/lib/remocon/error/unsupported_type_error.rb +9 -0
  29. data/lib/remocon/error/validation_error.rb +10 -0
  30. data/lib/remocon/interpreter/condition_file_interpreter.rb +32 -0
  31. data/lib/remocon/interpreter/parameter_file_interpreter.rb +60 -0
  32. data/lib/remocon/normalizer/boolean_normalizer.rb +23 -0
  33. data/lib/remocon/normalizer/integer_normalizer.rb +23 -0
  34. data/lib/remocon/normalizer/json_normalizer.rb +20 -0
  35. data/lib/remocon/normalizer/normalizer.rb +33 -0
  36. data/lib/remocon/normalizer/string_normalizer.rb +13 -0
  37. data/lib/remocon/normalizer/type_normalizer_factory.rb +25 -0
  38. data/lib/remocon/normalizer/void_normalizer.rb +9 -0
  39. data/lib/remocon/sorter/condition_sorter.rb +23 -0
  40. data/lib/remocon/sorter/parameter_sorter.rb +24 -0
  41. data/lib/remocon/type.rb +11 -0
  42. data/lib/remocon/util/array.rb +15 -0
  43. data/lib/remocon/util/hash.rb +29 -0
  44. data/lib/remocon/util/string.rb +13 -0
  45. data/lib/remocon/version.rb +5 -0
  46. data/remocon.gemspec +42 -0
  47. data/sample/basketball-b8548/conditions.yml +7 -0
  48. data/sample/basketball-b8548/config.json +67 -0
  49. data/sample/basketball-b8548/etag +1 -0
  50. data/sample/basketball-b8548/parameters.yml +20 -0
  51. metadata +193 -0
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Jumpei Matsuda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,91 @@
1
+ # Remocon
2
+
3
+ *remocon* is a CLI for Firebase Remote Config via its REST API.
4
+ Conditions and parameters are managed by YAML files.
5
+
6
+ *This is still in beta. A diff mode should be supported when managing configs heavily.*
7
+
8
+ ## Usage
9
+
10
+ You need to get an access token for your firebase project.
11
+
12
+ ```bash
13
+ export FIREBASE_PROJECT_ID='your project id'
14
+ export REMOTE_CONFIG_ACCESS_TOKEN='your access token'
15
+ ```
16
+
17
+ ### Get the current configs into your local
18
+
19
+ ```bash
20
+ bundle exec remocon pull --dest=${path to dir}
21
+
22
+ # you can see ${path to dir}/${FIREBASE_PROJECT_ID}/{paremeters.yml, conditions.yml, config.json, etag}
23
+ ```
24
+
25
+ ### Edit configs on your local
26
+
27
+ Condition definitions and parameter definitions are separated. You should modify these files.
28
+
29
+ *parameters.yml*
30
+
31
+ ```yaml
32
+ key1: # key name
33
+ value: 100 # default value
34
+ conditions:
35
+ condition1:
36
+ value: 200 # a value to be used if condition1 is satisfied
37
+ condition2:
38
+ file: path_to_file # you can use file content. the file content is used for a value
39
+ ```
40
+
41
+ *conditions.yml*
42
+
43
+ ```yaml
44
+ - name: condition1 # condition name
45
+ expression: device.os == 'android' # expression
46
+ tagColor: "INDIGO" # color name
47
+ - name: condition2
48
+ expression: device.os == 'ios'
49
+ tagColor: CYAN
50
+ ```
51
+
52
+ ### Update configs on remote
53
+
54
+ ```bash
55
+ bundle exec remocon push --source=${path to a json file} --etag=${string or path to a file}
56
+ ```
57
+
58
+ ## Installation
59
+
60
+ ```ruby
61
+ gem 'remocon'
62
+ ```
63
+
64
+ ## Format
65
+
66
+ ### Parameters
67
+
68
+ You can use String, Boolean, Integer, Json validators like below.
69
+
70
+ ```yaml
71
+ key:
72
+ value: # optional (either of this or file is required). Raw value and hash are allowed.
73
+ file: # optional (either of this or value is required). File content value.
74
+ normalizer: # optional. Either of ["integer", "string", "boolean", "json", "void"] (default: void).
75
+ conditions: # optional. If you want use conditional values, then you need to create this section.
76
+ condition_name: # must be in condition definitions.
77
+ value: ...
78
+ file: ...
79
+ ```
80
+
81
+ ### Conditions
82
+
83
+ It seems only three fields are supported by Remote Config. They are name, expression and tagColor.
84
+
85
+ ## Contributing
86
+
87
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jmatsu/remocon .
88
+
89
+ ## License
90
+
91
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'remocon'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
@@ -0,0 +1,11 @@
1
+ #!/bin/bash
2
+
3
+ set -eu
4
+
5
+ if type easy_install >/dev/null 2>&1; then
6
+ sudo easy_install --upgrade google-api-python-client
7
+ else
8
+ sudo pip install --upgrade google-api-python-client
9
+ fi
10
+
11
+ get_access_token.py
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env python
2
+
3
+ # python 2.x
4
+
5
+ import sys
6
+ from oauth2client.service_account import ServiceAccountCredentials
7
+
8
+ def print_access_token():
9
+ argv = sys.argv
10
+ if (len(argv) != 2):
11
+ print >> sys.stderr, 'a filepath to service-account.json must be specified as the 1st argument.'
12
+ quit()
13
+ credentials = ServiceAccountCredentials.from_json_keyfile_name(
14
+ argv[1], ['https://www.googleapis.com/auth/firebase.remoteconfig'])
15
+ access_token_info = credentials.get_access_token()
16
+ print(access_token_info.access_token)
17
+
18
+ print_access_token
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -eu
4
+
5
+ etag_file="$1"
6
+ config_json_file="$2"
7
+
8
+ curl --compressed \
9
+ -H "Content-Type: application/json; UTF8" \
10
+ -H "If-Match: $(cat $etag_file)" \
11
+ -H "Authorization: Bearer ${REMOTE_CONFIG_ACCESS_TOKEN}" \
12
+ -X PUT "https://firebaseremoteconfig.googleapis.com/v1/projects/${FIREBASE_PROJECT_ID}/remoteConfig" \
13
+ -d "@$config_json_file"
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'remocon'
5
+
6
+ Remocon::CLI.start(ARGV)
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'json'
5
+ require 'thor'
6
+ require 'active_support'
7
+ require 'active_support/core_ext'
8
+ require 'singleton'
9
+ require 'open-uri'
10
+ require 'fileutils'
11
+ require 'net/http'
12
+
13
+ require 'remocon/util/array'
14
+ require 'remocon/util/hash'
15
+ require 'remocon/util/string'
16
+
17
+ require 'remocon/version'
18
+ require 'remocon/type'
19
+
20
+ require 'remocon/error/unsupported_type_error'
21
+ require 'remocon/error/validation_error'
22
+
23
+ require 'remocon/sorter/condition_sorter'
24
+ require 'remocon/sorter/parameter_sorter'
25
+
26
+ require 'remocon/dumper/condition_file_dumper'
27
+ require 'remocon/dumper/parameter_file_dumper'
28
+
29
+ require 'remocon/interpreter/condition_file_interpreter'
30
+ require 'remocon/interpreter/parameter_file_interpreter'
31
+
32
+ require 'remocon/normalizer/normalizer'
33
+ require 'remocon/normalizer/boolean_normalizer'
34
+ require 'remocon/normalizer/integer_normalizer'
35
+ require 'remocon/normalizer/json_normalizer'
36
+ require 'remocon/normalizer/string_normalizer'
37
+ require 'remocon/normalizer/void_normalizer'
38
+ require 'remocon/normalizer/type_normalizer_factory'
39
+
40
+ require 'remocon/command/lib/interpreter_helper'
41
+
42
+ require 'remocon/command/create_command'
43
+ require 'remocon/command/pull_command'
44
+ require 'remocon/command/push_command'
45
+ require 'remocon/command/validate_command'
46
+
47
+ require 'remocon/cli'
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remocon
4
+ class CLI < ::Thor
5
+ desc 'create', 'Create a json to be pushed'
6
+ option :parameters, type: :string, required: true, desc: 'a filepath of parameters file'
7
+ option :conditions, type: :string, required: true, desc: 'a filepath of conditions file'
8
+ option :dest, type: :string, desc: 'a filepath or directory path of destination'
9
+ def create
10
+ execute(Remocon::Command::Create)
11
+ end
12
+
13
+ desc 'push', 'Upload remote configs based on a source json file'
14
+ class_option :source, type: :string, desc: 'a filepath of a source json file'
15
+ option :etag, type: :string, desc: 'a filepath or raw value of etag'
16
+ option :dest, type: :string, desc: 'a filepath or directory path of destination'
17
+ option :force, type: :boolean, default: false, desc: 'force to ignore some warnings'
18
+ def push
19
+ execute(Remocon::Command::Push)
20
+ end
21
+
22
+ desc 'pull', 'Pull remote configs'
23
+ option :dest, type: :string, desc: 'a filepath or directory path of destination'
24
+ def pull
25
+ execute(Remocon::Command::Pull)
26
+ end
27
+
28
+ desc 'validate', 'Validate yml files'
29
+ option :parameters, type: :string, required: true, desc: 'a filepath of parameters file'
30
+ option :conditions, type: :string, required: true, desc: 'a filepath of conditions file'
31
+ def validate
32
+ execute(Remocon::Command::Validate)
33
+ end
34
+
35
+ private
36
+
37
+ def execute(klass)
38
+ klass.new(options).run
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remocon
4
+ module Command
5
+ class Create
6
+ include Remocon::InterpreterHelper
7
+
8
+ def initialize(opts)
9
+ @opts = opts
10
+
11
+ @project_id = ENV.fetch('FIREBASE_PROJECT_ID')
12
+ @conditions_filepath = @opts[:conditions]
13
+ @parameters_filepath = @opts[:parameters]
14
+ @dest_dir = File.join(@opts[:dest], @project_id) if @opts[:dest]
15
+
16
+ @cmd_opts = { validate_only: false }
17
+ end
18
+
19
+ def run
20
+ validate_options
21
+
22
+ artifact = {
23
+ conditions: condition_array,
24
+ parameters: parameter_hash
25
+ }.skip_nil_values.stringify_values
26
+
27
+ if @dest_dir
28
+ File.open(File.join(@dest_dir, 'config.json'), 'w+') do |f|
29
+ # remote config allows only string values ;(
30
+ f.write(JSON.pretty_generate(artifact))
31
+ f.flush
32
+ end
33
+ else
34
+ STDOUT.puts JSON.pretty_generate(artifact)
35
+ end
36
+
37
+ artifact
38
+ end
39
+
40
+ private
41
+
42
+ def validate_options
43
+ raise ValidationError, 'A condition file must exist' unless File.exist?(@conditions_filepath)
44
+ raise ValidationError, 'A parameter file must exist' unless File.exist?(@parameters_filepath)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remocon
4
+ module InterpreterHelper
5
+ include Remocon::ConditionSorter
6
+ include Remocon::ParameterSorter
7
+
8
+ attr_reader :parameters_filepath, :conditions_filepath, :cmd_opts
9
+
10
+ def read_parameters
11
+ @read_parameters ||= begin
12
+ parameter_interpreter = Remocon::ParameterFileInterpreter.new(parameters_filepath)
13
+ parameter_interpreter.read(condition_names, cmd_opts)
14
+ end
15
+ end
16
+
17
+ def parameter_hash
18
+ @parameter_hash ||= sort_parameters(read_parameters.first)
19
+ end
20
+
21
+ def parameter_errors
22
+ @parameter_errors ||= read_parameters.second
23
+ end
24
+
25
+ def read_conditions
26
+ @read_conditions ||= begin
27
+ condition_interpreter = Remocon::ConditionFileInterpreter.new(conditions_filepath)
28
+ condition_interpreter.read(cmd_opts)
29
+ end
30
+ end
31
+
32
+ def condition_array
33
+ @condition_array ||= sort_conditions(read_conditions.first)
34
+ end
35
+
36
+ def condition_errors
37
+ @condition_errors ||= read_conditions.second
38
+ end
39
+
40
+ def condition_names
41
+ @condition_names ||= condition_array.map { |e| e[:name] }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remocon
4
+ module Command
5
+ class Pull
6
+ include Remocon::InterpreterHelper
7
+
8
+ def initialize(opts)
9
+ @opts = opts
10
+
11
+ @project_id = ENV.fetch('FIREBASE_PROJECT_ID')
12
+ @token = ENV.fetch('REMOTE_CONFIG_ACCESS_TOKEN')
13
+ @url = "https://firebaseremoteconfig.googleapis.com/v1/projects/#{@project_id}/remoteConfig"
14
+ @dest_dir = File.join(@opts[:dest], @project_id) if @opts[:dest]
15
+
16
+ @cmd_opts = { validate_only: false }
17
+ end
18
+
19
+ def run
20
+ if @dest_dir
21
+ FileUtils.mkdir_p(@dest_dir)
22
+
23
+ raw_hash = JSON.parse(raw_json).with_indifferent_access
24
+
25
+ raise 'etag cannot be fetched. please try again' unless @etag
26
+
27
+ conditions, parameters = [raw_hash[:conditions] || [], raw_hash[:parameters] || {}]
28
+
29
+ File.open(File.join(@dest_dir, "conditions.yml"), 'w+') do |f|
30
+ f.write(JSON.parse(Remocon::ConditionFileDumper.new(sort_conditions(conditions)).dump.to_json).to_yaml)
31
+ f.flush
32
+ end
33
+
34
+ File.open(File.join(@dest_dir, "parameters.yml"), 'w+') do |f|
35
+ f.write(JSON.parse(Remocon::ParameterFileDumper.new(sort_parameters(parameters)).dump.to_json).to_yaml)
36
+ f.flush
37
+ end
38
+
39
+ File.open(File.join(@dest_dir, "config.json"), 'w+') do |f|
40
+ f.write(JSON.pretty_generate({ conditions: sort_conditions(conditions), parameters: sort_parameters(parameters) }))
41
+ f.flush
42
+ end
43
+
44
+ File.open(File.join(@dest_dir, 'etag'), 'w+') do |f|
45
+ f.write(@etag)
46
+ f.flush
47
+ end
48
+ else
49
+ STDOUT.puts raw_json
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def raw_json
56
+ return @raw_json if @raw_json
57
+
58
+ @raw_json, @etag = open(@url, 'Authorization' => "Bearer #{@token}") do |io|
59
+ [io.read, io.meta['etag']]
60
+ end
61
+
62
+ @raw_json
63
+ end
64
+ end
65
+ end
66
+ end