state_mate 0.0.9 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5f01f81ae28c991389d0bb58e43f41fb551a690b
4
- data.tar.gz: 0e888f119ce00d45046ec008aa63d43784b0ff52
3
+ metadata.gz: 2a117a449ec19a632442d42240486e118f2fa0af
4
+ data.tar.gz: 2a300da504220c97e5b6eae01162604cf9cd1e29
5
5
  SHA512:
6
- metadata.gz: 3a3d7f7027493edc8aace942eb0cb15888a0145d500252f36e0d05edee3d953f4312fa009fdbb87cf9c6b86c45bee5ba177bdbb4540a3492e0284711b9979e1c
7
- data.tar.gz: 1d4fbf23716276cb3688e7d73165802c20a4f4445cf688bd2ced4a3b64d2a52ec8e0ffacabcb82b9768de76846b3b7978f8b36a3dc0ebaeef59bf20e6e3481e6
6
+ metadata.gz: 4421432fc0952a59e2b59c53940b2a46d9623a172fb01402be037c55ad935dcd2ea3c22625e9251cd048df6e8fe053a24d8f5e6012cf2ab1dcfae9cbc520387d
7
+ data.tar.gz: 40e024603a6b86775bbda6653bb40134b7edcd77b7a0ed1881c38acf3fc3404f83b638fa93b528f2cb085df55270b2705aeff19e201ebcb0a2a247b8dd4b777b
data/.gitignore CHANGED
@@ -20,3 +20,4 @@ tmp
20
20
  *.o
21
21
  *.a
22
22
  mkmf.log
23
+ .qb-playbook.yml
@@ -1,38 +1,25 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'pathname'
4
- require 'fileutils'
5
-
6
3
  require "bundler/setup"
7
- require "state_mate"
8
4
  require 'nrser'
9
- require 'nrser/extras'
5
+ require 'state_mate'
10
6
 
11
- using NRSER
12
7
 
13
- ROOT = NRSER.git_root __FILE__
8
+ require 'nrser/refinements'
9
+ using NRSER
14
10
 
15
- Pathname.glob(ROOT + 'lib' + 'state_mate' + 'adapters' + '*.rb').each do |pn|
16
- name = "state_mate/adapters/#{ pn.basename '.rb' }"
17
- begin
18
- require name
19
- rescue Exception => e
20
- puts NRSER.dedent <<-END
21
- failed to load adapter '#{ name }':
22
-
23
- #{ e.format }
24
-
25
- END
26
- end
27
-
28
- end
29
11
 
30
12
  # You can add fixtures and/or initialization code here to make experimenting
31
13
  # with your gem easier. You can also use a different console, if you like.
32
14
 
33
15
  # (If you use this, don't forget to add pry to your Gemfile!)
34
- # require "pry"
35
- # Pry.start
36
-
37
- require "irb"
38
- IRB.start
16
+ begin
17
+ require "pry"
18
+ rescue LoadError => error
19
+ puts "Failed to load `pry` gem - add it to your Gemfile or edit this file"
20
+
21
+ require "irb"
22
+ IRB.start
23
+ else
24
+ Pry.start
25
+ end
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+
3
+ bundle exec rake "$@"
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+
3
+ bundle exec rspec "$@"
@@ -1,12 +1,30 @@
1
+ # Requirements
2
+ # =======================================================================
3
+
4
+ # Stdlib
5
+ # -----------------------------------------------------------------------
1
6
  require 'set'
2
- require 'nrser'
3
- require 'nrser/refinements'
4
7
 
5
- using NRSER
8
+ # Deps
9
+ # -----------------------------------------------------------------------
6
10
 
11
+ # Project / Package
12
+ # -----------------------------------------------------------------------
7
13
  require "state_mate/version"
8
14
  require "state_mate/error"
9
15
  require "state_mate/adapters"
16
+ require "state_mate/state_set"
17
+
18
+
19
+ # Refinements
20
+ # =======================================================================
21
+
22
+ require 'nrser/refinements'
23
+ using NRSER
24
+
25
+
26
+ # Definitions
27
+ # =======================================================================
10
28
 
11
29
  module StateMate
12
30
  @debug = false
@@ -37,7 +55,7 @@ module StateMate
37
55
  #
38
56
  # - missing/nil/null:
39
57
  # - if the `unset_ok` option evaluates **true**:
40
- # - it will validate as a correct state and no action will be
58
+ # - it will validate as a correct state and no action will be
41
59
  # taken.
