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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de5fcd89773ec6806567c78ea7eebb3867c128c6766149b81e07d82cc4d76ab1
4
- data.tar.gz: 198774e8e6e3d4fea14f387a2862c0d88bd60923866a84a4bbdeaf0fd7be1cd1
3
+ metadata.gz: af5b8b6dc3dacef6201847123b04742310441ded70ec7952b42d07a668d83f2f
4
+ data.tar.gz: '00385227416285e5a4d1f2cfcf2cc0e5cb94bb368cf8995b014a7bf3fd91d9c6'
5
5
  SHA512:
6
- metadata.gz: 052042efbc411e8d09808f5bdfeb5d2994fd25ba45ddec8d45d06d3cc57b6dc2efd84ec840a20d601618bad494e2ba319c89a579f27e7de8f53c8d59849806be
7
- data.tar.gz: f73e1be62de7fe8540a12f4bb47ded101cf7d7bc4971ab1dbcbbe6917754ee84a82e3cc7cfdb529fd1b1f4c07d62406caa6e7ed7ab5ab528b9f8bb061120d852
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Structure
4
- VERSION = '2.3.0'
4
+ VERSION = "3.1.0"
5
5
  end
data/lib/structure.rb CHANGED
@@ -1,151 +1,61 @@
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
30
-
31
- alias attributes to_h
8
+ def new(&block)
9
+ builder = Builder.new
10
+ builder.instance_eval(&block) if block
32
11
 
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
12
+ data_class = Data.define(*builder.attributes)
39
13
 
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
- 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
- private :extended
92
- end
93
-
94
- def attribute(name, &block)
95
- name = name.to_s
96
-
97
- if name.end_with?('?')
98
- name = name.chop
99
- module_eval <<-CODE, __FILE__, __LINE__ + 1
100
- def #{name}?
101
- #{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
+ 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
- @#{name} = unmemoized_#{name}
45
+ # Apply type coercion or transformation
46
+ if types[attr] && !value.nil?
47
+ value = types[attr].call(value)
112
48
  end
113
49
 
114
- @#{name}
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
- 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
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
- def inherited(subclass)
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: 2.3.0
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: 2020-01-24 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: 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
- post_install_message:
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.4'
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.1.2
83
- signing_key:
40
+ rubygems_version: 3.6.9
84
41
  specification_version: 4
85
- summary: Lazy-parse data into attributes in a thread-safe way
42
+ summary: Structure your data
86
43
  test_files: []