structure 3.4.0 → 3.6.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: 2a7bd562939e000315d1904c5f766c0293c570413aa69930e522a01e70849715
4
- data.tar.gz: '0649cee7d47286ad9ba6e36721cc350f20a9ba40c971214e0edb91bde8bec7d9'
3
+ metadata.gz: 5f6aed363a91e514906a1c005fe2af6ec3583fa8742f997055def47e1f085c0c
4
+ data.tar.gz: 574b02c68a6a84f98384d9ba60c8b2d88b95c2cb3db49aa2eccb61e3e1dfc9ab
5
5
  SHA512:
6
- metadata.gz: 3d09a3983e553ccbf3f031c2bb2f920b430032ee5d778b1572f9423e2593808e458662f77615fc84a002136b0896a7bfb5ceff0119d880df1fc833f4123dd493
7
- data.tar.gz: ec6da8a717b8ccab314c808bb9ab3218d733790378f3b61dbdf52dffbf4b28fd7fd7d6ae7b484d390809a8396fccd894b0d087a3d1a90926149bc55db5a98848
6
+ metadata.gz: 0a73835caa30f00145df491efd1bbcedbb2a7ada0cd0b2328cc8c0d9004bd51607328883630529165e9863a9852df40e17414854133a1d1d0144f690e49a50ef
7
+ data.tar.gz: ecb9bc59302300646d8dad3ea1345aecf236aec833e35cf1c83cedbfe5de5369c87965f3877d93e952cbf3536ff603fea2564032a07dba3db77c9872a4d46318
@@ -33,16 +33,13 @@ module Structure
33
33
  # Money.new(value["amount"], value["currency"])
34
34
  # end
35
35
  def attribute(name, type = nil, from: nil, default: nil, &block)
36
- # Always store in mappings - use attribute name as default source
37
- @mappings[name] = from || name.to_s
38
- @defaults[name] = default unless default.nil?
36
+ mappings[name] = from || name.to_s
37
+ defaults[name] = default unless default.nil?
39
38
 
40
39
  if type && block
41
40
  raise ArgumentError, "Cannot specify both type and block for :#{name}"
42
- elsif block
43
- @types[name] = block
44
- elsif type
45
- @types[name] = type
41
+ else
42
+ types[name] = type || block
46
43
  end
47
44
  end
48
45
 
@@ -63,8 +60,8 @@ module Structure
63
60
  @mappings.keys
64
61
  end
65
62
 
66
- def coercions
67
- @types.transform_values { |type| Types.coerce(type) }
63
+ def coercions(context_class = nil)
64
+ @types.transform_values { |type| Types.coerce(type, context_class) }
68
65
  end
69
66
 
70
67
  def predicate_methods
data/lib/structure/rbs.rb CHANGED
@@ -12,12 +12,13 @@ module Structure
12
12
  class_name = klass.name
13
13
  return unless class_name
14
14
 
15
+ # @type var meta: Hash[Symbol, untyped]
15
16
  meta = klass.respond_to?(:__structure_meta__) ? klass.__structure_meta__ : {}
16
17
 
17
18
  emit_rbs_content(
18
19
  class_name: class_name,
19
20
  attributes: meta.fetch(:attributes, klass.members),
20
- types: meta.fetch(:types, {}),
21
+ types: meta.fetch(:types, {}), # steep:ignore
21
22
  has_structure_modules: meta.any?,
22
23
  )
23
24
  end
@@ -35,20 +36,18 @@ module Structure
35
36
  dir_path = dir_path.join(*path_segments) unless path_segments.empty?
36
37
  FileUtils.mkdir_p(dir_path)
37
38
 
38
- file_path = dir_path.join(filename)
39
+ file_path = dir_path.join(filename).to_s
39
40
  File.write(file_path, rbs_content)
40
41
 
41
- file_path.to_s
42
+ file_path
42
43
  end
43
44
 
44
45
  private
45
46
 
46
47
  def emit_rbs_content(class_name:, attributes:, types:, has_structure_modules:)
48
+ # @type var lines: Array[String]
47
49
  lines = []
48
50
  lines << "class #{class_name} < Data"
49
- lines << " extend Structure::ClassMethods" if has_structure_modules
50
- lines << " include Structure::InstanceMethods"
51
- lines << ""
52
51
 
53
52
  unless attributes.empty?
54
53
  # map types to rbs
@@ -66,6 +65,28 @@ module Structure
66
65
  lines << " | (#{positional_params}) -> instance"
67
66
  lines << ""
68
67
 
