nrser 0.1.1 → 0.1.2

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: e2f2d4d8bf9243e750dd94962a0331985b7ec0ca
4
- data.tar.gz: 55ae615d5733b4e183052cbe0afdc5cca39fd172
3
+ metadata.gz: 736ee433c6078e250b89ebbc65455ff157888b6e
4
+ data.tar.gz: a99e836e91a873ea4b06767376a6c38e30c2fe5a
5
5
  SHA512:
6
- metadata.gz: 740ca5dd0ada05a5adb259c13f1e2ca5150308b26686f0ffa8d63e399ad748d5c36beea986623bea9884a1fdcdd690f4bce7d03e3b8cd124b615f3f99b11b860
7
- data.tar.gz: 43421a77de4da50eb540cc1055e7a732f47b4a058af81898a63da68737b8a4bbe3c8d715d021d9d325580c8d623ffd8d2919fe39a9dd52826a8b458441c71a83
6
+ metadata.gz: ffed557cf2652d5d9b9af71c4eb65cda37a0a165cacb7f419d9e6717cbb810ab224e3a6710e6e762c72948707e7424180faafd6acd2ab3a310aa2a326422d794
7
+ data.tar.gz: 3b5307fc1dca415cb54a56ed32799b472c625c8a8d33d2dcef98e74e6ae85e21916cf2df5a211de3c4251c5e5828e5cb842942cf490cd10da8c5ad535ad8693b
@@ -0,0 +1,325 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Requirements
4
+ # =======================================================================
5
+
6
+ # Stdlib
7
+ # -----------------------------------------------------------------------
8
+
9
+ # Deps
10
+ # -----------------------------------------------------------------------
11
+
12
+ # Project / Package
13
+ # -----------------------------------------------------------------------
14
+
15
+
16
+ # Refinements
17
+ # =======================================================================
18
+
19
+ using NRSER
20
+
21
+
22
+ # Declarations
23
+ # =======================================================================
24
+
25
+
26
+ # Definitions
27
+ # =======================================================================
28
+
29
+ # @todo document NRSER::Env::Path class.
30
+ class NRSER::Env::Path
31
+ include Enumerable
32
+ include NRSER::Ext::Enumerable
33
+
34
+ # Constants
35
+ # ======================================================================
36
+
37
+ # Character used to separate path entries in string format.
38
+ #
39
+ # @return [String]
40
+ #
41
+ SEPARATOR = ':'
42
+
43
+
44
+ # Class Methods
45
+ # ======================================================================
46
+
47
+ # @todo Document normalize method.
48
+ #
49
+ # @param [nil | String | #each_index] source
50
+ # Path source.
51
+ #
52
+ # @return [return_type]
53
+ # @todo Document return value.
54
+ #
55
+ def self.normalize source
56
+ paths = if source.nil?
57
+ []
58
+
59
+ elsif source.is_a?( String )
60
+ source.to_s.split SEPARATOR
61
+
62
+ elsif NRSER.array_like?( source )
63
+ # Flatten it if supported
64
+ source = source.flatten if source.respond_to?( :flatten )
65
+
66
+ # Stringify each segment, split them and concat results
67
+ source.flat_map { |entry| entry.to_s.split SEPARATOR }
68
+
69
+ else
70
+ raise ArgumentError.new binding.erb <<-END
71
+ Expected a string or an "array-like" source, found:
72
+
73
+ <%= source.pretty_inspect %>
74
+
75
+ END
76
+ end
77
+
78
+ Hamster::Vector.new paths.
79
+ # Get rid of empty paths
80
+ reject( &:empty? ).
81
+ # Get rid of duplicates
82
+ uniq.
83
+ # Freeze all the strings
84
+ map( &:freeze )
85
+ end # .normalize
86
+
87
+
88
+ # See if a `path` matches any of `patterns`.
89
+ #
90
+ # Short-circuits as soon as a match is found (so patterns may not all be
91
+ # tested).
92
+ #
93
+ # @param [String] path
94
+ # Path to test against.
95
+ #
96
+ # @param [Array<String | Proc<String=>Boolean> | Regexp>] *patterns
97
+ # Patterns to test:
98
+ #
99
+ # - `String` - test if it and `path` are equal (`==`)
100
+ #
101
+ # - `Proc<String=>Boolean>` - call with `path` and evaluate result as
102
+ # Boolean.
103
+ #
104
+ # - `Regexp` - test if it matches `path` (`=~`)
105
+ #
106
+ # @return [Boolean]
107
+ # `true` if *any* of `patterns` match `path`.
108
+ #
109
+ def self.matches_pattern? path, *patterns
110
+ patterns.any? do |pattern|
111
+ case pattern
112
+ when String
113
+ path == pattern
114
+ when Proc
115
+ pattern.call path
116
+ when Regexp
117
+ path =~ pattern
118
+ else
119
+ raise TypeError.new binding.erb <<-END
120
+ Each `*patterns` arg should be String, Proc or Regexp, found:
121
+
122
+ <%= pattern.pretty_inspect %>
123
+
124
+ END
125
+ end
126
+ end
127
+ end # .matches_pattern?
128
+
129
+
130
+ # @todo Document from_env method.
131
+ #
132
+ # @param [type] arg_name
133
+ # @todo Add name param description.
134
+ #
135
+ # @return [return_type]
136
+ # @todo Document return value.
137
+ #
138
+ def self.from_ENV env_key
139
+ new ENV[env_key.to_s], env_key: env_key
140
+ end # .from_env
141
+
142
+
143
+ # Attributes
144
+ # ======================================================================
145
+
146
+ # Key for the value in `ENV` that the object represents. This is set when
147
+ # loading from `ENV` and used to know where to write when saving back to it.
148
+ #
149
+ # @return [nil | String]
150
+ #
151
+ attr_reader :env_key
152
+
153
+
154
+ # The object that was originally provided at construction.
155
+ #
156
+ # @return [nil | String | #each_index]
157
+ #
158
+ attr_reader :source
159
+
160
+
161
+ # The actual internal list of paths.
162
+ #
163
+ # @return [Hamster::Vector<String>]
164
+ #
165
+ attr_reader :value
166
+
167
+
168
+ # Constructor
169
+ # ======================================================================
170
+
171
+ # Instantiate a new `NRSER::Env::Path`.
172
+ def initialize source, env_key: nil
173
+ @env_key = env_key.to_s.freeze
174
+ @source = source.dup.freeze
175
+ @value = self.class.normalize source
176
+ end # #initialize
177
+
178
+
179
+ # Instance Methods
180
+ # ======================================================================
181
+
182
+ #
183
+ # @param source (see .normalize)
184
+ # @return [self]
185
+ #
186
+ def prepend source
187
+ # Normalize the new source to a flat array of strings
188
+ paths = self.class.normalize source
189
+
190
+ # The new value is the normalized paths followed by the current paths
191
+ # with the new ones removed (de-duplication)
192
+ @value = (paths + @value).uniq
193
+
194
+ # Return self for chain-ability
195
+ self
196
+ end
197
+
198
+ alias_method :unshift, :prepend
199
+ alias_method :>>, :prepend
200
+
201
+
202
+ #
203
+ # @param source (see .normalize)
204
+ # @return [self]
205
+ #
206
+ def append source
207
+ # Normalize the new source to a flat array of strings
208
+ paths = self.class.normalize source
209
+
210
+ # The new value is the current paths with the new paths appended, with
211
+ # any paths in the current path removed from the new ones (de-duplication)
212
+ @value = (@value + paths).uniq
213
+
214
+ # Return self for chain-ability
215
+ self
216
+ end
217
+
218
+ alias_method :push, :prepend
219
+ alias_method :<<, :prepend
220
+
221
+
222
+ def insert source, before: nil, after: nil
223
+ paths = self.class.normalize source
224
+
225
+ before_index = if before
226
+ @value.find_index do |path|
227
+ self.class.matches_pattern? path, *before
228
+ end
229
+ end
230
+
231
+ after_index = if after
232
+ index = @value.rindex { |path| self.class.matches_pattern? path, *after }
233
+ index += 1 if index
234
+ end
235
+
236
+ insert_index = if after_index && before_index
237
+ # Make sure the conditions don't conflict with each other
238
+ if after_index > before_index
239
+ raise "Conflicting bounds!"
240
+ end
241
+
242
+ # Insert as far "down" the path as allowed
243
+ [before_index, after_index].max
244
+ else
245
+ # Use the one that is not `nil`, or insert at the end if they both are
246
+ before_index || after_index || @value.length
247
+ end
248
+
249
+ @value = @value.insert( insert_index, *paths ).uniq
250
+
251
+ self
252
+ end
253
+
254
+
255
+ # Language Interop
256
+ # ============================================================================
257
+
258
+ # Support for {Enumerable} mixin. Yields each path in order.
259
+ #
260
+ # Specifically, proxies to {Hamster::Vector#each}
261
+ #
262
+ # @see http://www.rubydoc.info/gems/hamster/Hamster/Vector#each-instance_method
263
+ #
264
+ # @param [nil | Proc<(String)=>*>] &block
265
+ # When present, block will be called once for each string path in this
266
+ # object. First path is most prominent, down to least last.
267
+ #
268
+ # @return [self]
269
+ # When `&block` is provided.
270
+ #
271
+ # @return [Enumerator]
272
+ # When `&block` is omitted.
273
+ #
274
+ def each &block
275
+ if block
276
+ # Proxy for yielding
277
+ @value.each &block
278
+
279
+ # Return self for chain-ability
280
+ self
281
+ else
282
+ # Return the {Enumerator} from the vector
283
+ @value.each
284
+ end
285
+ end # #each
286
+
287
+
288
+ # The paths joined with ':'.
289
+ #
290
+ # @return [String]
291
+ #
292
+ def to_s
293
+ @value.join SEPARATOR
294
+ end
295
+
296
+
297
+ # The string paths in a new stdlib `Array`. Mutating this array will have no
298
+ # effect on the {NRSER::Env::Path} data.
299
+ #
300
+ # @return [Array<String>]
301
+ #
302
+ def to_a
303
+ @value.to_a
304
+ end
305
+
306
+ protected
307
+ # ========================================================================
308
+
309
+ # Internal method to remove paths that have already been normalized.
310
+ #
311
+ # @param [type] arg_name
312
+ # @todo Add name param description.
313
+ #
314
+ # @return [return_type]
315
+ # @todo Document return value.
316
+ #
317
+ def remove_paths paths
318
+ # method body...
319
+ end # #remove_paths
320
+
321
+
322
+ # end protected
323
+
324
+
325
+ end # class NRSER::Env::Path
data/lib/nrser/env.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Declarations
4
+ # =======================================================================
5
+
6
+ module NRSER::Env; end
7
+
8
+
9
+ # Post-Processing
10
+ # =======================================================================
11
+
12
+ require_relative './env/path'
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when we expected `#count` to be something it's not.
4
+ #
5
+ # Extends {NRSER::ValueError}, and the {#value} must be the instance that
6
+ #
7
+ class NRSER::AttrError < NRSER::ValueError
8
+
9
+ # Name of attribute that has invalid value.
10
+ #
11
+ # @return [Symbol]
12
+ #
13
+ attr_reader :symbol
14
+
15
+
16
+ # Actual invalid value of the subject's attribute.
17
+ #
18
+ # If not provided at construction, will be retrieved by sending {#symbol}
19
+ # to {#subject} in {#initialize}.
20
+ #
21
+ # @return [Object]
22
+ #
23
+ attr_reader :actual
24
+
25
+
26
+ # An optional expected value to use in {#build_message}.
27
+ #
28
+ # @return [Object]
29
+ #
30
+ attr_reader :expected
31
+
32
+
33
+ # @param [Object] subject:
34
+ # The object that has the invalid attribute value.
35
+ #
36
+ def initialize message = nil, symbol:, subject:, **options
37
+ @symbol = symbol.to_sym
38
+
39
+ @actual = if options.key?( :actual )
40
+ options[:actual]
41
+ else
42
+ value.send @symbol
43
+ end
44
+
45
+ @has_expected = options.key? :expected
46
+ @expected = options[:expected]
47
+
48
+ super message, subject: subject
49
+ end
50
+
51
+ def has_expected?
52
+ @has_expected
53
+ end
54
+
55
+ def build_message
56
+ headline = if has_expected?
57
+ "#{ subject.class } object has invalid ##{ symbol }: " +
58
+ "expected #{ expected }, found #{ actual }"
59
+ else
60
+ "#{ subject.class } object has invalid ##{ symbol } (found #{ actual })"
61
+ end
62
+
63
+ binding.erb <<-END
64
+ <%= headline %>
65
+
66
+ Subject:
67
+
68
+ <%= subject.pretty_inspect %>
69
+
70
+ END
71
+ end
72
+
73
+ end # class NRSER::CountError
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when we expected `#count` to be something it's not.
4
+ #
5
+ # Extends {NRSER::ValueError}, and the {#value} must be the instance that
6
+ #
7
+ class NRSER::CountError < NRSER::AttrError
8
+ def initialize message = nil, subject:, expected:, count: nil
9
+ super message,
10
+ subject: subject,
11
+ symbol: :count,
12
+ actual: (count || subject.count),
13
+ expected: expected
14
+ end
15
+
16
+ def count
17
+ actual
18
+ end
19
+ end # class NRSER::CountError
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Requirements
4
+ # =======================================================================
5
+
6
+ # Stdlib
7
+ # -----------------------------------------------------------------------
8
+
9
+ # Deps
10
+ # -----------------------------------------------------------------------
11
+
12
+ # Project / Package
13
+ # -----------------------------------------------------------------------
14
+
15
+
16
+ # Definitions
17
+ # =======================================================================
18
+
19
+ # A mixin for {Exception} and utilities to make errors nicer.
20
+ #
21
+ module NRSER::NicerError
22
+
23
+
24
+ # TODO document `context` attribute.
25
+ #
26
+ # @return [Hash<Symbol, V>]
27
+ #
28
+ attr_reader :context
29
+
30
+
31
+ # @todo Document render_message method.
32
+ #
33
+ # @param [type] arg_name
34
+ # @todo Add name param description.
35
+ #
36
+ # @return [return_type]
37
+ # @todo Document return value.
38
+ #
39
+ def self.render_message message,
40
+ context: {},
41
+ add_context: true,
42
+ &get_extended_message
43
+
44
+ # 1. Figure out if `message` is just the "short message" (single line)
45
+ # or if it's "old-style" where it's just the whole thing.
46
+
47
+ message_lines = message.lines
48
+
49
+ if message_lines.length > 1
50
+ message_lines = NRSER.dedent message_lines, return_lines: true
51
+ short_message = message_lines.first.chomp
52
+ extended_message_lines = message_lines.rest
53
+ else
54
+ short_message = message
55
+ extended_message_lines = nil
56
+ end
57
+
58
+ # Ok, `short_message` is a single line string
59
+ # `extended_message_lines` is an array of string lines or `nil`
60
+
61
+ # 2.
62
+
63
+ if get_extended_message
64
+ got_extended_message = if get_extended_message.arity == 0
65
+ get_extended_message.call
66
+ else
67
+ get_extended_message.call context
68
+ end
69
+
70
+ got_extended_message_lines = NRSER.dedent \
71
+ got_extended_message,
72
+ return_lines: true
73
+
74
+ if extended_message_lines
75
+ extended_message_lines += [
76
+ "\n",
77
+ *got_extended_message_lines
78
+ ]
79
+ else
80
+ extended_message_lines = got_extended_message_lines
81
+ end
82
+ end
83
+
84
+ if add_context
85
+ extended_message_lines += [
86
+ "Context:\n",
87
+ "\n"
88
+ "\n <%= %>"
89
+ ]
90
+ end
91
+
92
+ end # .render_message
93
+
94
+
95
+ # @todo Document initialize method.
96
+ #
97
+ # @param [type] arg_name
98
+ # @todo Add name param description.
99
+ #
100
+ # @return [return_type]
101
+ # @todo Document return value.
102
+ #
103
+ def initialize message,
104
+ context: {},
105
+ add_context: true,
106
+ &
107
+ @context = context
108
+ end # #initialize
109
+
110
+ end # module NRSER::NicerError
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ # Raised when there is a problem with a *value* that does not fall into one
5
+ # of the other built-in exception categories (non-exhaustive list):
6
+ #
7
+ # 1. It's the wrong type (TypeError)
8
+ # 2. It's an argument (ArgumentError)
9
+ #
10
+ # The invalid value is attached to the error as an instance value so that
11
+ # rescuers up the stack can do more intelligent things with it if need be.
12
+ #
13
+ class NRSER::ValueError < StandardError
14
+
15
+ # The invalid value.
16
+ #
17
+ # @return [Object]
18
+ #
19
+ attr_reader :subject
20
+
21
+
22
+ def initialize message = nil, subject:
23
+ @subject = subject
24
+
25
+ # If we received `nil` for the message, call {#build_message} to get it.
26
+ #
27
+ # This provides a "hook" to assemble the message at the last possible
28
+ # moment before it needs to go up to {StandardError#initialize}, allowing
29
+ # {#build_message} to work with an otherwise fully-initialized instance.
30
+ #
31
+ # Of course, {NRSER::ValueError#build_message}
32
+ # throws {NotImplementedError} since it doesn't really have enough
33
+ # knowledge to build anything useful (we're going for useful errors,
34
+ # "Value #{ value } is invalid" does not suffice).
35
+ #
36
+ message = build_message if message.nil?
37
+
38
+ super message
39
+ end
40
+
41
+
42
+ # Build the error message when none is provided to `#initialize`.
43
+ #
44
+ # When no `message` (or `nil`) is provided to {NRSER::ValueError.initialize}
45
+ # it will call this method to get the error message just before it needs it
46
+ # to call up to {StandardError#initialize} (via `super message`).
47
+ #
48
+ # This allows {NRSER::ValueError} subclasses that are able to build a useful
49
+ # default message or would like to augment the user-provided one to do so
50
+ # at the last possible moment before it's needed, letting them work with an
51
+ # otherwise fully-initialized instance.
52
+ #
53
+ # Hence a subclass several generations down from {NRSER::ValueError} can
54
+ # use values initialized in all the constructors in-between, avoiding a lot
55
+ # of headache.
56
+ #
57
+ # This implementation always raises {NRSER::AbstractMethodError} because
58
+ # {NRSER::ValueError} does not have enough information to construct a useful
59
+ # message.
60
+ #
61
+ # @return [String]
62
+ # Implementations must return the message string for
63
+ # {StadardError#initialize}.
64
+ #
65
+ # @raise [NRSER::AbstractMethodError]
66
+ # Must be implemented by subclasses if they wish to use message building.
67
+ #
68
+ def build_message
69
+ raise NRSER::AbstractMethodError.new( self, __method__ )
70
+ end
71
+
72
+ end
data/lib/nrser/errors.rb CHANGED
@@ -1,3 +1,20 @@
1
+
2
+ # Requirements
3
+ # =======================================================================
4
+
5
+ # Stdlib
6
+ # -----------------------------------------------------------------------
7
+
8
+ # Deps
9
+ # -----------------------------------------------------------------------
10
+
11
+ # Project / Package
12
+ # -----------------------------------------------------------------------
13
+ require_relative './errors/value_error'
14
+ require_relative './errors/attr_error'
15
+ require_relative './errors/count_error'
16
+
17
+
1
18
  module NRSER