42
60
  # - **NOTE: this is the ONLY case where the action succeeds
43
61
  # and the value IS NOT an array afterwards**.
@@ -56,293 +74,7 @@ module StateMate
56
74
  # initializes a value - setting it only if it is missing/nil
57
75
  :init,
58
76
  ]
59
-
60
- class StateSet
61
- attr_accessor :spec
62
- attr_reader :states,
63
- :read_values,
64
- :states_to_change,
65
- :new_values,
66
- :written_states,
67
- :write_error,
68
- :rollback_errors,
69
- :changes
70
-
71
-
72
- State = Struct.new :adapter,
73
- :key,
74
- :directive,
75
- :value,
76
- :options
77
-
78
- def self.from_spec spec
79
- state_set = self.new
80
- state_set.spec = spec
81
-
82
- unless spec.is_a? Hash
83
- raise Error::TypeError.new spec,
84
- "spec must be a Hash of adapter names to states"
85
- end
86
-
87
- spec.each do |adapter_name, states|
88
- adapter = StateMate::Adapters.get adapter_name
89
-
90
- states = case states
91
- when Hash
92
- [states]
93
- when Array
94
- states
95
- else
96
- raise Error::TypeError.new states, <<-BLOCK.unblock
97
- each value of the spec needs to be a single state hash or an
98
- array or state
99
- BLOCK
100
- end
101
-
102
- states.each do |state|
103
- unless spec.is_a? Hash
104
- raise Error::TypeError.new state, "each state needs to be a Hash"
105
- end
106
-
107
- key = nil
108
- directives = []
109
- type_name = nil
110
- unset_when_false = false
111
-
112
- # the :unset_when option can be provided to change the directive to
113
- # :unset when the option's value is true.
114
- #
115
- # this is useful for things that should simply unset the key when
116
- # turned off instead of setting it to false or something.
117
- #
118
- unset_when = nil
119
-
120
- options = state['options'] || {}
121
-
122
- unless options.is_a? Hash
123
- raise TypeError.new binding.erb <<-END
124
- options must be a hash, found <%= options.class %>:
125
-
126
- <%= options.inspect %>
127
-
128
- state:
129
-
130
- <%= state.inspect %>
131
-
132
- END
133
- end
134
-
135
- state.each do |k, v|
136
- # normalize to symbols
137
- k = k.to_sym if k.is_a? String
138
-
139
- if k == :key
140
- key = v
141
- elsif k == :options
142
- # pass, dealt with above
143
- elsif DIRECTIVES.include? k
144
- directives << [k, v]
145
- elsif k == :type
146
- type_name = v
147
- elsif k == :unset_when_false
148
- unset_when_false = v
149
- elsif k == :unset_when
150
- unset_when = StateMate.cast 'bool', v
151
- else
152
- # any other keys are set as options
153
- # this is a little convenience feature that avoids having to
154
- # nest inside an `options` key unless your option conflicts
155
- # with 'key' or a directive.
156
- #
157
- # check for conflicts
158
- if options.key? k
159
- raise ArgumentError.new binding.erb <<-END
160
- top-level state key #{ k.inspect } was also provided in the
161
- options.
162
-
163
- state:
164
-
165
- <%= state.inspect %>
166
-
167
- END
168
- end
169
-
170
- options[k] = v
171
- end
172
- end
173
-
174
- directive, value = case directives.length
175
- when 0
176
- raise "no directive found in #{ state.inspect }"
177
- when 1
178
- directives.first
179
- else
180
- raise "multiple directives found in #{ state.inspect }"
181
- end
182
-
183
- # handle :unset_when_false option, which changes the operation to
184
- # an unset when the *directive value* is explicitly false
185
- if unset_when_false &&
186
- (value === false || ['False', 'false'].include?(value))
187
- directive = :unset
188
- value = nil
189
-
190
- # handle :unset_when, which also changes the operation to :unset
191
- # when the option's value is true.
192
- elsif unset_when
193
- directive = :unset
194
- value = nil
195
-
196
- elsif type_name
197
- value = StateMate.cast type_name, value
198
- end
199
-
200
- state_set.add adapter, key, directive, value, options
201
- end # state.each
202
- end # states.each
203
-
204
- state_set
205
- end # from_spec
206
-
207
- def initialize
208
- @spec = nil
209
- @states = []
210
- @read_values = {}
211
- @states_to_change = []
212
- @new_values = []
213
- @written_states = []
214
- @write_error = nil
215
- # map of states to errors raised when trying to rollback
216
- @rollback_errors = {}
217
- # report of changes made
218
- @changes = {}
219
- end
220
-
221
- def add adapter, key, directive, value, options = {}
222
- @states << State.new(adapter, key, directive, value, options)
223
- end
224
-
225
- def execute
226
- # find out what needs to be changed
227
- @states.each do |state|
228
- # read the current value
229
- read_value = state.adapter.read state.key, state.options
230
-
231
- # store it for use in the actual change
232
- @read_values[state] = read_value
233
-
234
- # the test method is the directive with a '?' appended,
235
- # like `set?` or `array_contains?`
236
- test_method = StateMate.method "#{ state.directive }?"
237
-
238
- # find out if the state is in sync
239
- in_sync = test_method.call state.key,
240
- read_value,
241
- state.value,
242
- state.adapter,
243
- state.options
244
-
245
- # add to the list of changes to be made for states that are
246
- # out of sync
247
- @states_to_change << state unless in_sync
248
- end
249
-
250
- # if everything is in sync, no changes need to be attempted
251
- # reutrn the empty hash of changes
252
- return @changes if @states_to_change.empty?
253
-
254
- # do the change to each in-memory value
255
- # this will raise an excption if the operation can't be done for
256
- # some reason
257
- states_to_change.each do |state|
258
- sync_method = StateMate.method state.directive
259
- # we want to catch any error and report it
260
- begin
261
- new_value = sync_method.call state.key,
262
- @read_values[state],
263
- state.value,
264
- state.options
265
- rescue Exception => e
266
- @new_value_error = e
267
- raise Error::ValueSyncError.new binding.erb <<-BLOCK
268
- an error occured when changing a values:
269
-
270
- <%= @new_value_error.format %>
271
-
272
- no changes were attempted to the system, so there is no rollback
273
- necessary.
274
- BLOCK
275
- end
276
- # change successful, store the new value along-side the state
277
- # for use in the next block
278
- @new_values << [state, new_value]
279
- end
280
-
281
- new_values.each do |state, new_value|
282
- begin
283
- state.adapter.write state.key, new_value, state.options
284
- rescue Exception => e
285
- @write_error = e
286
- rollback
287
- raise Error::WriteError.new binding.erb <<-BLOCK
288
- an error occured when writing new state values:
289
-
290
- <%= @write_error.format %>
291
-
292
- <% if @written_states.empty? %>
293
- the error occured on the first write, so no values were rolled
294
- back.
295
-
296
- <% else %>
297
- <% if @rollback_errors.empty? %>
298
- all values were sucessfully rolled back:
299
-
300
- <% else %>
301
- some values failed to rollback:
302
-
303
- <% end %>
304
-
305
- <% @written_states.each do |state| %>
306
- <% if @rollback_errors[state] %>
307
- <% state.key %>: <% @rollback_errors[state].format.indent(8) %>
308
- <% else %>
309
- <%= state.key %>: rolled back.
310
- <% end %>
311
- <% end %>
312
- <% end %>
313
- BLOCK
314
- else
315
- @written_states << state
316
- end # begin / rescue / else
317
- end # new_values.each
318
-
319
- # ok, we made it. report the changes
320
- new_values_hash = Hash[@new_values]
321
- @written_states.each do |state|
322
- @changes[[state.adapter.name, state.key]] = [@read_values[state], new_values_hash[state]]
323
- end
324
-
325
- @changes
326
- end # execute
327
-
328
- private
329
-
330
- def rollback
331
- # go through the writes that were sucessfully made and try to
332
- # reverse them
333
- @written_states.reverse.each do |state|
334
- # wrap in rescue so that we can record that the rollback failed
335
- # for a value and continue
336
- begin
337
- state.adapter.write state.key, state.value, state.options
338
- rescue Exception => e
339
- # record when and why a rollback fails to include it in the
340
- # exiting exception
341
- @rollback_errors[state] = e
342
- end
343
- end
344
- end # rollback
345
- end # StateSet
77
+
346
78
 