68
+ needs_parse_data = types.any? do |_attr, type|
69
+ type == :self || type == [:self] || (type.is_a?(Array) && type.first == :array)
70
+ end
71
+
72
+ if needs_parse_data
73
+ lines << " type parse_data = {"
74
+ attributes.each do |attr|
75
+ type = types.fetch(attr, nil)
76
+ parse_type = parse_data_type(type, class_name)
77
+ lines << " ?#{attr}: #{parse_type},"
78
+ end
79
+ lines[-1] = lines[-1].chomp(",")
80
+ lines << " }"
81
+ lines << ""
82
+ lines << " def self.parse: (?parse_data data) -> instance"
83
+ lines << " | (?Hash[String, untyped] data) -> instance"
84
+ else
85
+ # For structures without special types, just use Hash
86
+ lines << " def self.parse: (?(Hash[String | Symbol, untyped]), **untyped) -> instance"
87
+ end
88
+ lines << ""
89
+
69
90
  attributes.each do |attr|
70
91
  lines << " attr_reader #{attr}: #{rbs_types[attr]}"
71
92
  end
@@ -85,6 +106,33 @@ module Structure
85
106
  lines.join("\n")
86
107
  end
87
108
 
109
+ def parse_data_type(type, class_name)
110
+ case type
111
+ when [:self]
112
+ "Array[#{class_name} | parse_data]"
113
+ when Array
114
+ if type.first == :array && type.last == :self
115
+ "Array[#{class_name} | parse_data]"
116
+ elsif type.first == :array
117
+ # For [:array, SomeType] format, use Array[untyped] since we coerce
118
+ "Array[untyped]"
119
+ elsif type.size == 1 && type.first == :self
120
+ # [:self] is handled above, this shouldn't happen
121
+ "Array[#{class_name} | parse_data]"
122
+ elsif type.size == 1
123
+ # Regular array type like [String], [Integer], etc.
124
+ # Use Array[untyped] since we coerce values
125
+ "Array[untyped]"
126
+ else
127
+ "untyped"
128
+ end
129
+ when :self
130
+ "#{class_name} | parse_data"
131
+ else
132
+ "untyped"
133
+ end
134
+ end
135
+
88
136
  def map_type_to_rbs(type, class_name)
89
137
  case type
90
138
  when Class
@@ -97,8 +145,10 @@ module Structure
97
145
  if type.size == 2 && type.first == :array
98
146
  element_type = map_type_to_rbs(type.last, class_name)
99
147
  "Array[#{element_type}]"
100
- elsif type == [:self]
101
- "Array[#{class_name || "untyped"}]"
148
+ elsif type.size == 1
149
+ # Single element array means array of that type
150
+ element_type = map_type_to_rbs(type.first, class_name)
151
+ "Array[#{element_type}]"
102
152
  else
103
153
  "untyped"
104
154
  end
@@ -3,77 +3,139 @@
3
3
  module Structure
4
4
  # Type coercion methods for converting values to specific types
5
5
  module Types
6
- extend self
6
+ class << self
7
+ # Rails-style boolean truthy values
8
+ # Reference: https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
9
+ BOOLEAN_TRUTHY = [true, 1, "1", "t", "T", "true", "TRUE", "on", "ON"].freeze
10
+ private_constant :BOOLEAN_TRUTHY
7
11
 
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
- # Main factory method for creating type coercers
14
- #
15
- # @param type [Class, Symbol, Array] Type specification
16
- # @return [Proc, Object] Coercion proc or the type itself if no coercion available
17
- #
18
- # @example Boolean type
19
- # coerce(:boolean) # => boolean proc
20
- #
21
- # @example Kernel types
22
- # coerce(Integer) # => proc that calls Kernel.Integer
23
- #
24
- # @example Parseable types
25
- # coerce(Date) # => proc that calls Date.parse
26
- #
27
- # @example Array types
28
- # coerce([String]) # => proc that coerces array elements to String
29
- def coerce(type)
30
- case type
31
- when :boolean
32
- boolean
33
- when :self
34
- self_referential
35
- when Array
36
- if type.length == 1
37
- array(type.first)
12
+ # Main factory method for creating type coercers
13
+ #
14
+ # @param type [Class, Symbol, Array, String] Type specification
15
+ # @return [Proc, Object] Coercion proc or the type itself if no coercion available
16
+ #
17
+ # @example Boolean type
18
+ # coerce(:boolean) # => boolean proc
19
+ #
20
+ # @example Kernel types
21
+ # coerce(Integer) # => proc that calls Kernel.Integer
22
+ #
23
+ # @example Parseable types
24
+ # coerce(Date) # => proc that calls Date.parse
25
+ #
26
+ # @example Array types
27
+ # coerce([String]) # => proc that coerces array elements to String
28
+ #
29
+ # @example String class name (lazy resolved)
30
+ # coerce("MyClass") # => proc that resolves and coerces to MyClass
31
+ def coerce(type, context_class = nil)
32
+ case type
33
+ when :boolean
34
+ boolean
35
+ when :self
36
+ self_referential
37
+ when String
38
+ string_class(type, context_class)
39
+ when Array
40
+ if type.length == 1
41
+ array(type.first, context_class)
42
+ else
43
+ type
44
+ end
38
45
  else