2
19
 
3
20
  # Indicates some piece of application state is in conflict with the attempted
@@ -5,7 +22,7 @@ module NRSER
5
22
  class ConflictError < StandardError; end
6
23
 
7
24
 
8
- # Extension of Ruby's {NotImplementedError} to provide a useful message
25
+ # Extension of Ruby's {NotImplementedError} to provide a useful message
9
26
  # and convenient constructor for abstract methods.
10
27
  #
11
28
  # @example
@@ -31,22 +48,22 @@ module NRSER
31
48
 
32
49
  message = if @method.owner == instance.class
33
50
  NRSER.dedent <<-END
34
- Method #{ @method.owner.name }##{ @method_name } is abstract, meaning
35
- #{ @method.owner.name } is an abstract class and the invoking
51
+ Method #{ @method.owner.name }##{ @method_name } is abstract, meaning
52
+ #{ @method.owner.name } is an abstract class and the invoking
36
53
  instance #{ @instance } should NOT have been constructed.
37
54
  END
38
55
  else
39
56
  NRSER.squish <<-END
40
- Method #{ @method.owner.name }##{ @method_name } is abstract and
57
+ Method #{ @method.owner.name }##{ @method_name } is abstract and
41
58
  has not been implemented in invoking class #{ @instance.class }.