347
79
  # @api dev
348
80
  #
@@ -382,7 +114,7 @@ module StateMate
382
114
  # *pure*
383
115
  #
384
116
  # casts a value to a type, or raises an error if not possible.
385
- # this is useful because ansible in particular likes to pass things
117
+ # this is useful because Ansible in particular likes to pass things
386
118
  # as strings.
387
119
  #
388
120
  # @param type_name [String] the 'name' of the type to cast to.
@@ -436,10 +168,10 @@ module StateMate
436
168
  false
437
169
  when 1, '1', 'True', 'true', 'TRUE'
438
170
  true
439
- else
171
+ else
440
172
  raise ArgumentError.new "can't cast type to boolean: #{ value.inspect }"
441
173
  end
442
- else
174
+ else
443
175
  raise ArgumentError.new "bad type name: #{ type_name.inspect }"
444
176
  end
445
177
  end
@@ -486,7 +218,7 @@ module StateMate
486
218
  when Array
487
219
  # this is just to make the function consistent, so it doesn't add another
488
220
  # copy of value if it's there... in practice StateMate should not
489
- # call {.array_contains} if the value is alreay in the array
221
+ # call {.array_contains} if the value is already in the array
490
222
  # (that's what {.array_contains?} tests for)
491
223
  if current.include? value