39
- type
46
+ if type.respond_to?(:parse)
47
+ parseable(type)
48
+ elsif type.respond_to?(:name) && type.name && Kernel.respond_to?(type.name)
49
+ kernel(type)
50
+ else
51
+ type
52
+ end
40
53
  end
41
- else
42
- # Handle Class, Module, and any other types
43
- if type.respond_to?(:parse)
44
- parseable(type)
45
- elsif type.respond_to?(:name) && type.name && Kernel.respond_to?(type.name)
46
- kernel(type)
54
+ end
55
+
56
+ def resolve_class(class_name, context_class)
57
+ if context_class && defined?(context_class.name)
58
+ namespace = context_class.name.to_s.split("::")[0...-1]
59
+ if namespace.any?
60
+ begin
61
+ namespace.reduce(Object) { |mod, name| mod.const_get(name) }.const_get(class_name)
62
+ rescue NameError
63
+ Object.const_get(class_name)
64
+ end
65
+ else
66
+ Object.const_get(class_name)
67
+ end
47
68
  else
48
- type
69
+ Object.const_get(class_name)
49
70
  end
71
+ rescue NameError => e
72
+ raise NameError, "Unable to resolve class '#{class_name}': #{e.message}"
50
73
  end
51
- end
52
74
 
53
- private
75
+ private
54
76
 
55
- def boolean
56
- @boolean ||= ->(val) { BOOLEAN_TRUTHY.include?(val) }
57
- end
77
+ def boolean
78
+ @boolean ||= ->(val) { BOOLEAN_TRUTHY.include?(val) }
79
+ end
58
80
 
59
- def self_referential
60
- proc { |val| parse(val) }
61
- end
81
+ def self_referential
82
+ proc { |val| parse(val) }
83
+ end
62
84
 
63
- def kernel(type)
64
- ->(val) { Kernel.send(type.name, val) }
65
- end
85
+ def kernel(type)
86
+ ->(val) { Kernel.send(type.name, val) }
87
+ end
66
88
 
67
- def parseable(type)
68
- ->(val) { type.parse(val) }
69
- end
89
+ def parseable(type)
90
+ ->(val) { type.parse(val) }
91
+ end
92
+
93
+ def string_class(class_name, context_class)
94
+ resolved_class = nil
95
+ proc do |value|
96
+ resolved_class ||= Structure::Types.resolve_class(class_name, context_class)
97
+ if resolved_class.respond_to?(:parse)
98
+ resolved_class.parse(value) # steep:ignore
99
+ else
100
+ value
101
+ end
102
+ end
103
+ end
70
104
 
71
- def array(element_type)
72
- if element_type == :self
73
- proc { |array| array.map { |element| parse(element) } }
74
- else
75
- element_coercer = coerce(element_type)
76
- ->(array) { array.map { |element| element_coercer.call(element) } }
105
+ def array(element_type, context_class = nil)
106
+ if element_type == :self
107
+ proc do |value|
108
+ unless value.respond_to?(:map)
109
+ raise TypeError, "can't convert #{value.class} into Array"
110
+ end
111
+
112
+ value.map { |element| parse(element) }
113
+ end
114
+ elsif element_type.is_a?(String)
115
+ proc do |value|
116
+ unless value.respond_to?(:map)
117
+ raise TypeError, "can't convert #{value.class} into Array"
118
+ end
119
+
120
+ resolved_class = Structure::Types.resolve_class(element_type, context_class)
121
+ value.map do |element|
122
+ if resolved_class.respond_to?(:parse)
123
+ resolved_class.parse(element)
124
+ else
125
+ element
126
+ end
127
+ end
128
+ end
129
+ else
130
+ element_coercer = coerce(element_type, context_class)
131
+ lambda do |value|
132
+ unless value.respond_to?(:map)
133
+ raise TypeError, "can't convert #{value.class} into Array"
134
+ end
135
+
136
+ value.map { |element| element_coercer.call(element) }
137
+ end
138
+ end
77
139
  end
78
140
  end
79
141
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Structure
4
- VERSION = "3.4.0"
4
+ VERSION = "3.6.0"
5
5
  end
