structure 2.3.0 → 3.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/lib/structure/builder.rb +49 -0
- data/lib/structure/types.rb +59 -0
- data/lib/structure/version.rb +1 -1
- data/lib/structure.rb +42 -132
- metadata +12 -55
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: af5b8b6dc3dacef6201847123b04742310441ded70ec7952b42d07a668d83f2f
|
4
|
+
data.tar.gz: '00385227416285e5a4d1f2cfcf2cc0e5cb94bb368cf8995b014a7bf3fd91d9c6'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ba80d09a4867a1497133026cc4d4654e03586f677d44d07a7f2387b40fce16e8fc188bc46cc8c768663ded6387b580d3dc210ae884d14530bf62e7e56ec4ace4
|
7
|
+
data.tar.gz: 8a0c8e7d5f9585a946817cc31a3cd831ea0332833030798d178ce81ee1737809226321ac105f106559358aa4ae24753368d808007ef6ab37230b6d6fc98ffe69
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "structure/types"
|
4
|
+
|
5
|
+
module Structure
|
6
|
+
# Builder class for accumulating attribute definitions
|
7
|
+
class Builder
|
8
|
+
attr_reader :mappings, :types, :defaults, :after_parse_callback
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@mappings = {}
|
12
|
+
@types = {}
|
13
|
+
@defaults = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def attribute(name, type = nil, from: nil, default: nil, &block)
|
17
|
+
# Always store in mappings - use attribute name as default source
|
18
|
+
@mappings[name] = from || name.to_s
|
19
|
+
@defaults[name] = default unless default.nil?
|
20
|
+
|
21
|
+
if type && block
|
22
|
+
raise ArgumentError, "Cannot specify both type and block for :#{name}"
|
23
|
+
elsif block
|
24
|
+
@types[name] = block
|
25
|
+
elsif type
|
26
|
+
@types[name] = Types.coerce(type)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Deduced from mappings - maintains order of definition
|
31
|
+
def attributes
|
32
|
+
@mappings.keys
|
33
|
+
end
|
34
|
+
|
35
|
+
# Deduced from types that are boolean
|
36
|
+
def predicate_methods
|
37
|
+
@types.filter_map do |name, type_lambda|
|
38
|
+
if type_lambda == Types.boolean
|
39
|
+
predicate_name = "#{name}?"
|
40
|
+
[predicate_name.to_sym, name]
|
41
|
+
end
|
42
|
+
end.to_h
|
43
|
+
end
|
44
|
+
|
45
|
+
def after_parse(&block)
|
46
|
+
@after_parse_callback = block
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Structure
|
4
|
+
# Type coercion methods for converting values to specific types
|
5
|
+
module Types
|
6
|
+
extend self
|
7
|
+
|
8
|
+
# Rails-style boolean truthy values
|
9
|
+
# Reference: https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
|
10
|
+
BOOLEAN_TRUTHY = [true, 1, "1", "t", "T", "true", "TRUE", "on", "ON"].freeze
|
11
|
+
private_constant :BOOLEAN_TRUTHY
|
12
|
+
|
13
|
+
# Boolean conversion
|
14
|
+
# Memoized so predicate method detection works via object identity comparison
|
15
|
+
def boolean
|
16
|
+
@boolean ||= ->(val) { BOOLEAN_TRUTHY.include?(val) }
|
17
|
+
end
|
18
|
+
|
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
|
+
# Main factory method for creating type coercers
|
36
|
+
def coerce(type)
|
37
|
+
case type
|
38
|
+
when :boolean
|
39
|
+
boolean
|
40
|
+
when Class, Module
|
41
|
+
if type.name && Kernel.respond_to?(type.name)
|
42
|
+
kernel(type)
|
43
|
+
elsif type.respond_to?(:parse)
|
44
|
+
parseable(type)
|
45
|
+
else
|
46
|
+
type
|
47
|
+
end
|
48
|
+
when Array
|
49
|
+
if type.length == 1
|
50
|
+
array(type.first)
|
51
|
+
else
|
52
|
+
type
|
53
|
+
end
|
54
|
+
else
|
55
|
+
type
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/structure/version.rb
CHANGED
data/lib/structure.rb
CHANGED
@@ -1,151 +1,61 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require "structure/builder"
|
4
|
+
|
5
|
+
# A library for parsing data into immutable Ruby Data objects with type coercion
|
4
6
|
module Structure
|
5
7
|
class << self
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
if base.is_a?(Class)
|
10
|
-
base.extend ClassMethods
|
11
|
-
else
|
12
|
-
def base.included(base)
|
13
|
-
::Structure.send(:included, base)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
def attribute_names
|
20
|
-
self.class.attribute_names
|
21
|
-
end
|
22
|
-
|
23
|
-
def to_a
|
24
|
-
attribute_names.map { |key| [key, send(key)] }
|
25
|
-
end
|
26
|
-
|
27
|
-
def to_h
|
28
|
-
Hash[to_a]
|
29
|
-
end
|
30
|
-
|
31
|
-
alias attributes to_h
|
8
|
+
def new(&block)
|
9
|
+
builder = Builder.new
|
10
|
+
builder.instance_eval(&block) if block
|
32
11
|
|
33
|
-
|
34
|
-
detail = if public_methods(false).include?(:to_s)
|
35
|
-
to_s
|
36
|
-
else
|
37
|
-
to_a.map { |key, val| "#{key}=#{val.inspect}" }.join(', ')
|
38
|
-
end
|
12
|
+
data_class = Data.define(*builder.attributes)
|
39
13
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
def ==(other)
|
46
|
-
if public_methods(false).include?(:<=>)
|
47
|
-
super
|
48
|
-
else
|
49
|
-
attributes == other.attributes
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
def eql?(other)
|
54
|
-
return false if other.class != self.class
|
55
|
-
|
56
|
-
self == other
|
57
|
-
end
|
58
|
-
|
59
|
-
def freeze
|
60
|
-
attribute_names.each { |key| send(key) }
|
61
|
-
super
|
62
|
-
end
|
63
|
-
|
64
|
-
def marshal_dump
|
65
|
-
attributes.values
|
66
|
-
end
|
67
|
-
|
68
|
-
def marshal_load(data)
|
69
|
-
@mutex = ::Thread::Mutex.new
|
70
|
-
attribute_names.zip(data).each do |key, val|
|
71
|
-
instance_variable_set(:"@#{key}", val)
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
private
|
76
|
-
|
77
|
-
def with_mutex(&block)
|
78
|
-
@mutex.owned? ? block.call : @mutex.synchronize { block.call }
|
79
|
-
end
|
80
|
-
|
81
|
-
# The class interface
|
82
|
-
module ClassMethods
|
83
|
-
attr_reader :attribute_names
|
84
|
-
|
85
|
-
class << self
|
86
|
-
def extended(base)
|
87
|
-
base.instance_variable_set :@attribute_names, []
|
88
|
-
base.send :override_initialize
|
14
|
+
# Generate predicate methods
|
15
|
+
builder.predicate_methods.each do |predicate_name, attribute_name|
|
16
|
+
data_class.define_method(predicate_name) do
|
17
|
+
send(attribute_name)
|
18
|
+
end
|
89
19
|
end
|
90
20
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
21
|
+
# Capture builder data in closure for parse method
|
22
|
+
mappings = builder.mappings
|
23
|
+
types = builder.types
|
24
|
+
defaults = builder.defaults
|
25
|
+
attributes = builder.attributes
|
26
|
+
after_parse_callback = builder.after_parse_callback
|
27
|
+
|
28
|
+
data_class.define_singleton_method(:parse) do |data = {}, **kwargs|
|
29
|
+
# Merge kwargs into data - kwargs take priority as overrides
|
30
|
+
# Convert kwargs symbol keys to strings to match source_key lookups
|
31
|
+
string_kwargs = kwargs.transform_keys(&:to_s)
|
32
|
+
data = data.merge(string_kwargs)
|
33
|
+
|
34
|
+
final_kwargs = {}
|
35
|
+
attributes.each do |attr|
|
36
|
+
source_key = mappings[attr]
|
37
|
+
value = if data.key?(source_key)
|
38
|
+
data[source_key]
|
39
|
+
elsif data.key?(source_key.to_sym)
|
40
|
+
data[source_key.to_sym]
|
41
|
+
elsif defaults.key?(attr)
|
42
|
+
defaults[attr]
|
102
43
|
end
|
103
|
-
CODE
|
104
|
-
end
|
105
|
-
|
106
|
-
module_eval <<-CODE, __FILE__, __LINE__ + 1
|
107
|
-
def #{name}
|
108
|
-
with_mutex do
|
109
|
-
break if defined?(@#{name})
|
110
44
|
|
111
|
-
|
45
|
+
# Apply type coercion or transformation
|
46
|
+
if types[attr] && !value.nil?
|
47
|
+
value = types[attr].call(value)
|
112
48
|
end
|
113
49
|
|
114
|
-
|
115
|
-
end
|
116
|
-
CODE
|
117
|
-
private define_method "unmemoized_#{name}", block
|
118
|
-
@attribute_names << name
|
119
|
-
|
120
|
-
name.to_sym
|
121
|
-
end
|
122
|
-
|
123
|
-
private
|
124
|
-
|
125
|
-
def override_initialize
|
126
|
-
class_eval do
|
127
|
-
unless method_defined?(:overriding_initialize)
|
128
|
-
define_method :overriding_initialize do |*arguments, &block|
|
129
|
-
@mutex = ::Thread::Mutex.new
|
130
|
-
original_initialize(*arguments, &block)
|
131
|
-
end
|
50
|
+
final_kwargs[attr] = value
|
132
51
|
end
|
133
52
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
alias_method :original_initialize, :initialize
|
138
|
-
alias_method :initialize, :overriding_initialize
|
139
|
-
private :overriding_initialize, :original_initialize
|
53
|
+
instance = new(**final_kwargs)
|
54
|
+
after_parse_callback&.call(instance)
|
55
|
+
instance
|
140
56
|
end
|
141
|
-
end
|
142
|
-
|
143
|
-
def method_added(name)
|
144
|
-
override_initialize if name == :initialize
|
145
|
-
end
|
146
57
|
|
147
|
-
|
148
|
-
subclass.instance_variable_set :@attribute_names, attribute_names.dup
|
58
|
+
data_class
|
149
59
|
end
|
150
60
|
end
|
151
61
|
end
|
metadata
CHANGED
@@ -1,70 +1,28 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: structure
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Hakan Ensari
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
13
|
-
|
14
|
-
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
20
|
-
type: :development
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: rake
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - ">="
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0'
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - ">="
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: rubocop
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - ">="
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - ">="
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '0'
|
55
|
-
description:
|
56
|
-
email:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies: []
|
12
|
+
description: Provides a DSL for generating immutable Ruby Data objects with type coercion
|
13
|
+
and data transformation capabilities.
|
57
14
|
executables: []
|
58
15
|
extensions: []
|
59
16
|
extra_rdoc_files: []
|
60
17
|
files:
|
61
18
|
- lib/structure.rb
|
19
|
+
- lib/structure/builder.rb
|
20
|
+
- lib/structure/types.rb
|
62
21
|
- lib/structure/version.rb
|
63
|
-
homepage:
|
64
22
|
licenses:
|
65
23
|
- MIT
|
66
|
-
metadata:
|
67
|
-
|
24
|
+
metadata:
|
25
|
+
rubygems_mfa_required: 'true'
|
68
26
|
rdoc_options: []
|
69
27
|
require_paths:
|
70
28
|
- lib
|
@@ -72,15 +30,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
30
|
requirements:
|
73
31
|
- - ">="
|
74
32
|
- !ruby/object:Gem::Version
|
75
|
-
version: '2
|
33
|
+
version: '3.2'
|
76
34
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
35
|
requirements:
|
78
36
|
- - ">="
|
79
37
|
- !ruby/object:Gem::Version
|
80
38
|
version: '0'
|
81
39
|
requirements: []
|
82
|
-
rubygems_version: 3.
|
83
|
-
signing_key:
|
40
|
+
rubygems_version: 3.6.9
|
84
41
|
specification_version: 4
|
85
|
-
summary:
|
42
|
+
summary: Structure your data
|
86
43
|
test_files: []
|