492
224
  current
@@ -5,6 +5,12 @@ module StateMate; end
5
5
  module StateMate::Adapters
6
6
  API_METHOD_NAMES = [:read, :write]
7
7
 
8
+ # Default character to split string keys on.
9
+ #
10
+ # @return [String]
11
+ #
12
+ DEFAULT_KEY_SEP = ':'
13
+
8
14
  @@index = {}
9
15
 
10
16
  module IncludeClassMethods
@@ -0,0 +1,145 @@
1
+ # Requirements
2
+ # =======================================================================
3
+
4
+ # Stdlib
5
+ # -----------------------------------------------------------------------
6
+
7
+ # Deps
8
+ # -----------------------------------------------------------------------
9
+
10
+ # Project / Package
11
+ # -----------------------------------------------------------------------
12
+
13
+
14
+ # Refinements
15
+ # =======================================================================
16
+
17
+
18
+ # Declarations
19
+ # =======================================================================
20
+
21
+
22
+ # Definitions
23
+ # =======================================================================
24
+
25
+
26
+ # Abstract base class for adapters whose data is stored in a single file.
27
+ #
28
+ class StateMate::Adapters::File
29
+
30
+ # Constants
31
+ # ======================================================================
32
+
33
+
34
+ # Class Methods
35
+ # ======================================================================
36
+
37
+ # *pure*
38
+ #
39
+ # Parses a key into path segments, the first of which should be the file
40
+ # path.
41
+ #
42
+ # Checks that there is at least one resulting segment and that none of the
43
+ # segments are empty.
44
+ #
45
+ # If `key` is an array, assumes it's already split, and just checks that
46
+ # the segments meet the above criteria, allowing key segments that contain
47
+ # the key separator (which defaults to the
48
+ # {StateMate::Adapters::DEFAULT_KEY_SEP} `:`).
49
+ #
50
+ # @example `:`-separated string key
51
+ # parse_key '/Users/nrser/what/ever.json:x:y:z'
52
+ # # => ['/Users/nrser/what/ever.json', 'x', 'y', 'z']
53
+ #
54
+ # @example Array key with segments containing `:`
55
+ # parse_key ['/Users/nrser/filename:with:colons.json', 'x', 'y']
56
+ # # => ['/Users/nrser/filename:with:colons.json', 'x', 'y']
57
+ #
58
+ # @param key [Array<String>, String] an Array of non-empty Strings or a
59
+ # a String that splits by `:` into an non-empty Array of non-empty
60
+ # Strings.
61
+ #
62
+ # @return [Array<String, Array<String>>] the String domain followed by an
63
+ # array of key segments.
64
+ #
65
+ # @raise [ArgumentError] if the key does not parse into a non-empty list
66
+ # of non-empty strings.
67
+ #
68
+ def self.parse_key key, key_sep = StateMate::Adapters::DEFAULT_KEY_SEP
69
+ strings = case key
70
+ when Array
71
+ key
72
+ when String
73
+ key.split key_sep
74
+ else
75
+ raise TypeError,
76
+ "key must be string or array, not #{ key.inspect }"
77
+ end # case
78
+
79
+ # make sure there is at least one element
80
+ if strings.empty?
81
+ raise ArgumentError,
82
+ "key parsed into empty list: #{ key.inspect }"
83
+ end
84
+
85
+ # check for non-strings, empty domain or key segments
86
+ strings.each do |string|
87
+ if !string.is_a?(String) || string.empty?
88
+ raise ArgumentError.new NRSER.squish <<-END
89
+ all key segments must be non-empty,
90
+ found #{ string.inspect } in key #{ key.inspect }.
91
+ END
92
+ end
93
+ end
94
+
95
+ strings
96
+ end # ::parse_key
97
+
98
+
99
+ # Attributes
100
+ # ======================================================================
101
+
102
+
103
+ # Constructor
104
+ # ======================================================================
105
+
106
+ # Instantiate a new `StateMate::Adapters::File`.
107
+ def initialize
108
+
109
+ end # #initialize
110
+
111
+
112
+ # Instance Methods
113
+ # ======================================================================
114
+
115
+
116
+ # Parse file contents into state structure.
117
+ #
118
+ # @abstract
119
+ #
120
+ # @param [String] file_contents
121
+ # File contents to parse.
122
+ #
123
+ # @return [Hash]
124
+ # @todo Document return value.
125
+ #
126
+ def parse file_contents
127
+ raise NRSER::AbstractMethodError.new self, __method__
128
+ end # #parse
129
+
130
+
131
+
132
+ # @todo Document read method.
133
+ #
134
+ # @param [type] arg_name
135
+ # @todo Add name param description.
136
+ #
137
+ # @return [return_type]
138
+ # @todo Document return value.
139
+ #
140
+ def read key, **options
141
+
142
+ end # #read
143
+
144
+
145
+ end # class StateMate::Adapters::File
@@ -0,0 +1,85 @@
1
+ # Requirements
2
+ # =======================================================================
3
+
4
+ # Stdlib
5
+ # -----------------------------------------------------------------------
6
+
7
+ # Deps
8
+ # -----------------------------------------------------------------------
9
+
10
+ # Project / Package
11
+ # -----------------------------------------------------------------------
12
+
13
+ require 'cmds'
14
+
15
+ require 'state_mate/adapters'
16
+
17
+
18
+ # Declarations
19
+ # =======================================================================
20
+
21
+ module StateMate::Adapters::Git; end
22
+
23
+
24
+ # Definitions
25
+ # =======================================================================
26
+
27
+ # Adapter for working with `.gitingore` files.
28
+ #
29
+ module StateMate::Adapters::Git::Ignore
30
+ include StateMate::Adapters
31
+
32
+ register 'gitignore'
33
+
34
+
35
+ # adapter API call that reads a value from the git global config.
36
+ #
37
+ # @api adapter
38
+ #
39
+ # @param key [String] the key to read
40
+ # @param options [Hash] unused options to conform to adapter API
41
+ #
42
+ # @return [String, nil] the git config value, or nil if it's missing.
43
+ #
44
+ # @raise [SystemCallError] if the key is bad or something else caused the
45
+ # command to fail.
46
+ def self.read key, options = {}
47
+ result = Cmds "git config --global --get %{key}", key: key
48
+
49
+ # if the command succeeded the result is the output
50
+ # (minus trailing newline)
51
+ if result.ok?
52
+ result.out.chomp
53
+
54
+ # if it errored with no output then the key is missing
55
+ elsif result.err == ''
56
+ nil
57
+
58
+ # otherwise, raise an error
59
+ else
60
+ result.assert
61
+ end
62
+ end # ::read
63
+
64
+
65
+ # @api adapter
66
+ #
67
+ # adapter API call that writes a value to the git global config.
68
+ #
69
+ # @param key [String] the key to write
70
+ # @param value [String] the value to write
71
+ # @param options [Hash] unused options to conform to adapter API
72
+ #
73
+ # @return nil
74
+ #
75
+ def self.write key, value, options = {}
76
+ # decide to add or replace based on if the key has a value
77
+ action = read(key, options).nil? ? '--add' : '--replace'
78
+
79
+ result = Cmds! "git config --global #{ action } %{key} %{value}",
80
+ key: key,
81
+ value: value
82
+
83
+ nil
84
+ end # ::write
85
+ end # GitConfig
@@ -8,7 +8,7 @@ module StateMate::Adapters::JSON
8
8
  register 'json'
