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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de5fcd89773ec6806567c78ea7eebb3867c128c6766149b81e07d82cc4d76ab1
4
- data.tar.gz: 198774e8e6e3d4fea14f387a2862c0d88bd60923866a84a4bbdeaf0fd7be1cd1
3
+ metadata.gz: 7f0f08ca763d757b47a9ff12c51168361a37acdfec5db9d7c45a82c15f9059fb
4
+ data.tar.gz: 7ea79c6470d4fabe005474f572bf3f9564217dca45f18b118babf94c6e8ef36f
5
5
  SHA512:
6
- metadata.gz: 052042efbc411e8d09808f5bdfeb5d2994fd25ba45ddec8d45d06d3cc57b6dc2efd84ec840a20d601618bad494e2ba319c89a579f27e7de8f53c8d59849806be
7
- data.tar.gz: f73e1be62de7fe8540a12f4bb47ded101cf7d7bc4971ab1dbcbbe6917754ee84a82e3cc7cfdb529fd1b1f4c07d62406caa6e7ed7ab5ab528b9f8bb061120d852
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.3.0'
4
+ VERSION = "3.0.0"
5
5
  end
data/lib/structure.rb CHANGED
@@ -1,151 +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
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
- 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
12
+ data_class = Data.define(*builder.attributes)
80
13
 
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
+
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
- module_eval <<-CODE, __FILE__, __LINE__ + 1
107
- def #{name}
108
- with_mutex do
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
- @#{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
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
- def inherited(subclass)
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: 2.3.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: 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: 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.1.2
83
- signing_key:
39
+ rubygems_version: 3.6.9
84
40
  specification_version: 4
85
- summary: Lazy-parse data into attributes in a thread-safe way
41
+ summary: Structure your data!
86
42
  test_files: []