structure 2.2.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 -122
- 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,140 +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
|
8
|
+
def new(&block)
|
9
|
+
builder = Builder.new
|
10
|
+
builder.instance_eval(&block) if block
|
30
11
|
|
31
|
-
|
12
|
+
data_class = Data.define(*builder.attributes)
|
32
13
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
63
|
-
|
64
|
-
private
|
65
|
-
|
66
|
-
def with_mutex(&block)
|
67
|
-
@mutex.owned? ? block.call : @mutex.synchronize { block.call }
|
68
|
-
end
|
69
|
-
|
70
|
-
# The class interface
|
71
|
-
module ClassMethods
|
72
|
-
attr_reader :attribute_names
|
73
|
-
|
74
|
-
class << self
|
75
|
-
def extended(base)
|
76
|
-
base.instance_variable_set :@attribute_names, []
|
77
|
-
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
|
78
19
|
end
|
79
20
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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]
|
91
42
|
end
|
92
|
-
CODE
|
93
|
-
end
|
94
|
-
|
95
|
-
module_eval <<-CODE, __FILE__, __LINE__ + 1
|
96
|
-
def #{name}
|
97
|
-
with_mutex do
|
98
|
-
break if defined?(@#{name})
|
99
43
|
|
100
|
-
|
44
|
+
# Apply type coercion or transformation
|
45
|
+
if types[attr] && !value.nil?
|
46
|
+
value = types[attr].call(value)
|
101
47
|
end
|
102
48
|
|
103
|
-
|
49
|
+
final_kwargs[attr] = value
|
104
50
|
end
|
105
|
-
|
106
|
-
private define_method "unmemoized_#{name}", block
|
107
|
-
@attribute_names << name
|
108
|
-
|
109
|
-
name.to_sym
|
110
|
-
end
|
111
|
-
|
112
|
-
private
|
113
|
-
|
114
|
-
def override_initialize
|
115
|
-
class_eval do
|
116
|
-
unless method_defined?(:overriding_initialize)
|
117
|
-
define_method :overriding_initialize do |*arguments, &block|
|
118
|
-
@mutex = ::Thread::Mutex.new
|
119
|
-
original_initialize(*arguments, &block)
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
return if instance_method(:initialize) ==
|
124
|
-
instance_method(:overriding_initialize)
|
125
|
-
|
126
|
-
alias_method :original_initialize, :initialize
|
127
|
-
alias_method :initialize, :overriding_initialize
|
128
|
-
private :overriding_initialize, :original_initialize
|
51
|
+
new(**final_kwargs)
|
129
52
|
end
|
130
|
-
end
|
131
|
-
|
132
|
-
def method_added(name)
|
133
|
-
override_initialize if name == :initialize
|
134
|
-
end
|
135
53
|
|
136
|
-
|
137
|
-
subclass.instance_variable_set :@attribute_names, attribute_names.dup
|
54
|
+
data_class
|
138
55
|
end
|
139
56
|
end
|
140
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: []
|