9
9
 
10
10
  def self.parse_key key
11
- # use the same key seperation as Defaults
11
+ # use the same key separation as Defaults
12
12
  StateMate::Adapters::Defaults.parse_key key
13
13
  end
14
14
 
@@ -0,0 +1,316 @@
1
+ # Requirements
2
+ # =======================================================================
3
+
4
+ # Stdlib
5
+ # -----------------------------------------------------------------------
6
+
7
+ # Deps
8
+ # -----------------------------------------------------------------------
9
+
10
+ # Project / Package
11
+ # -----------------------------------------------------------------------
12
+
13
+
14
+ # Refinements
15
+ # =======================================================================
16
+
17
+ require 'nrser/refinements'
18
+ using NRSER
19
+
20
+
21
+ # Declarations
22
+ # =======================================================================
23
+
24
+ module StateMate; end
25
+
26
+
27
+ # Definitions
28
+ # =======================================================================
29
+
30
+ class StateMate::StateSet
31
+ attr_accessor :spec
32
+ attr_reader :states,
33
+ :read_values,
34
+ :states_to_change,
35
+ :new_values,
36
+ :written_states,
37
+ :write_error,
38
+ :rollback_errors,
39
+ :changes
40
+
41
+
42
+ State = Struct.new :adapter,
43
+ :key,
44
+ :directive,
45
+ :value,
46
+ :options
47
+
48
+ def self.from_spec spec
49
+ state_set = self.new
50
+ state_set.spec = spec
51
+
52
+ unless spec.is_a? Hash
53
+ raise StateMate::Error::TypeError.new spec,
54
+ "spec must be a Hash of adapter names to states"
55
+ end
56
+
57
+ spec.each do |adapter_name, states|
58
+ adapter = StateMate::Adapters.get adapter_name
59
+
60
+ states = case states
61
+ when Hash
62
+ [states]
63
+ when Array
64
+ states
65
+ else
66
+ raise StateMate::Error::TypeError.new states, <<-BLOCK.unblock
67
+ each value of the spec needs to be a single state hash or an
68
+ array or state
69
+ BLOCK
70
+ end
71
+
72
+ states.each do |state|
73
+ unless spec.is_a? Hash
74
+ raise StateMate::Error::TypeError.new state,
75
+ "each state needs to be a Hash"
76
+ end
77
+
78
+ key = nil
79
+ directives = []
80
+ type_name = nil
81
+ unset_when_false = false
82
+
83
+ # the :unset_when option can be provided to change the directive to
84
+ # :unset when the option's value is true.
85
+ #
86
+ # this is useful for things that should simply unset the key when
87
+ # turned off instead of setting it to false or something.
88
+ #
89
+ unset_when = nil
90
+
91
+ options = state['options'] || {}
92
+
93
+ unless options.is_a? Hash
94
+ raise TypeError.new binding.erb <<-END
95
+ options must be a hash, found <%= options.class %>:
96
+
97
+ <%= options.inspect %>
98
+
99
+ state:
100
+
101
+ <%= state.inspect %>
102
+
103
+ END
104
+ end
105
+
106
+ state.each do |k, v|
107
+ # normalize to symbols
108
+ k = k.to_sym if k.is_a? String
109
+
110
+ if k == :key
111
+ key = v
112
+ elsif k == :options
113
+ # pass, dealt with above
114
+ elsif StateMate::DIRECTIVES.include? k
115
+ directives << [k, v]
116
+ elsif k == :type
117
+ type_name = v
118
+ elsif k == :unset_when_false
119
+ unset_when_false = v
120
+ elsif k == :unset_when
121
+ unset_when = StateMate.cast 'bool', v
122
+ else
123
+ # any other keys are set as options
124
+ # this is a little convenience feature that avoids having to
125
+ # nest inside an `options` key unless your option conflicts
126
+ # with 'key' or a directive.
127
+ #
128
+ # check for conflicts
129
+ if options.key? k
130
+ raise ArgumentError.new binding.erb <<-END
131
+ top-level state key #{ k.inspect } was also provided in the
132
+ options.
133
+
134
+ state:
135
+
136
+ <%= state.inspect %>
137
+
138
+ END
139
+ end
140
+
141
+ options[k] = v
142
+ end
143
+ end
144
+
145
+ directive, value = case directives.length
146
+ when 0
147
+ raise "no directive found in #{ state.inspect }"
148
+ when 1
149
+ directives.first
150
+ else
151
+ raise "multiple directives found in #{ state.inspect }"
152
+ end
153
+
154
+ # handle :unset_when_false option, which changes the operation to
155
+ # an unset when the *directive value* is explicitly false
156
+ if unset_when_false &&
157
+ (value === false || ['False', 'false'].include?(value))
158
+ directive = :unset
159
+ value = nil
160
+
161
+ # handle :unset_when, which also changes the operation to :unset
162
+ # when the option's value is true.
163
+ elsif unset_when
164
+ directive = :unset
165
+ value = nil
166
+
167
+ elsif type_name
168
+ value = StateMate.cast type_name, value
169
+ end
170
+
171
+ state_set.add adapter, key, directive, value, options
172
+ end # state.each
173
+ end # states.each
174
+
175
+ state_set
176
+ end # from_spec
177
+
178
+ def initialize
179
+ @spec = nil
180
+ @states = []
181
+ @read_values = {}
182
+ @states_to_change = []
183
+ @new_values = []
184
+ @written_states = []
185
+ @write_error = nil
186
+ # map of states to errors raised when trying to rollback
187
+ @rollback_errors = {}
188
+ # report of changes made
189
+ @changes = {}
190
+ end
191
+
192
+ def add adapter, key, directive, value, options = {}
193
+ @states << State.new(adapter, key, directive, value, options)
194
+ end
195
+
196
+ def execute
197
+ # find out what needs to be changed
198
+ @states.each do |state|
199
+ # read the current value
200
+ read_value = state.adapter.read state.key, state.options
201
+
202
+ # store it for use in the actual change
203
+ @read_values[state] = read_value
204
+
205
+ # the test method is the directive with a '?' appended,
206
+ # like `set?` or `array_contains?`
207
+ test_method = StateMate.method "#{ state.directive }?"
208
+
209
+ # find out if the state is in sync
210
+ in_sync = test_method.call state.key,
211
+ read_value,
212
+ state.value,
213
+ state.adapter,
214
+ state.options
215
+
216
+ # add to the list of changes to be made for states that are
217
+ # out of sync
218
+ @states_to_change << state unless in_sync
219
+ end
220
+
221
+ # if everything is in sync, no changes need to be attempted
222
+ # reutrn the empty hash of changes
223
+ return @changes if @states_to_change.empty?
224
+
225
+ # do the change to each in-memory value
226
+ # this will raise an excption if the operation can't be done for
227
+ # some reason
228
+ states_to_change.each do |state|
229
+ sync_method = StateMate.method state.directive
230
+ # we want to catch any error and report it
231
+ begin
232
+ new_value = sync_method.call state.key,
233
+ @read_values[state],
234
+ state.value,
235
+ state.options
236
+ rescue Exception => e
237
+ @new_value_error = e
238
+ raise StateMate::Error::ValueSyncError.new binding.erb <<-BLOCK
239
+ an error occured when changing a values:
240
+
241
+ <%= @new_value_error.format %>
242
+
243
+ no changes were attempted to the system, so there is no rollback
244
+ necessary.
245
+ BLOCK
246
+ end
247
+ # change successful, store the new value along-side the state
248
+ # for use in the next block
249
+ @new_values << [state, new_value]
250
+ end
251
+
252
+ new_values.each do |state, new_value|
253
+ begin
254
+ state.adapter.write state.key, new_value, state.options
255
+ rescue Exception => e
256
+ @write_error = e
257
+ rollback
258
+ raise StateMate::Error::WriteError.new binding.erb <<-BLOCK
259
+ an error occured when writing new state values:
260
+
261
+ <%= @write_error.format %>
262
+
263
+ <% if @written_states.empty? %>
264
+ the error occured on the first write, so no values were rolled
265
+ back.
266
+
267
+ <% else %>
268
+ <% if @rollback_errors.empty? %>
269
+ all values were sucessfully rolled back:
270
+
271
+ <% else %>
272
+ some values failed to rollback:
273
+
274
+ <% end %>
275
+
276
+ <% @written_states.each do |state| %>
277
+ <% if @rollback_errors[state] %>
278
+ <% state.key %>: <% @rollback_errors[state].format.indent(8) %>
279
+ <% else %>
280
+ <%= state.key %>: rolled back.
281
+ <% end %>
282
+ <% end %>
283
+ <% end %>
284
+ BLOCK
285
+ else
286
+ @written_states << state
287
+ end # begin / rescue / else
288
+ end # new_values.each
289
+
290
+ # ok, we made it. report the changes
291
+ new_values_hash = Hash[@new_values]
292
+ @written_states.each do |state|
293
+ @changes[[state.adapter.name, state.key]] = [@read_values[state], new_values_hash[state]]
294
+ end
295
+
296
+ @changes
297
+ end # execute
298
+
299
+ private
300
+
301
+ def rollback
302
+ # go through the writes that were successfully made and try to
303
+ # reverse them
304
+ @written_states.reverse.each do |state|
305
+ # wrap in rescue so that we can record that the rollback failed
306
+ # for a value and continue
307
+ begin
308
+ state.adapter.write state.key, state.value, state.options
309
+ rescue Exception => e
310
+ # record when and why a rollback fails to include it in the
311
+ # exiting exception
312
+ @rollback_errors[state] = e
313
+ end
314
+ end
315
+ end # rollback
316
+ end # class StateMate::StateSet
@@ -1,3 +1,3 @@
1
1
  module StateMate