42
59
 
43
60
  If you *are* developing the invoking class #{ @instance.class } it
44
- (or a parent class between it and #{ @method.owner.name }) must
61
+ (or a parent class between it and #{ @method.owner.name }) must
45
62
  implement ##{ @method_name }.
46
63
 
47
64
  If you *are not* developing #{ @instance.class } it should be treated
48
65
  as an abstract base class and should NOT be constructed. You need to
49
- find a subclass of #{ @instance.class } to instantiate or write
66
+ find a subclass of #{ @instance.class } to instantiate or write
50
67
  your own.
51
68
  END
52
69
  end
@@ -193,8 +193,10 @@ module NRSER
193
193
  # If `enum` does not have `#count == 1`.
194
194
  #
195
195
  def self.only! enum
196
- unless enum.count == 1
197
- raise ArgumentError.new erb binding, <<-END
196
+ count = enum.count
197
+
198
+ unless count == 1
199
+ message = erb binding, <<-END
198
200
  Expected enumerable to have #count == 1 but it has
199
201
 
200
202
  #count = <%= enum.count %>
@@ -204,6 +206,11 @@ module NRSER
204
206
  <%= enum.pretty_inspect %>
205
207
 
206
208
  END
209
+
210
+ raise NRSER::CountError.new message,
211
+ subject: enum,
212
+ count: count,
213
+ expected: 1
207
214
  end
208
215
 
209
216
  enum.first