attributor 5.5 → 6.1
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 +5 -5
- data/CHANGELOG.md +17 -0
- data/lib/attributor/attribute.rb +67 -79
- data/lib/attributor/hash_dsl_compiler.rb +6 -5
- data/lib/attributor/type.rb +3 -0
- data/lib/attributor/types/collection.rb +4 -1
- data/lib/attributor/types/hash.rb +24 -16
- data/lib/attributor/types/model.rb +9 -21
- data/lib/attributor/version.rb +1 -1
- data/lib/attributor.rb +2 -6
- data/spec/attribute_spec.rb +114 -121
- data/spec/hash_dsl_compiler_spec.rb +5 -5
- data/spec/spec_helper.rb +0 -2
- data/spec/support/models.rb +7 -7
- data/spec/types/hash_spec.rb +63 -22
- data/spec/types/model_spec.rb +7 -1
- metadata +3 -7
- data/lib/attributor/attribute_resolver.rb +0 -111
- data/spec/attribute_resolver_spec.rb +0 -237
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1a27077cb0a67b6ab8fdde19c88a993ba648c6deb38d5a83766df49b7199338d
|
4
|
+
data.tar.gz: 99bf9996744d757817fd167fa510b462fab6ee1dd3b52992594d06768966bd53
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e91ad1a7759be0e56c66f53bafbe6d22f1a2ce724fc777ade2649987d6bf19d184eb0fc5f43d00dcbf2310bbae72359199f34d25c38aa65cc69f38cc28d33a55
|
7
|
+
data.tar.gz: 947764564827d2d05e967f60ccc100d42eedf219fddab8ffff56bb8f61c0264c67e9bdd77d3a3585b99d80e5e54f22ef2a6e3424c3ba83549df0932874b0c0af
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,23 @@
|
|
1
1
|
# Attributor Changelog
|
2
2
|
|
3
3
|
## next
|
4
|
+
- added support for enum's out of values in json_schema generation
|
5
|
+
|
6
|
+
## 6.1 (1/7/2022)
|
7
|
+
- added support for enum's out of values in json_schema generation
|
8
|
+
|
9
|
+
## 6.0 (22/11/2021)
|
10
|
+
- removed `required_if` support and all of the necessary code.
|
11
|
+
- changed the semantics of the `required:` option in attributes, to really mean if the "key" is required to be passed in or not (i.e., check if the key is null, not if its value is null)
|
12
|
+
- Introduced a new option`null: true|false` to allow for the value of an attribute to be nullable or not when the attribute is passed in.
|
13
|
+
* The default behavior for an attribute nullability currently `null: false` (but it can be easily changed by overriding the `Attributor::Attribute.default_for_null` function to return `true`)
|
14
|
+
## 5.7 (1/7/2021)
|
15
|
+
|
16
|
+
- added `custom_option` to Attributor::Attribute class, accepting a name and Attribute arguments that will be used to validate the option value(s) provided.
|
17
|
+
|
18
|
+
## 5.6 (11/02/2020)
|
19
|
+
|
20
|
+
- Small fixes for dumping JSON-schema default values if they're Proc's or dumpable objects
|
4
21
|
|
5
22
|
## 5.5 (21/08/2020)
|
6
23
|
|
data/lib/attributor/attribute.rb
CHANGED
@@ -17,18 +17,30 @@ module Attributor
|
|
17
17
|
FakeParent
|
18
18
|
end
|
19
19
|
end
|
20
|
+
|
20
21
|
# It is the abstract base class to hold an attribute, both a leaf and a container (hash/Array...)
|
21
22
|
# TODO: should this be a mixin since it is an abstract class?
|
22
23
|
class Attribute
|
23
24
|
attr_reader :type, :options
|
24
25
|
|
26
|
+
@custom_options = {}
|
27
|
+
|
28
|
+
class << self
|
29
|
+
attr_accessor :custom_options
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.custom_option(name, attr_type, options = {}, &block)
|
33
|
+
if TOP_LEVEL_OPTIONS.include?(name) || INTERNAL_OPTIONS.include?(name)
|
34
|
+
raise ArgumentError, "can not define custom_option with name #{name.inspect}, it is reserved by Attributor"
|
35
|
+
end
|
36
|
+
self.custom_options[name] = Attributor::Attribute.new(attr_type, options, &block)
|
37
|
+
end
|
38
|
+
|
25
39
|
# @options: metadata about the attribute
|
26
40
|
# @block: code definition for struct attributes (nil for predefined types or leaf/simple types)
|
27
41
|
def initialize(type, options = {}, &block)
|
28
42
|
@type = Attributor.resolve_type(type, options, block)
|
29
|
-
|
30
|
-
@options = options
|
31
|
-
@options = @type.options.merge(@options) if @type.respond_to?(:options)
|
43
|
+
@options = @type.respond_to?(:options) ? @type.options.merge(options) : options
|
32
44
|
|
33
45
|
check_options!
|
34
46
|
end
|
@@ -82,18 +94,16 @@ module Attributor
|
|
82
94
|
|
83
95
|
def validate_type(value, context)
|
84
96
|
# delegate check to type subclass if it exists
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
[]
|
97
|
+
return [] if value.nil? || type.valid_type?(value)
|
98
|
+
|
99
|
+
msg = "Attribute #{Attributor.humanize_context(context)} received value: "
|
100
|
+
msg += "#{Attributor.errorize_value(value)} is of the wrong type "
|
101
|
+
msg += "(got: #{value.class.name}, expected: #{type.name})"
|
102
|
+
[msg]
|
92
103
|
end
|
93
104
|
|
94
|
-
TOP_LEVEL_OPTIONS = [:description, :values, :default, :example, :required, :
|
105
|
+
TOP_LEVEL_OPTIONS = [:description, :values, :default, :example, :required, :null, :custom_data].freeze
|
95
106
|
INTERNAL_OPTIONS = [:dsl_compiler, :dsl_compiler_options].freeze # Options we don't want to expose when describing attributes
|
96
|
-
JSON_SCHEMA_UNSUPPORTED_OPTIONS = [ :required, :required_if ].freeze
|
97
107
|
def describe(shallow=true, example: nil)
|
98
108
|
description = { }
|
99
109
|
# Clone the common options
|
@@ -161,7 +171,10 @@ module Attributor
|
|
161
171
|
|
162
172
|
description[:description] = self.options[:description] if self.options[:description]
|
163
173
|
description[:enum] = self.options[:values] if self.options[:values]
|
164
|
-
|
174
|
+
if the_default = self.options[:default]
|
175
|
+
the_object = the_default.is_a?(Proc) ? the_default.call : the_default
|
176
|
+
description[:default] = the_object.is_a?(Attributor::Dumpable) ? the_object.dump : the_object
|
177
|
+
end
|
165
178
|
#TODO description[:title] = "TODO: do we want to use a title??..."
|
166
179
|
|
167
180
|
# Change the reference option to the actual class name.
|
@@ -172,6 +185,11 @@ module Attributor
|
|
172
185
|
# TODO: not sure if that's correct (we used to get it from the described hash...
|
173
186
|
description[:example] = self.dump(example) if example
|
174
187
|
|
188
|
+
# add custom options as x-optionname
|
189
|
+
self.class.custom_options.each do |name, _|
|
190
|
+
description["x-#{name}".to_sym] = self.options[name] if self.options.key?(name)
|
191
|
+
end
|
192
|
+
|
175
193
|
description
|
176
194
|
end
|
177
195
|
|
@@ -206,78 +224,38 @@ module Attributor
|
|
206
224
|
type.attributes if @type_has_attributes ||= type.respond_to?(:attributes)
|
207
225
|
end
|
208
226
|
|
227
|
+
# Default value for a non-specified null: option
|
228
|
+
def self.default_for_null
|
229
|
+
false
|
230
|
+
end
|
231
|
+
|
232
|
+
# It is only nullable if there is an explicit null: true (or if it's not passed/set, and the default is true)
|
233
|
+
def self.nullable_attribute?(options)
|
234
|
+
!options.key?(:null) ? default_for_null : options[:null]
|
235
|
+
end
|
236
|
+
|
209
237
|
# Validates stuff and checks dependencies
|
210
238
|
def validate(object, context = Attributor::DEFAULT_ROOT_CONTEXT)
|
211
239
|
raise "INVALID CONTEXT!! #{context}" unless context
|
212
240
|
# Validate any requirements, absolute or conditional, and return.
|
213
241
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
return validate_missing_value(context)
|
218
|
-
end
|
219
|
-
|
220
|
-
# TODO: support validation for other types of conditional dependencies based on values of other attributes
|
221
|
-
|
222
|
-
errors = validate_type(object, context)
|
223
|
-
|
224
|
-
# End validation if we don't even have the proper type to begin with
|
225
|
-
return errors if errors.any?
|
226
|
-
|
227
|
-
if options[:values] && !options[:values].include?(object)
|
228
|
-
errors << "Attribute #{Attributor.humanize_context(context)}: #{Attributor.errorize_value(object)} is not within the allowed values=#{options[:values].inspect} "
|
229
|
-
end
|
230
|
-
|
231
|
-
errors + type.validate(object, context, self)
|
232
|
-
end
|
233
|
-
|
234
|
-
def validate_missing_value(context)
|
235
|
-
raise "INVALID CONTEXT!!! (got: #{context.inspect})" unless context.is_a? Enumerable
|
236
|
-
|
237
|
-
# Missing attribute was required if :required option was set
|
238
|
-
return ["Attribute #{Attributor.humanize_context(context)} is required"] if options[:required]
|
239
|
-
|
240
|
-
# Missing attribute was not required if :required_if (and :required)
|
241
|
-
# option was NOT set
|
242
|
-
requirement = options[:required_if]
|
243
|
-
return [] unless requirement
|
244
|
-
|
245
|
-
case requirement
|
246
|
-
when ::String
|
247
|
-
key_path = requirement
|
248
|
-
predicate = nil
|
249
|
-
when ::Hash
|
250
|
-
# TODO: support multiple dependencies?
|
251
|
-
key_path = requirement.keys.first
|
252
|
-
predicate = requirement.values.first
|
242
|
+
errors = []
|
243
|
+
if object.nil? && !self.class.nullable_attribute?(options)
|
244
|
+
errors << "Attribute #{Attributor.humanize_context(context)} is not nullable"
|
253
245
|
else
|
254
|
-
|
255
|
-
raise AttributorException, "unknown type of dependency: #{requirement.inspect} for #{Attributor.humanize_context(context)}"
|
256
|
-
end
|
257
|
-
|
258
|
-
# chop off the last part
|
259
|
-
requirement_context = context[0..-2]
|
260
|
-
requirement_context_string = requirement_context.join(Attributor::SEPARATOR)
|
246
|
+
errors.push *validate_type(object, context)
|
261
247
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
unless key_path[0..0] == Attributor::AttributeResolver::ROOT_PREFIX
|
268
|
-
message << "(for #{Attributor.humanize_context(requirement_context)}) "
|
248
|
+
# If the value is null we skip value validation:
|
249
|
+
# a) If null wasn't allowed, it would have failed above.
|
250
|
+
# b) If null was allowed, we always allow that as a valid value
|
251
|
+
if !object.nil? && options[:values] && !options[:values].include?(object)
|
252
|
+
errors << "Attribute #{Attributor.humanize_context(context)}: #{Attributor.errorize_value(object)} is not within the allowed values=#{options[:values].inspect} "
|
269
253
|
end
|
270
|
-
|
271
|
-
message << if predicate
|
272
|
-
"matches #{predicate.inspect}."
|
273
|
-
else
|
274
|
-
'is present.'
|
275
|
-
end
|
276
|
-
|
277
|
-
[message]
|
278
|
-
else
|
279
|
-
[]
|
280
254
|
end
|
255
|
+
|
256
|
+
return errors if errors.any?
|
257
|
+
|
258
|
+
object.nil? ? errors : errors + type.validate(object, context, self)
|
281
259
|
end
|
282
260
|
|
283
261
|
def check_options!
|
@@ -293,6 +271,8 @@ module Attributor
|
|
293
271
|
|
294
272
|
# TODO: override in type subclass
|
295
273
|
def check_option!(name, definition)
|
274
|
+
return check_custom_option(name, definition) if self.class.custom_options.include? name
|
275
|
+
|
296
276
|
case name
|
297
277
|
when :values
|
298
278
|
raise AttributorException, "Allowed set of values requires an array. Got (#{definition})" unless definition.is_a? ::Array
|
@@ -304,9 +284,8 @@ module Attributor
|
|
304
284
|
when :required
|
305
285
|
raise AttributorException, 'Required must be a boolean' unless definition == true || definition == false
|
306
286
|
raise AttributorException, 'Required cannot be enabled in combination with :default' if definition == true && options.key?(:default)
|
307
|
-
when :
|
308
|
-
raise AttributorException, '
|
309
|
-
raise AttributorException, 'Required_if cannot be specified together with :required' if options[:required]
|
287
|
+
when :null
|
288
|
+
raise AttributorException, 'Null must be a boolean' unless definition == true || definition == false
|
310
289
|
when :example
|
311
290
|
unless definition.is_a?(::Regexp) || definition.is_a?(::String) || definition.is_a?(::Array) || definition.is_a?(::Proc) || definition.nil? || type.valid_type?(definition)
|
312
291
|
raise AttributorException, "Invalid example type (got: #{definition.class.name}). It must always match the type of the attribute (except if passing Regex that is allowed for some types)"
|
@@ -319,5 +298,14 @@ module Attributor
|
|
319
298
|
|
320
299
|
:ok # passes
|
321
300
|
end
|
301
|
+
|
302
|
+
def check_custom_option(name, definition)
|
303
|
+
attribute = self.class.custom_options.fetch(name)
|
304
|
+
|
305
|
+
errors = attribute.validate(definition)
|
306
|
+
raise AttributorException, "Custom option #{name.inspect} is invalid: #{errors.inspect}" if errors.any?
|
307
|
+
|
308
|
+
:ok
|
309
|
+
end
|
322
310
|
end
|
323
311
|
end
|
@@ -35,30 +35,31 @@ module Attributor
|
|
35
35
|
rest = attr_names - keys
|
36
36
|
unless rest.empty?
|
37
37
|
rest.each do |attr|
|
38
|
-
|
38
|
+
sub_context = Attributor::Hash.generate_subcontext(context, attr)
|
39
|
+
result.push "Attribute #{Attributor.humanize_context(sub_context)} is required."
|
39
40
|
end
|
40
41
|
end
|
41
42
|
when :exactly
|
42
43
|
included = attr_names & keys
|
43
44
|
unless included.size == number
|
44
|
-
result.push "Exactly #{number} of the following
|
45
|
+
result.push "Exactly #{number} of the following attributes #{attr_names} are required for #{Attributor.humanize_context(context)}. Found #{included.size} instead: #{included.inspect}"
|
45
46
|
end
|
46
47
|
when :at_most
|
47
48
|
rest = attr_names & keys
|
48
49
|
if rest.size > number
|
49
50
|
found = rest.empty? ? 'none' : rest.inspect
|
50
|
-
result.push "At most #{number}
|
51
|
+
result.push "At most #{number} attributes out of #{attr_names} can be passed in for #{Attributor.humanize_context(context)}. Found #{found}"
|
51
52
|
end
|
52
53
|
when :at_least
|
53
54
|
rest = attr_names & keys
|
54
55
|
if rest.size < number
|
55
56
|
found = rest.empty? ? 'none' : rest.inspect
|
56
|
-
result.push "At least #{number}
|
57
|
+
result.push "At least #{number} attributes out of #{attr_names} are required to be passed in for #{Attributor.humanize_context(context)}. Found #{found}"
|
57
58
|
end
|
58
59
|
when :exclusive
|
59
60
|
intersection = attr_names & keys
|
60
61
|
if intersection.size > 1
|
61
|
-
result.push "
|
62
|
+
result.push "Attributes #{intersection.inspect} are mutually exclusive for #{Attributor.humanize_context(context)}."
|
62
63
|
end
|
63
64
|
end
|
64
65
|
result
|
data/lib/attributor/type.rb
CHANGED
@@ -125,7 +125,10 @@ module Attributor
|
|
125
125
|
hash = super
|
126
126
|
opts = self.options.merge( attribute_options )
|
127
127
|
hash[:description] = opts[:description] if opts[:description]
|
128
|
-
|
128
|
+
if the_default = opts[:default]
|
129
|
+
the_object = the_default.is_a?(Proc) ? the_default.call : the_default
|
130
|
+
hash[:description] = the_object.is_a?(Attributor::Dumpable) ? the_object.dump : the_object
|
131
|
+
end
|
129
132
|
|
130
133
|
#hash[:examples] = [ example.dump ] if example
|
131
134
|
member_example = example && example.first
|
@@ -426,7 +426,6 @@ module Attributor
|
|
426
426
|
unless object.is_a?(self)
|
427
427
|
raise ArgumentError, "#{name} can not validate object of type #{object.class.name} for #{Attributor.humanize_context(context)}."
|
428
428
|
end
|
429
|
-
|
430
429
|
object.validate(context)
|
431
430
|
end
|
432
431
|
|
@@ -514,7 +513,6 @@ module Attributor
|
|
514
513
|
end
|
515
514
|
end
|
516
515
|
# TODO: minProperties and maxProperties and patternProperties
|
517
|
-
# TODO: map our required_if (and possible our above requirements 'at_least...' to json schema dependencies)
|
518
516
|
hash
|
519
517
|
end
|
520
518
|
|
@@ -613,6 +611,12 @@ module Attributor
|
|
613
611
|
context = [context] if context.is_a? ::String
|
614
612
|
|
615
613
|
if self.class.keys.any?
|
614
|
+
extra_keys = @contents.keys - self.class.keys.keys
|
615
|
+
if extra_keys.any? && !self.class.options[:allow_extra]
|
616
|
+
return extra_keys.collect do |k|
|
617
|
+
"#{Attributor.humanize_context(context)} can not have key: #{k.inspect}"
|
618
|
+
end
|
619
|
+
end
|
616
620
|
self.validate_keys(context)
|
617
621
|
else
|
618
622
|
self.validate_generic(context)
|
@@ -622,30 +626,34 @@ module Attributor
|
|
622
626
|
end
|
623
627
|
|
624
628
|
def validate_keys(context)
|
625
|
-
extra_keys = @contents.keys - self.class.keys.keys
|
626
|
-
if extra_keys.any? && !self.class.options[:allow_extra]
|
627
|
-
return extra_keys.collect do |k|
|
628
|
-
"#{Attributor.humanize_context(context)} can not have key: #{k.inspect}"
|
629
|
-
end
|
630
|
-
end
|
631
|
-
|
632
629
|
errors = []
|
633
|
-
|
630
|
+
keys_provided = []
|
634
631
|
|
635
632
|
self.class.keys.each do |key, attribute|
|
636
633
|
sub_context = self.class.generate_subcontext(context, key)
|
637
634
|
|
638
|
-
value =
|
639
|
-
|
635
|
+
value = _get_attr(key)
|
636
|
+
keys_provided << key if @contents.key?(key)
|
640
637
|
|
641
638
|
if value.respond_to?(:validating) # really, it's a thing with sub-attributes
|
642
639
|
next if value.validating
|
643
640
|
end
|
644
|
-
|
645
|
-
|
641
|
+
# Isn't this handled by the requirements validation? NO! we might want to combine
|
642
|
+
if attribute.options[:required] && !@contents.key?(key)
|
643
|
+
errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is required."]
|
644
|
+
end
|
645
|
+
if @contents[key].nil?
|
646
|
+
if !Attribute.nullable_attribute?(attribute.options) && @contents.key?(key)
|
647
|
+
errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is not nullable."]
|
648
|
+
end
|
649
|
+
# No need to validate the attribute further if the key wasn't passed...(or we would get nullable errors etc..cause the attribute has no
|
650
|
+
# context if its containing key was even passed (and there might not be a containing key for a top level attribute anyways))
|
651
|
+
else
|
652
|
+
errors.concat attribute.validate(value, sub_context)
|
653
|
+
end
|
646
654
|
end
|
647
655
|
self.class.requirements.each do |requirement|
|
648
|
-
validation_errors = requirement.validate(
|
656
|
+
validation_errors = requirement.validate(keys_provided, context)
|
649
657
|
errors.concat(validation_errors) unless validation_errors.empty?
|
650
658
|
end
|
651
659
|
errors
|
@@ -661,7 +669,7 @@ module Attributor
|
|
661
669
|
|
662
670
|
unless value_type == Attributor::Object
|
663
671
|
sub_context = context + ["value(#{value.inspect})"]
|
664
|
-
|
672
|
+
errors.concat value_attribute.validate(value, sub_context)
|
665
673
|
end
|
666
674
|
end
|
667
675
|
end
|
@@ -129,27 +129,9 @@ module Attributor
|
|
129
129
|
@validating = true
|
130
130
|
|
131
131
|
context = [context] if context.is_a? ::String
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
self.class.attributes.each do |sub_attribute_name, sub_attribute|
|
136
|
-
sub_context = self.class.generate_subcontext(context, sub_attribute_name)
|
137
|
-
|
138
|
-
value = __send__(sub_attribute_name)
|
139
|
-
keys_with_values << sub_attribute_name unless value.nil?
|
140
|
-
|
141
|
-
if value.respond_to?(:validating) # really, it's a thing with sub-attributes
|
142
|
-
next if value.validating
|
143
|
-
end
|
144
|
-
|
145
|
-
errors.concat sub_attribute.validate(value, sub_context)
|
146
|
-
end
|
147
|
-
self.class.requirements.each do |req|
|
148
|
-
validation_errors = req.validate(keys_with_values, context)
|
149
|
-
errors.concat(validation_errors) unless validation_errors.empty?
|
150
|
-
end
|
151
|
-
|
152
|
-
errors
|
132
|
+
# Use the common, underlying attribute validation of the hash (which will use our _get_attr)
|
133
|
+
# to know how to retrieve a value from a model (instead of a hash)
|
134
|
+
validate_keys(context)
|
153
135
|
ensure
|
154
136
|
@validating = false
|
155
137
|
end
|
@@ -198,4 +180,10 @@ module Attributor
|
|
198
180
|
@dumping = false
|
199
181
|
end
|
200
182
|
end
|
183
|
+
|
184
|
+
# Override the generic way to get a value from an instance (models need to call the method)
|
185
|
+
def _get_attr(k)
|
186
|
+
__send__(k)
|
187
|
+
end
|
188
|
+
|
201
189
|
end
|
data/lib/attributor/version.rb
CHANGED
data/lib/attributor.rb
CHANGED
@@ -13,7 +13,6 @@ module Attributor
|
|
13
13
|
require_relative 'attributor/type'
|
14
14
|
require_relative 'attributor/dsl_compiler'
|
15
15
|
require_relative 'attributor/hash_dsl_compiler'
|
16
|
-
require_relative 'attributor/attribute_resolver'
|
17
16
|
require_relative 'attributor/smart_attribute_selector'
|
18
17
|
|
19
18
|
require_relative 'attributor/example_mixin'
|
@@ -22,7 +21,8 @@ module Attributor
|
|
22
21
|
|
23
22
|
# hierarchical separator string for composing human readable attributes
|
24
23
|
SEPARATOR = '.'.freeze
|
25
|
-
|
24
|
+
ROOT_PREFIX = '$'.freeze
|
25
|
+
DEFAULT_ROOT_CONTEXT = [ROOT_PREFIX].freeze
|
26
26
|
|
27
27
|
# @param type [Class] The class of the type to resolve
|
28
28
|
#
|
@@ -56,10 +56,6 @@ module Attributor
|
|
56
56
|
|
57
57
|
context = Array(context) if context.is_a? ::String
|
58
58
|
|
59
|
-
unless context.is_a? Enumerable
|
60
|
-
raise "INVALID CONTEXT!!! (got: #{context.inspect})"
|
61
|
-
end
|
62
|
-
|
63
59
|
begin
|
64
60
|
return context.join('.')
|
65
61
|
rescue e
|