2
- VERSION = "0.0.9"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -28,8 +28,8 @@ END
28
28
  spec.add_development_dependency "redcarpet"
29
29
  spec.add_development_dependency "nrser-extras"
30
30
 
31
- spec.add_dependency 'nrser', '~> 0.0', '>= 0.0.13'
31
+ spec.add_dependency 'nrser', '~> 0.0', '>= 0.0.29'
32
32
  spec.add_dependency 'CFPropertyList', '~> 2.3'
33
- spec.add_dependency 'cmds', '~> 0.0', '>= 0.0.9'
33
+ spec.add_dependency 'cmds', '~> 0.0', '>= 0.2.4'
34
34
  spec.add_dependency 'diffable_yaml', '~> 0.0', '>= 0.0.2'
35
35
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: state_mate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - nrser
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-11-27 00:00:00.000000000 Z
11
+ date: 2018-01-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -103,7 +103,7 @@ dependencies:
103
103
  version: '0.0'
104
104
  - - ">="
105
105
  - !ruby/object:Gem::Version
106
- version: 0.0.13
106
+ version: 0.0.29
107
107
  type: :runtime
108
108
  prerelease: false
109
109
  version_requirements: !ruby/object:Gem::Requirement
@@ -113,7 +113,7 @@ dependencies:
113
113
  version: '0.0'