data/lib/structure.rb CHANGED
@@ -23,6 +23,7 @@ module Structure
23
23
  builder = Builder.new
24
24
  builder.instance_eval(&block) if block
25
25
 
26
+ # @type var klass: untyped
26
27
  klass = Data.define(*builder.attributes)
27
28
 
28
29
  # capture metadata and attach to class
@@ -36,19 +37,20 @@ module Structure
36
37
 
37
38
  # capture locals for method generation
38
39
  mappings = builder.mappings
39
- coercions = builder.coercions
40
+ coercions = builder.coercions(klass)
40
41
  predicates = builder.predicate_methods
41
42
  after = builder.after_parse_callback
42
43
 
43
44
  # Define predicate methods
44
45
  predicates.each do |pred, attr|
45
- klass.define_method(pred) { public_send(attr) }
46
+ klass.define_method(pred) { !!public_send(attr) }
46
47
  end
47
48
 
48
49
  # recursive to_h
49
50
  klass.define_method(:to_h) do
50
- #: Hash[Symbol, untyped]
51
- self.class.members.each_with_object({}) do |m, h|
51
+ # @type var h: Hash[Symbol, untyped]
52
+ h = {}
53
+ klass.members.each do |m|
52
54
  v = public_send(m)
53
55
  h[m] =
54
56
  case v
@@ -57,23 +59,38 @@ module Structure
57
59
  else v
58
60
  end
59
61
  end
62
+ h
60
63
  end
61
64
 
62
65
  # parse accepts JSON-ish hashes + kwargs override
63
66
  klass.define_singleton_method(:parse) do |data = {}, **kwargs|
64
- string_kwargs = kwargs.transform_keys(&:to_s)
65
- data = data.merge(string_kwargs)
67
+ return data if data.is_a?(self)
68
+
69
+ unless data.respond_to?(:merge!)
70
+ raise TypeError, "can't convert #{data.class} into #{self}"
71
+ end
66
72
 
67
- #: Hash[Symbol, untyped]
73
+ # @type var kwargs: Hash[Symbol, untyped]
74
+ string_kwargs = kwargs.transform_keys(&:to_s)
75
+ data.merge!(string_kwargs)
76
+ # @type self: singleton(Data) & _StructuredDataClass
77
+ # @type var final: Hash[Symbol, untyped]
68
78
  final = {}
79
+
80
+ # TODO: `__structure_meta__` exists but seems not to return the types it defines, so going untyped for now
81
+ #
82
+ # @type var meta: untyped
69
83
  meta = __structure_meta__
70
84
 
71
- meta[:attributes].each do |attr|
85
+ attributes = meta.fetch(:attributes)
86
+ defaults = meta.fetch(:defaults)
87
+
88
+ attributes.each do |attr|
72
89
  source = mappings[attr] || attr.to_s
73
90
  value =
74
91
  if data.key?(source) then data[source]
75
92
  elsif data.key?(source.to_sym) then data[source.to_sym]
76
- elsif meta[:defaults].key?(attr) then meta[:defaults][attr]
93
+ elsif defaults.key?(attr) then defaults[attr]
77
94
  end
78
95
 
79
96
  coercion = coercions[attr]
@@ -81,7 +98,7 @@ module Structure
81
98
  # self-referential types need class context to call parse
82
99
  value =
83
100
  if coercion.is_a?(Proc) && !coercion.lambda?
84
- instance_exec(value, &coercion)
101
+ instance_exec(value, &coercion) # steep:ignore
85
102
  else
86
103
  coercion.call(value)
87
104
  end
@@ -90,7 +107,6 @@ module Structure
90
107
  final[attr] = value
91
108
  end
92
109
 
93
- #: untyped
94
110
  obj = new(**final)
95
111
  after&.call(obj)
96
112
  obj
@@ -1,16 +1,20 @@
1
1
  module Structure
2
2
  class Builder
3
- def attribute: (Symbol name, untyped type, ?from: String, ?default: untyped) ?{ (untyped) -> untyped } -> void
3
+ @mappings: Hash[Symbol, String]
4
+ @types: Hash[Symbol, untyped]
5
+ @defaults: Hash[Symbol, untyped]
6
+ @after_parse_callback: Proc?
7
+
8
+ def attribute: (Symbol name, untyped type, ?from: String?, ?default: untyped) ?{ (untyped) -> untyped } -> void
4
9
  | (Symbol name, ?from: String, ?default: untyped) ?{ (untyped) -> untyped } -> void
5
10
 
6
11
  def after_parse: () { (Data) -> void } -> void
