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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba7cfab709c0800d232bcf33dc717b38d884b8ad3322602701247d9af39e4297
4
- data.tar.gz: efe78a271e6ddf19e641180bde8469f78f28c6e6a07fbe7b4cd2214460575336
3
+ metadata.gz: 7f0f08ca763d757b47a9ff12c51168361a37acdfec5db9d7c45a82c15f9059fb
4
+ data.tar.gz: 7ea79c6470d4fabe005474f572bf3f9564217dca45f18b118babf94c6e8ef36f
5
5
  SHA512:
6
- metadata.gz: 499eb639627d1a27d8b80cbb3cab4e1725d41d108620f1166293b8c05c7c361945fb4680c8ac8ba1b56083d7b23cab14cc7ed0d09172f848149b7815b51ed016
7
- data.tar.gz: 5dbea4b6be05e2ed97885c8b3031ce0d643f70722ab5627c6a1b4bdf258607153d9a6cb28b12a710268de1df30872b8cab41ea39a87ca86262f8f9d25c6d926a
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Structure
4
- VERSION = '2.2.0'
4
+ VERSION = "3.0.0"
5
5
  end
data/lib/structure.rb CHANGED
@@ -1,140 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # A tiny library for lazy parsing data with memoized attributes
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
- private
7
-
8
- def included(base)
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
- alias attributes to_h
12
+ data_class = Data.define(*builder.attributes)
32
13
 
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
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
- private :extended
81
- end
82
-
83
- def attribute(name, &block)
84
- name = name.to_s
85
-
86
- if name.end_with?('?')
87
- name = name.chop
88
- module_eval <<-CODE, __FILE__, __LINE__ + 1
89
- def #{name}?
90
- #{name}
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
- @#{name} = unmemoized_#{name}
44
+ # Apply type coercion or transformation
45
+ if types[attr] && !value.nil?
46
+ value = types[attr].call(value)
101
47
  end
102
48
 
103
- @#{name}
49
+ final_kwargs[attr] = value
104
50
  end
105
- CODE
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
- def inherited(subclass)
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: 2.2.0
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: 2019-09-25 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
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
- post_install_message:
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.4'
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.0.3
83
- signing_key:
39
+ rubygems_version: 3.6.9
84
40
  specification_version: 4
85
- summary: Lazy parse data into memoized attributes
41
+ summary: Structure your data!
86
42
  test_files: []