state_mate 0.0.9 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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!