7
12
 
8
- # Methods used by the factory after the block
9
13
  def attributes: () -> Array[Symbol]
10
14
  def mappings: () -> Hash[Symbol, String]
11
15
  def types: () -> Hash[Symbol, untyped]
12
16
  def defaults: () -> Hash[Symbol, untyped]
13
- def coercions: () -> Hash[Symbol, Proc]
17
+ def coercions: (?untyped? context_class) -> Hash[Symbol, Proc]
14
18
  def predicate_methods: () -> Hash[Symbol, Symbol]
15
19
  def after_parse_callback: () -> (Proc | nil)
16
20
  end
@@ -1,11 +1,10 @@
1
1
  module Structure
2
2
  module RBS
3
- def self.emit: (Class klass) -> String?
4
- def self.write: (Class klass, ?dir: String) -> String?
3
+ def self.emit: (untyped klass) -> String?
4
+ def self.write: (untyped klass, ?dir: String) -> String?
5
5
 
6
- private
7
-
8
- def self.emit_rbs_content: (class_name: String, attributes: Array[Symbol], types: Hash[Symbol, untyped], has_structure_modules: bool) -> String
9
- def self.map_type_to_rbs: (untyped type, String class_name) -> String
6
+ private def self.emit_rbs_content: (class_name: String, attributes: Array[Symbol], types: Hash[Symbol, untyped], has_structure_modules: bool) -> String
7
+ private def self.parse_data_type: (untyped type, String class_name) -> String
8
+ private def self.map_type_to_rbs: (untyped type, String class_name) -> String
10
9
  end
11
10
  end
@@ -2,15 +2,17 @@ module Structure
2
2
  module Types
3
3
  BOOLEAN_TRUTHY: Array[untyped]
4
4
 
5
- def self.coerce: (untyped type) -> Proc
5
+ self.@boolean: Proc
6
6
 
7
- private
7
+ def self.coerce: (untyped type, ?untyped? context_class) -> untyped
8
+ def self.resolve_class: (String class_name, untyped? context_class) -> untyped
8
9
 
9
- def self.boolean: () -> Proc
10
- def self.self_referential: () -> Proc
11
- def self.array: (untyped element_type) -> Proc
12
- def self.parseable: (Class type) -> Proc
13
- def self.kernel: (Class type) -> Proc
14
- def self.parse: (untyped val) -> untyped
10
+ private def self.boolean: () -> Proc
11
+ private def self.self_referential: () -> Proc
12
+ private def self.string_class: (String class_name, untyped? context_class) -> Proc
13
+ private def self.array: (untyped element_type, ?untyped? context_class) -> Proc
14
+ private def self.parseable: (untyped type) -> Proc
15
+ private def self.kernel: (Class type) -> Proc
16
+ private def self.parse: (untyped val) -> untyped
15
17
  end
16
18
  end
@@ -0,0 +1,3 @@
1
+ module Structure
2
+ VERSION: String
3
+ end
data/sig/structure.rbs CHANGED
@@ -1,21 +1,12 @@
1
1
  module Structure
2
- def self.new: () ?{ () [self: Structure::Builder] -> void } -> singleton(Data)
3
-
4
- module ClassMethods
5
- def parse: (?(Hash[String | Symbol, untyped] | nil) data, **untyped kwargs) -> instance
6
- def members: () -> Array[Symbol]
2
+ interface _StructuredDataClass
7
3
  def __structure_meta__: () -> {
8
4
  attributes: Array[Symbol],
9
5
  types: Hash[Symbol, untyped],
10
- defaults: Hash[Symbol, untyped],
11
- from: Hash[Symbol, String]
6
+ defaults: Hash[Symbol, untyped]
12
7
  }
8
+ def parse: (?Hash[String | Symbol, untyped] data, **untyped kwargs) -> instance
13
9
  end
14
10
 
15
- module InstanceMethods
16
- def deconstruct: () -> Array[untyped]
17
- def deconstruct_keys: (Array[Symbol]?) -> Hash[Symbol, untyped]
18
- def with: (**untyped) -> instance
19
- def to_h: () -> Hash[Symbol, untyped]
20
- end
11
+ def self.new: () ?{ (Structure::Builder) [self: Structure::Builder] -> void } -> untyped
21
12
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: structure
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.0
4
+ version: 3.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hakan Ensari
@@ -24,6 +24,7 @@ files:
24
24
  - sig/structure/builder.rbs
25
25
  - sig/structure/rbs.rbs
26
26
  - sig/structure/types.rbs
27
+ - sig/structure/version.rbs
27
28
  licenses:
28
29
  - MIT
29
30
  metadata: