nugrant 2.0.0.dev2 → 2.0.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +6 -14
  2. data/.gitignore +2 -1
  3. data/.travis.yml +2 -2
  4. data/CHANGELOG.md +148 -3
  5. data/Gemfile +8 -20
  6. data/README.md +266 -72
  7. data/Rakefile +1 -0
  8. data/lib/nugrant.rb +14 -6
  9. data/lib/nugrant/bag.rb +116 -62
  10. data/lib/nugrant/helper/bag.rb +19 -19
  11. data/lib/nugrant/helper/env/exporter.rb +208 -0
  12. data/lib/nugrant/helper/env/namer.rb +47 -0
  13. data/lib/nugrant/helper/parameters.rb +12 -0
  14. data/lib/nugrant/helper/stack.rb +86 -0
  15. data/lib/nugrant/mixin/parameters.rb +98 -0
  16. data/lib/nugrant/parameters.rb +14 -68
  17. data/lib/nugrant/vagrant/errors.rb +27 -0
  18. data/lib/nugrant/vagrant/v2/command/env.rb +101 -0
  19. data/lib/nugrant/vagrant/v2/command/helper.rb +30 -0
  20. data/lib/nugrant/vagrant/v2/command/parameters.rb +16 -4
  21. data/lib/nugrant/vagrant/v2/command/restricted_keys.rb +60 -0
  22. data/lib/nugrant/vagrant/v2/command/root.rb +12 -2
  23. data/lib/nugrant/vagrant/v2/config/user.rb +9 -21
  24. data/lib/nugrant/vagrant/v2/plugin.rb +0 -1
  25. data/lib/nugrant/version.rb +1 -1
  26. data/locales/en.yml +13 -0
  27. data/nugrant.gemspec +3 -7
  28. data/test/lib/nugrant/helper/env/test_exporter.rb +238 -0
  29. data/test/lib/nugrant/helper/test_bag.rb +16 -0
  30. data/test/lib/nugrant/helper/test_parameters.rb +17 -0
  31. data/test/lib/nugrant/helper/test_stack.rb +152 -0
  32. data/test/lib/nugrant/test_bag.rb +132 -22
  33. data/test/lib/nugrant/test_config.rb +95 -92
  34. data/test/lib/nugrant/test_parameters.rb +232 -177
  35. data/test/lib/test_helper.rb +3 -0
  36. data/test/resources/json/params_user_nil_values.json +9 -0
  37. data/test/resources/vagrantfiles/v2.defaults_mixed_string_symbols +18 -0
  38. data/test/resources/vagrantfiles/v2.defaults_null_values_in_vagrantuser +23 -0
  39. data/test/resources/vagrantfiles/v2.defaults_using_string +18 -0
  40. data/test/resources/vagrantfiles/v2.defaults_using_symbol +18 -0
  41. data/test/resources/{Vagrantfile.v2.empty → vagrantfiles/v2.empty} +0 -2
  42. data/test/resources/{Vagrantfile.v2.fake → vagrantfiles/v2.fake} +4 -3
  43. data/test/resources/vagrantfiles/v2.missing_parameter +3 -0
  44. data/test/resources/{Vagrantfile.v2.real → vagrantfiles/v2.real} +0 -2
  45. data/test/resources/yaml/params_user_nil_values.yml +5 -0
  46. metadata +55 -88
  47. data/lib/nugrant/vagrant/v1/command/parameters.rb +0 -134
  48. data/lib/nugrant/vagrant/v1/command/root.rb +0 -81
  49. data/lib/nugrant/vagrant/v1/config/user.rb +0 -37
  50. data/lib/nugrant/vagrant/v1/plugin.rb +0 -6
  51. data/lib/vagrant_init.rb +0 -2
  52. data/test/resources/Vagrantfile.v1.empty +0 -2
  53. data/test/resources/Vagrantfile.v1.fake +0 -10
  54. data/test/resources/Vagrantfile.v1.real +0 -19
data/Rakefile CHANGED
@@ -17,4 +17,5 @@ end
17
17
 
18
18
  desc "Run tests"
19
19
  task :default => :test
20
+ task :tests => :test
20
21
 
data/lib/nugrant.rb CHANGED
@@ -8,16 +8,24 @@ unless defined?(KeyError)
8
8
  end
9
9
  end
10
10
 
11
+ module Nugrant
12
+ def self.setup_i18n()
13
+ I18n.load_path << File.expand_path("locales/en.yml", Nugrant.source_root)
14
+ I18n.reload!
15
+ end
16
+
17
+ def self.source_root
18
+ @source_root ||= Pathname.new(File.expand_path("../../", __FILE__))
19
+ end
20
+ end
21
+
11
22
  if defined?(Vagrant)
23
+ Nugrant.setup_i18n()
24
+
12
25
  case
13
26
  when defined?(Vagrant::Plugin::V2)
14
27
  require 'nugrant/vagrant/v2/plugin'
15
- when Vagrant::VERSION =~ /1\.0\..*/
16
- # Nothing to do, v1 plugins are picked by the vagrant_init.rb file
17
28
  else
18
- abort("You are trying to use Nugrant with an unsupported Vagrant version [#{Vagrant::VERSION}]")
29
+ raise RuntimeError, "Vagrant [#{Vagrant::VERSION}] is not supported by Nugrant."
19
30
  end
20
31
  end
21
-
22
- module Nugrant
23
- end
data/lib/nugrant/bag.rb CHANGED
@@ -1,96 +1,150 @@
1
1
  module Nugrant
2
- class Bag
3
- attr_reader :__elements
2
+ class Bag < Hash
4
3
 
5
- def initialize(elements = nil)
6
- if elements.kind_of?(Bag)
7
- @__elements = elements
4
+ ##
5
+ # Create a new Bag object which holds key/value pairs.
6
+ # The Bag object inherits from the Hash object, the main
7
+ # differences with a normal Hash are indifferent access
8
+ # (symbol or string) and method access (via method call).
9
+ #
10
+ # =| Arguments
11
+ # * `elements`
12
+ # The initial elements the bag should be built with it.'
13
+ # Must be an object responding to `each` and accepting
14
+ # a block with two arguments: `key, value`.]. Defaults to
15
+ # the empty hash.
16
+ #
17
+ # * `options`
18
+ # An options hash where some customization option can be passed.
19
+ # Defaults to an empty hash, see options for specific option default
20
+ # values.
21
+ #
22
+ # =| Options
23
+ # * `:key_error`
24
+ # A callable object receiving a single parameter `key` that is
25
+ # called when a key cannot be found in the Bag. The received key
26
+ # is already converted to a symbol. If the callable does not
27
+ # raise an exception, the result of it's execution is returned.
28
+ # The default value is a callable that throws a KeyError exception.
29
+ #
30
+ def initialize(elements = {}, options = {})
31
+ super()
32
+
33
+ @__key_error = options[:key_error] || Proc.new do |key|
34
+ raise KeyError, "Undefined parameter '#{key}'" if not key?(key)
8
35
  end
9
36
 
10
- __recompute(elements)
37
+ (elements || {}).each do |key, value|
38
+ self[key] = value.kind_of?(Hash) ? Bag.new(value, options) : value
39
+ end
11
40
  end
12
41
 
13
- def [](key)
14
- return __fetch(key)
42
+ def method_missing(method, *args, &block)
43
+ return self[method]
15
44
  end
16
45
 
17
- def method_missing(method, *args, &block)
18
- return __fetch(method)
46
+ ##
47
+ ### Hash Overriden Methods (for string & symbol indifferent access)
48
+ ##
49
+
50
+ def [](input)
51
+ key = __convert_key(input)
52
+ return @__key_error.call(key) if not key?(key)
53
+
54
+ super(key)
19
55
  end
20
56
 
21
- def has?(key)
22
- return @__elements.has_key?(key)
57
+ def []=(input, value)
58
+ super(__convert_key(input), value)
23
59
  end
24
60
 
25
- def empty?()
26
- @__elements.size() <= 0
61
+ def key?(key)
62
+ super(__convert_key(key))
27
63
  end
28
64
 
29
65
  ##
30
- # This method always perform a deep merge and will deep merge
31
- # array scalar values only. This means that we do not merge
32
- # within array themselves.
66
+ # This method first start by converting the `input` parameter
67
+ # into a bag. It will then *deep* merge current values with
68
+ # the new ones coming from the `input`.
33
69
  #
34
- def __merge!(elements)
35
- bag = elements.kind_of?(Bag) ? elements : Bag.new(elements)
36
- return if bag.empty?()
37
-
38
- bag.each do |key, value|
39
- if has?(key)
40
- current = @__elements[key]
41
- if current.kind_of?(Bag) and value.kind_of?(Bag)
42
- current.__merge!(value)
43
- elsif current.kind_of?(Array) and value.kind_of?(Array)
44
- @__elements[key] = current | value
45
- else
46
- @__elements[key] = value
47
- end
48
-
49
- next
50
- end
70
+ # The array merge strategy is by default to replace current
71
+ # values with new ones. You can use option `:array_strategy`
72
+ # to change this default behavior.
73
+ #
74
+ # +Options+
75
+ # * :array_strategy
76
+ # * :replace (Default) => Replace current values by new ones
77
+ # * :extend => Merge current values with new ones
78
+ # * :concat => Append new values to current ones
79
+ #
80
+ def merge!(input, options = {})
81
+ options = {:array_strategy => :replace}.merge(options)
51
82
 
52
- @__elements[key] = value
53
- end
54
- end
83
+ array_strategy = options[:array_strategy]
84
+ input.each do |key, value|
85
+ current = __get(key)
86
+ case
87
+ when current == nil
88
+ self[key] = value
89
+
90
+ when current.kind_of?(Hash) && value.kind_of?(Hash)
91
+ current.merge!(value, options)
55
92
 
56
- def each()
57
- @__elements.each do |key, value|
58
- yield key, value
93
+ when current.kind_of?(Array) && value.kind_of?(Array)
94
+ self[key] = send("__#{array_strategy}_array_merge", current, value)
95
+
96
+ when value != nil
97
+ self[key] = value
98
+ end
59
99
  end
60
100
  end
61
101
 
62
- def __to_hash()
102
+ def to_hash(options = {})
63
103
  return {} if empty?()
64
104
 
65
- hash = {}
66
- each do |key, value|
67
- hash[key.to_sym()] = value.kind_of?(Bag) ? value.__to_hash() : value
68
- end
105
+ use_string_key = options[:use_string_key]
69
106
 
70
- return hash
107
+ Hash[map do |key, value|
108
+ key = use_string_key ? key.to_s() : key
109
+ value = value.kind_of?(Bag) ? value.to_hash(options) : value
110
+
111
+ [key, value]
112
+ end]
71
113
  end
72
114
 
73
- def __recompute(hash = nil)
74
- @__elements = {}
75
- return if hash == nil or not hash.kind_of?(Hash)
115
+ ##
116
+ ### Aliases
117
+ ##
76
118
 
77
- hash.each do |key, value|
78
- if not value.kind_of?(Hash)
79
- @__elements[key.to_sym()] = value
80
- next
81
- end
119
+ alias_method :to_ary, :to_a
82
120
 
83
- # It is a hash, transform it into a bag
84
- @__elements[key.to_sym()] = Bag.new(value)
85
- end
121
+ ##
122
+ ### Private Methods
123
+ ##
124
+
125
+ private
126
+
127
+ def __convert_key(key)
128
+ return key.to_sym() if key.respond_to?(:to_sym)
129
+
130
+ raise ArgumentError, "Key cannot be converted to symbol, current value [#{key}] (#{key.class.name})"
86
131
  end
87
132
 
88
- def __fetch(key)
89
- if not has?(key)
90
- raise KeyError, "Undefined parameter '#{key}'"
91
- end
133
+ def __get(key)
134
+ # Calls Hash method [__convert_key(key)], used internally to retrieve value without raising Undefined parameter
135
+ self.class.superclass.instance_method(:[]).bind(self).call(__convert_key(key))
136
+ end
137
+
138
+ def __concat_array_merge(current_array, new_array)
139
+ current_array + new_array
140
+ end
141
+
142
+ def __extend_array_merge(current_array, new_array)
143
+ current_array | new_array
144
+ end
92
145
 
93
- return @__elements[key]
146
+ def __replace_array_merge(current_array, new_array)
147
+ new_array
94
148
  end
95
149
  end
96
150
  end
@@ -6,36 +6,36 @@ require 'nugrant/bag'
6
6
  module Nugrant
7
7
  module Helper
8
8
  module Bag
9
- def self.read(filepath, format, error_handler = nil)
10
- data = parse_data(filepath, format, error_handler)
9
+ def self.read(filepath, filetype, options = {})
10
+ data = parse_data(filepath, filetype, options)
11
11
 
12
- return Nugrant::Bag.new(data)
12
+ return Nugrant::Bag.new(data, options)
13
13
  end
14
14
 
15
- def self.parse_data(filepath, format, error_handler = nil)
15
+ def self.restricted_keys()
16
+ Nugrant::Bag.instance_methods()
17
+ end
18
+
19
+ def self.parse_data(filepath, filetype, options = {})
16
20
  return if not File.exists?(filepath)
17
21
 
