structure 2.3.0 → 3.0.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 +45 -0
- data/lib/structure/types.rb +58 -0
- data/lib/structure/version.rb +1 -1
- data/lib/structure.rb +39 -133
- metadata +11 -55
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7f0f08ca763d757b47a9ff12c51168361a37acdfec5db9d7c45a82c15f9059fb
|
4
|
+
data.tar.gz: 7ea79c6470d4fabe005474f572bf3f9564217dca45f18b118babf94c6e8ef36f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c37505be3556ec54d0277008506868f4fade809f8baa800336f54b32ceca265c61ee232ceca7326b2104793d6af8a6022fb193c61fbb28aaeb9d0493223b6677
|
7
|
+
data.tar.gz: 725e923fddcb8cca372340d12c9cf67c38d20782dac2a1fc2b1273e06e12d6cc712668c6a634d32af4661d8b70c8369e25fda5109e6e80e97ad65ddc0babf8d7
|
@@ -0,0 +1,45 @@
|
|
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
|
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
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,58 @@
|
|
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
|
+
def boolean
|
15
|
+
@boolean ||= ->(val) { BOOLEAN_TRUTHY.include?(val) }
|
16
|
+
end
|
17
|
+
|
18
|
+
# Generic handler for classes with kernel methods (String, Integer, Float, etc.)
|
19
|
+
def kernel(type)
|
20
|
+
->(val) { Kernel.send(type.name, val) }
|
21
|
+
end
|
22
|
+
|
23
|
+
# Handler for nested Structure classes
|
24
|
+
def structure(type)
|
25
|
+
->(val) { type.parse(val) }
|
26
|
+
end
|
27
|
+
|
28
|
+
# Create coercer for array elements
|
29
|
+
def array(element_type)
|
30
|
+
element_coercer = coerce(element_type)
|
31
|
+
->(array) { array.map { |element| element_coercer.call(element) } }
|
32
|
+
end
|
33
|
+
|
34
|
+
# Main factory method for creating type coercers
|
35
|
+
def coerce(type)
|
36
|
+
case type
|
37
|
+
when :boolean
|
38
|
+
boolean
|
39
|
+
when Class
|
40
|
+
if type.name && Kernel.respond_to?(type.name)
|
41
|
+
kernel(type)
|
42
|
+
elsif type.respond_to?(:parse)
|
43
|
+
structure(type)
|
44
|
+
else
|
45
|
+
type
|
46
|
+
end
|
47
|
+
when Array
|
48
|
+
if type.length == 1
|
49
|
+
array(type.first)
|
50
|
+
else
|
51
|
+
type
|
52
|
+
end
|
53
|
+
else
|
54
|
+
type
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/structure/version.rb
CHANGED
data/lib/structure.rb
CHANGED
@@ -1,151 +1,57 @@
|
|
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
|
32
|
-
|
33
|
-
def inspect
|
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
|
39
|
-
|
40
|
-
"#<#{self.class.name || '?'} #{detail}>"
|
41
|
-
end
|
42
|
-
|
43
|
-
alias to_s inspect
|
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
|
8
|
+
def new(&block)
|
9
|
+
builder = Builder.new
|
10
|
+
builder.instance_eval(&block) if block
|
63
11
|
|
64
|
-
|
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
|
12
|
+
data_class = Data.define(*builder.attributes)
|
80
13
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
+
|
27
|
+
data_class.define_singleton_method(:parse) do |data = {}, **kwargs|
|
28
|
+
# Merge kwargs into data - kwargs take priority as overrides
|
29
|
+
# Convert kwargs symbol keys to strings to match source_key lookups
|
30
|
+
string_kwargs = kwargs.transform_keys(&:to_s)
|
31
|
+
data = data.merge(string_kwargs)
|
32
|
+
|
33
|
+
final_kwargs = {}
|
34
|
+
attributes.each do |attr|
|
35
|
+
source_key = mappings[attr]
|
36
|
+
value = if data.key?(source_key)
|
37
|
+
data[source_key]
|
38
|
+
elsif data.key?(source_key.to_sym)
|
39
|
+
data[source_key.to_sym]
|
40
|
+
elsif defaults.key?(attr)
|
41
|
+
defaults[attr]
|
102
42
|
end
|
103
|
-
CODE
|
104
|
-
end
|
105
43
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
break if defined?(@#{name})
|
110
|
-
|
111
|
-
@#{name} = unmemoized_#{name}
|
44
|
+
# Apply type coercion or transformation
|
45
|
+
if types[attr] && !value.nil?
|
46
|
+
value = types[attr].call(value)
|
112
47
|
end
|
113
48
|
|
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
|
49
|
+
final_kwargs[attr] = value
|
132
50
|
end
|
133
|
-
|
134
|
-
return if instance_method(:initialize) ==
|
135
|
-
instance_method(:overriding_initialize)
|
136
|
-
|
137
|
-
alias_method :original_initialize, :initialize
|
138
|
-
alias_method :initialize, :overriding_initialize
|
139
|
-
private :overriding_initialize, :original_initialize
|
51
|
+
new(**final_kwargs)
|
140
52
|
end
|
141
|
-
end
|
142
|
-
|
143
|
-
def method_added(name)
|
144
|
-
override_initialize if name == :initialize
|
145
|
-
end
|
146
53
|
|
147
|
-
|
148
|
-
subclass.instance_variable_set :@attribute_names, attribute_names.dup
|
54
|
+
data_class
|
149
55
|
end
|
150
56
|
end
|
151
57
|
end
|
metadata
CHANGED
@@ -1,70 +1,27 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: structure
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.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
|
-
name: minitest
|
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: Parse data into immutable Ruby Data objects with type coercion
|
57
13
|
executables: []
|
58
14
|
extensions: []
|
59
15
|
extra_rdoc_files: []
|
60
16
|
files:
|
61
17
|
- lib/structure.rb
|
18
|
+
- lib/structure/builder.rb
|
19
|
+
- lib/structure/types.rb
|
62
20
|
- lib/structure/version.rb
|
63
|
-
homepage:
|
64
21
|
licenses:
|
65
22
|
- MIT
|
66
|
-
metadata:
|
67
|
-
|
23
|
+
metadata:
|
24
|
+
rubygems_mfa_required: 'true'
|
68
25
|
rdoc_options: []
|
69
26
|
require_paths:
|
70
27
|
- lib
|
@@ -72,15 +29,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
29
|
requirements:
|
73
30
|
- - ">="
|
74
31
|
- !ruby/object:Gem::Version
|
75
|
-
version: '2
|
32
|
+
version: '3.2'
|
76
33
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
34
|
requirements:
|
78
35
|
- - ">="
|
79
36
|
- !ruby/object:Gem::Version
|
80
37
|
version: '0'
|
81
38
|
requirements: []
|
82
|
-
rubygems_version: 3.
|
83
|
-
signing_key:
|
39
|
+
rubygems_version: 3.6.9
|
84
40
|
specification_version: 4
|
85
|
-
summary:
|
41
|
+
summary: Structure your data!
|
86
42
|
test_files: []
|