114
114
  - - ">="
115
115
  - !ruby/object:Gem::Version
116
- version: 0.0.13
116
+ version: 0.0.29
117
117
  - !ruby/object:Gem::Dependency
118
118
  name: CFPropertyList
119
119
  requirement: !ruby/object:Gem::Requirement
@@ -137,7 +137,7 @@ dependencies:
137
137
  version: '0.0'
138
138
  - - ">="
139
139
  - !ruby/object:Gem::Version
140
- version: 0.0.9
140
+ version: 0.2.4
141
141
  type: :runtime
142
142
  prerelease: false
143
143
  version_requirements: !ruby/object:Gem::Requirement
@@ -147,7 +147,7 @@ dependencies:
147
147
  version: '0.0'
148
148
  - - ">="
149
149
  - !ruby/object:Gem::Version
150
- version: 0.0.9
150
+ version: 0.2.4
151
151
  - !ruby/object:Gem::Dependency
152
152
  name: diffable_yaml
153
153
  requirement: !ruby/object:Gem::Requirement
@@ -185,9 +185,13 @@ files:
185
185
  - README.md
186
186
  - Rakefile
187
187
  - bin/console
188
+ - bin/rake
189
+ - bin/rspec
188
190
  - lib/state_mate.rb
189
191
  - lib/state_mate/adapters.rb
190
192
  - lib/state_mate/adapters/defaults.rb
193
+ - lib/state_mate/adapters/file.rb
194
+ - lib/state_mate/adapters/git/ignore.rb
191
195
  - lib/state_mate/adapters/git_config.rb
192
196
  - lib/state_mate/adapters/json.rb
193
197
  - lib/state_mate/adapters/launchd.rb
@@ -197,6 +201,7 @@ files:
197
201
  - lib/state_mate/adapters/time_machine.rb
198
202
  - lib/state_mate/adapters/yaml.rb
199
203
  - lib/state_mate/error.rb
204
+ - lib/state_mate/state_set.rb
200
205
  - lib/state_mate/version.rb
201
206
  - notes/state-set-steps.md
202
207
  - state_mate.gemspec
@@ -220,7 +225,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
220
225
  version: '0'
221
226
  requirements: []
222
227
  rubyforge_project:
223
- rubygems_version: 2.6.4
228
+ rubygems_version: 2.5.2
224
229
  signing_key:
225
230
  specification_version: 4
226
231
  summary: i heard it's meant to help you with your state, mate!