structure 3.1.0 → 3.2.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/lib/structure/builder.rb +33 -7
- data/lib/structure/types.rb +48 -27
- data/lib/structure/version.rb +1 -1
- data/lib/structure.rb +21 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c798ddd0dce500c2053077d3964ffe907c1f13fa0b1b9f9394c18a1cc9829b31
|
4
|
+
data.tar.gz: 2c82c99faa38aeb774415121d793ff2205a628aecab194fe60a36746efcb983f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f3ec870b530f010c1356ee2ecadb6ca6e2880e29b4f15b80e395812aad8d342737a547811c442c737c50ff1b4b6dc98a2496ae2aa7df6e657d76198bb964b82a
|
7
|
+
data.tar.gz: 30bafd22ee1ed82b7e5e5d702617200a31689fccad006df1e884d66d99b43c8b85ad9ae99004ca2b9a271f8b721a86c200553a289adc24496abf691b6f63573f
|
data/lib/structure/builder.rb
CHANGED
@@ -13,6 +13,25 @@ module Structure
|
|
13
13
|
@defaults = {}
|
14
14
|
end
|
15
15
|
|
16
|
+
# DSL method for defining attributes with optional type coercion
|
17
|
+
#
|
18
|
+
# @param name [Symbol] The attribute name
|
19
|
+
# @param type [Class, Symbol, Array, nil] Type for coercion (e.g., String, :boolean, [String])
|
20
|
+
# @param from [String, nil] Source key in the data hash (defaults to name.to_s)
|
21
|
+
# @param default [Object, nil] Default value if attribute is missing
|
22
|
+
# @yield [value] Block for custom transformation
|
23
|
+
# @raise [ArgumentError] If both type and block are provided
|
24
|
+
#
|
25
|
+
# @example With type coercion
|
26
|
+
# attribute :age, Integer
|
27
|
+
#
|
28
|
+
# @example With custom source key
|
29
|
+
# attribute :created_at, Time, from: "CreatedAt"
|
30
|
+
#
|
31
|
+
# @example With transformation block
|
32
|
+
# attribute :price do |value|
|
33
|
+
# Money.new(value["amount"], value["currency"])
|
34
|
+
# end
|
16
35
|
def attribute(name, type = nil, from: nil, default: nil, &block)
|
17
36
|
# Always store in mappings - use attribute name as default source
|
18
37
|
@mappings[name] = from || name.to_s
|
@@ -27,23 +46,30 @@ module Structure
|
|
27
46
|
end
|
28
47
|
end
|
29
48
|
|
30
|
-
#
|
49
|
+
# Defines a callback to run after parsing
|
50
|
+
#
|
51
|
+
# @yield [instance] Block that receives the parsed instance
|
52
|
+
# @return [void]
|
53
|
+
#
|
54
|
+
# @example Validation
|
55
|
+
# after_parse do |order|
|
56
|
+
# raise "Invalid order" if order.total < 0
|
57
|
+
# end
|
58
|
+
def after_parse(&block)
|
59
|
+
@after_parse_callback = block
|
60
|
+
end
|
61
|
+
|
31
62
|
def attributes
|
32
63
|
@mappings.keys
|
33
64
|
end
|
34
65
|
|
35
|
-
# Deduced from types that are boolean
|
36
66
|
def predicate_methods
|
37
67
|
@types.filter_map do |name, type_lambda|
|
38
|
-
if type_lambda == Types.boolean
|
68
|
+
if type_lambda == Types.boolean && !name.to_s.end_with?("?")
|
39
69
|
predicate_name = "#{name}?"
|
40
70
|
[predicate_name.to_sym, name]
|
41
71
|
end
|
42
72
|
end.to_h
|
43
73
|
end
|
44
|
-
|
45
|
-
def after_parse(&block)
|
46
|
-
@after_parse_callback = block
|
47
|
-
end
|
48
74
|
end
|
49
75
|
end
|
data/lib/structure/types.rb
CHANGED
@@ -10,41 +10,32 @@ module Structure
|
|
10
10
|
BOOLEAN_TRUTHY = [true, 1, "1", "t", "T", "true", "TRUE", "on", "ON"].freeze
|
11
11
|
private_constant :BOOLEAN_TRUTHY
|
12
12
|
|
13
|
-
# Boolean conversion
|
14
|
-
# Memoized so predicate method detection works via object identity comparison
|
15
13
|
def boolean
|
16
14
|
@boolean ||= ->(val) { BOOLEAN_TRUTHY.include?(val) }
|
17
15
|
end
|
18
16
|
|
19
|
-
# Generic handler for classes with kernel methods (String, Integer, Float, etc.)
|
20
|
-
def kernel(type)
|
21
|
-
->(val) { Kernel.send(type.name, val) }
|
22
|
-
end
|
23
|
-
|
24
|
-
# Handler for classes with parse methods (e.g., Date, Time, URI, nested Structure classes)
|
25
|
-
def parseable(type)
|
26
|
-
->(val) { type.parse(val) }
|
27
|
-
end
|
28
|
-
|
29
|
-
# Create coercer for array elements
|
30
|
-
def array(element_type)
|
31
|
-
element_coercer = coerce(element_type)
|
32
|
-
->(array) { array.map { |element| element_coercer.call(element) } }
|
33
|
-
end
|
34
|
-
|
35
17
|
# Main factory method for creating type coercers
|
18
|
+
#
|
19
|
+
# @param type [Class, Symbol, Array] Type specification
|
20
|
+
# @return [Proc, Object] Coercion proc or the type itself if no coercion available
|
21
|
+
#
|
22
|
+
# @example Boolean type
|
23
|
+
# coerce(:boolean) # => boolean proc
|
24
|
+
#
|
25
|
+
# @example Kernel types
|
26
|
+
# coerce(Integer) # => proc that calls Kernel.Integer
|
27
|
+
#
|
28
|
+
# @example Parseable types
|
29
|
+
# coerce(Date) # => proc that calls Date.parse
|
30
|
+
#
|
31
|
+
# @example Array types
|
32
|
+
# coerce([String]) # => proc that coerces array elements to String
|
36
33
|
def coerce(type)
|
37
34
|
case type
|
38
35
|
when :boolean
|
39
36
|
boolean
|
40
|
-
when
|
41
|
-
|
42
|
-
kernel(type)
|
43
|
-
elsif type.respond_to?(:parse)
|
44
|
-
parseable(type)
|
45
|
-
else
|
46
|
-
type
|
47
|
-
end
|
37
|
+
when :self
|
38
|
+
self_referential
|
48
39
|
when Array
|
49
40
|
if type.length == 1
|
50
41
|
array(type.first)
|
@@ -52,7 +43,37 @@ module Structure
|
|
52
43
|
type
|
53
44
|
end
|
54
45
|
else
|
55
|
-
|
46
|
+
# Handle Class, Module, and any other types
|
47
|
+
if type.respond_to?(:parse)
|
48
|
+
parseable(type)
|
49
|
+
elsif type.respond_to?(:name) && type.name && Kernel.respond_to?(type.name)
|
50
|
+
kernel(type)
|
51
|
+
else
|
52
|
+
type
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def self_referential
|
60
|
+
proc { |val| parse(val) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def kernel(type)
|
64
|
+
->(val) { Kernel.send(type.name, val) }
|
65
|
+
end
|
66
|
+
|
67
|
+
def parseable(type)
|
68
|
+
->(val) { type.parse(val) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def array(element_type)
|
72
|
+
if element_type == :self
|
73
|
+
proc { |array| array.map { |element| parse(element) } }
|
74
|
+
else
|
75
|
+
element_coercer = coerce(element_type)
|
76
|
+
->(array) { array.map { |element| element_coercer.call(element) } }
|
56
77
|
end
|
57
78
|
end
|
58
79
|
end
|
data/lib/structure/version.rb
CHANGED
data/lib/structure.rb
CHANGED
@@ -5,6 +5,20 @@ require "structure/builder"
|
|
5
5
|
# A library for parsing data into immutable Ruby Data objects with type coercion
|
6
6
|
module Structure
|
7
7
|
class << self
|
8
|
+
# Creates a new Data class with attribute definitions and type coercion
|
9
|
+
#
|
10
|
+
# @yield [Builder] Block for defining attributes using the DSL
|
11
|
+
# @return [Class] A Data class with a parse method
|
12
|
+
#
|
13
|
+
# @example Basic usage
|
14
|
+
# Person = Structure.new do
|
15
|
+
# attribute :name, String
|
16
|
+
# attribute :age, Integer
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# person = Person.parse(name: "Alice", age: "30")
|
20
|
+
# person.name # => "Alice"
|
21
|
+
# person.age # => 30
|
8
22
|
def new(&block)
|
9
23
|
builder = Builder.new
|
10
24
|
builder.instance_eval(&block) if block
|
@@ -44,7 +58,13 @@ module Structure
|
|
44
58
|
|
45
59
|
# Apply type coercion or transformation
|
46
60
|
if types[attr] && !value.nil?
|
47
|
-
|
61
|
+
type_or_proc = types[attr]
|
62
|
+
# Use instance_exec for non-lambda procs (self-referential types)
|
63
|
+
value = if type_or_proc.is_a?(Proc) && !type_or_proc.lambda?
|
64
|
+
instance_exec(value, &type_or_proc)
|
65
|
+
else
|
66
|
+
type_or_proc.call(value)
|
67
|
+
end
|
48
68
|
end
|
49
69
|
|
50
70
|
final_kwargs[attr] = value
|