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 +4 -4
- data/.gitignore +1 -0
- data/bin/console +13 -26
- data/bin/rake +3 -0
- data/bin/rspec +3 -0
- data/lib/state_mate.rb +27 -295
- data/lib/state_mate/adapters.rb +6 -0
- data/lib/state_mate/adapters/file.rb +145 -0
- data/lib/state_mate/adapters/git/ignore.rb +85 -0
- data/lib/state_mate/adapters/json.rb +1 -1
- data/lib/state_mate/state_set.rb +316 -0
- data/lib/state_mate/version.rb +1 -1
- data/state_mate.gemspec +2 -2
- metadata +12 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2a117a449ec19a632442d42240486e118f2fa0af
|
4
|
+
data.tar.gz: 2a300da504220c97e5b6eae01162604cf9cd1e29
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4421432fc0952a59e2b59c53940b2a46d9623a172fb01402be037c55ad935dcd2ea3c22625e9251cd048df6e8fe053a24d8f5e6012cf2ab1dcfae9cbc520387d
|
7
|
+
data.tar.gz: 40e024603a6b86775bbda6653bb40134b7edcd77b7a0ed1881c38acf3fc3404f83b638fa93b528f2cb085df55270b2705aeff19e201ebcb0a2a247b8dd4b777b
|
data/.gitignore
CHANGED
data/bin/console
CHANGED
@@ -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 '
|
5
|
+
require 'state_mate'
|
10
6
|
|
11
|
-
using NRSER
|
12
7
|
|
13
|
-
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
data/bin/rake
ADDED
data/bin/rspec
ADDED
data/lib/state_mate.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
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
|
data/lib/state_mate/adapters.rb
CHANGED
@@ -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
|
@@ -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
|
data/lib/state_mate/version.rb
CHANGED
data/state_mate.gemspec
CHANGED
@@ -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.
|
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.
|
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
|
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:
|
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.
|
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.
|
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.
|
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.
|
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.
|
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!
|