18
- begin
19
- File.open(filepath, "rb") do |file|
20
- parsing_method = "parse_#{format.to_s}"
21
- return send(parsing_method, file.read())
22
- end
23
- rescue => error
24
- if error_handler
25
- # TODO: Implements error handler logic
26
- error_handler.handle("Could not parse the user #{format.to_s} parameters file '#{filepath}': #{error}")
27
- end
22
+ File.open(filepath, "rb") do |file|
23
+ return send("parse_#{filetype}", file)
24
+ end
25
+ rescue => error
26
+ if options[:error_handler]
27
+ options[:error_handler].handle("Could not parse the user #{filetype} parameters file '#{filepath}': #{error}")
28
28
  end
29
29
  end
30
30
 
31
- def self.parse_json(input)
32
- JSON.parse(input)
31
+ def self.parse_json(io)
32
+ MultiJson.load(io.read())
33
33
  end
34
34
 
35
- def self.parse_yaml(input)
36
- YAML::ENGINE.yamler= 'syck' if defined?(YAML::ENGINE)
35
+ def self.parse_yaml(io)
36
+ YAML::ENGINE.yamler = 'syck' if (defined?(Syck) || defined?(YAML::Syck)) && defined?(YAML::ENGINE)
37
37
 
38
- YAML.load(input)
38
+ YAML.load(io.read())
39
39
  end
40
40
  end
41
41
  end
@@ -0,0 +1,208 @@
1
+ require 'shellwords'
2
+
3
+ require 'nugrant/bag'
4
+ require 'nugrant/helper/env/namer'
5
+
6
+ module Nugrant
7
+ module Helper
8
+ module Env
9
+ module Exporter
10
+ @@DEFAULT_AUTOENV_PATH = "./.env"
11
+ @@DEFAULT_SCRIPT_PATH = "./nugrant2env.sh"
12
+
13
+ @@VALID_EXPORTERS = [:autoenv, :script, :terminal]
14
+
15
+ ##
16
+ # Returns true if the exporter name received is a valid
17
+ # valid export, false otherwise.
18
+ #
19
+ # @param exporter The exporter name to check validity
20
+ #
21
+ # @return true if exporter is valid, false otherwise.
22
+ def self.valid?(exporter)
23
+ @@VALID_EXPORTERS.include?(exporter)
24
+ end
25
+
26
+ ##
27
+ # Creates an autoenv script containing the commands that are required
28
+ # to export or unset a bunch of environment variables taken from the
29
+ # bag.
30
+ #
31
+ # @param bag The bag to create the script for.
32
+ #
33
+ # @return (side-effect) Creates a script file containing commands
34
+ # to export or unset environment variables for
35
+ # bag.
36
+ #
37
+ # Options:
38
+ # * :autoenv_path => The path where to write the script, defaults to `./.env`.
39
+ # * :escape_value => If true, escape the value to export (or unset), default to true.
40
+ # * :io => The io where the command should be written, default to nil which create the autoenv on disk.
41
+ # * :namer => The namer used to transform bag segments into variable name, default to Namer::default().
42
+ # * :override => If true, variable a exported even when the override an existing env key, default to true.
43
+ # * :type => The type of command, default to :export.
44
+ #
45
+ def self.autoenv_exporter(bag, options = {})
46
+ io = options[:io] || (File.open(File.expand_path(options[:autoenv_path] || @@DEFAULT_AUTOENV_PATH), "w"))
47
+
48
+ terminal_exporter(bag, options.merge({:io => io}))
49
+ ensure
50
+ io.close() if io
51
+ end
52
+
53
+ ##
54
+ # Creates a bash script containing the commands that are required
55
+ # to export or unset a bunch of environment variables taken from the
56
+ # bag.
57
+ #
58
+ # @param bag The bag to create the script for.
59
+ #
60
+ # @return (side-effect) Creates a script file containing commands
61
+ # to export or unset environment variables for
62
+ # bag.
63
+ #
64
+ # Options:
65
+ # * :escape_value => If true, escape the value to export (or unset), default to true.
66
+ # * :io => The io where the command should be written, default to nil which create the script on disk.
67
+ # * :namer => The namer used to transform bag segments into variable name, default to Namer::default().
68
+ # * :override => If true, variable a exported even when the override an existing env key, default to true.
69
+ # * :script_path => The path where to write the script, defaults to `./nugrant2env.sh`.
70
+ # * :type => The type of command, default to :export.
71
+ #
72
+ def self.script_exporter(bag, options = {})
73
+ io = options[:io] || (File.open(File.expand_path(options[:script_path] || @@DEFAULT_SCRIPT_PATH), "w"))
74
+
75
+ io.puts("#!/bin/env sh")
76
+ io.puts()
77
+
78
+ terminal_exporter(bag, options.merge({:io => io}))
79
+ ensure
80
+ io.close() if io
81
+ end
82
+
83
+ ##
84
+ # Export to terminal the commands that are required
85
+ # to export or unset a bunch of environment variables taken from the
86
+ # bag.
87
+ #
88
+ # @param bag The bag to create the script for.
89
+ #
90
+ # @return (side-effect) Outputs to io the commands generated.
91
+ #
92
+ # Options:
93
+ # * :escape_value => If true, escape the value to export (or unset), default to true.
94
+ # * :io => The io where the command should be displayed, default to $stdout.
95
+ # * :namer => The namer used to transform bag segments into variable name, default to Namer::default().
96
+ # * :override => If true, variable a exported even when the override an existing env key, default to true.
97
+ # * :type => The type of command, default to :export.
98
+ #
99
+ def self.terminal_exporter(bag, options = {})
100
+ io = options[:io] || $stdout
101
+ type = options[:type] || :export
102
+
103
+ export(bag, options) do |key, value|
104
+ io.puts(command(type, key, value, options))
105
+ end
106
+ end
107
+
108
+ ##
109
+ # Generic function to export a bag. This walk the bag,
110
+ # for each element, it creates the key using the namer
111
+ # and then forward the key and value to the block if
112
+ # the variable does not override an existing environment
113
+ # variable or if options :override is set to true.
114
+ #
115
+ # @param bag The bag to export.
116
+ #
117
+ # @return (side-effect) Yields each key and value to a block
118
+ #
119
+ # Options:
120
+ # * :namer => The namer used to transform bag segments into variable name, default to Namer::default().
121
+ # * :override => If true, variable a exported even when the override an existing env key, default to true.
122
+ #
123
+ def self.export(bag, options = {})
124
+ namer = options[:namer] || Env::Namer.default()
125
+ override = options.fetch(:override, true)
126
+
127
+ variables = {}
128
+ walk_bag(bag) do |segments, key, value|
129
+ key = namer.call(segments)
130
+
131
+ variables[key] = value if override or not ENV[key]
132
+ end
133
+
134
+ variables.sort().each do |key, value|
135
+ yield key, value
136
+ end
137
+ end
138
+
139
+ ##
140
+ # Given a key and a value, return a string representation
141
+ # of the command type requested. Available types:
142
+ #
143
+ # * :export => A bash compatible export command
144
+ # * :unset => A bash compatible export command
145
+ #
146
+ def self.command(type, key, value, options = {})
147
+ # TODO: Replace by a map type => function name
148
+ case
149
+ when type == :export
150
+ export_command(key, value, options)
151
+ when type == :unset
152
+ unset_command(key, value, options)
153
+ end
154
+ end
155
+
156
+ ##
157
+ # Returns a string representation of the command
158
+ # that needs to be used on the current platform
159
+ # to export an environment variable.
160
+ #
161
+ # @param key The key of the environment variable to export.
162
+ # It cannot be nil.
163
+ # @param value The value of the environment variable to export
164
+ #
165
+ # @return The export command, as a string
166
+ #
167
+ # Options:
168
+ # * :escape_value (true) => If true, escape the value to export.
169
+ #
170
+ def self.export_command(key, value, options = {})
171
+ value = value.to_s()
172
+ value = Shellwords.escape(value) if options[:escape_value] == nil || options[:escape_value]
173
+
174
+ # TODO: Handle platform differently
175
+ "export #{key}=#{value}"
176
+ end
177
+
178
+ ##
179
+ # Returns a string representation of the command
180
+ # that needs to be used on the current platform
181
+ # to unset an environment variable.
182
+ #
183
+ # @param key The key of the environment variable to export.
184
+ # It cannot be nil.
185
+ #
186
+ # @return The unset command, as a string
187
+ #
188
+ def self.unset_command(key, value, options = {})
189
+ # TODO: Handle platform differently
190
+ "unset #{key}"
191
+ end
192
+
193
+ # FIXME: Move this directly into bag class
194
+ def self.walk_bag(bag, parents = [], &block)
195
+ commands = []
196
+
197
+ bag.each do |key, value|
198
+ segments = parents + [key]
199
+ nested_bag = value.kind_of?(Nugrant::Bag)
200
+
201
+ walk_bag(value, segments, &block) if nested_bag
202
+ yield segments, key, value if not nested_bag
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end