structure 3.4.0 → 3.5.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: 69015a521f4a1a35438cd69d0d195e8f0fa276156d64371c8a6437030b14caf0
4
+ data.tar.gz: d4e5c263a7c92ae2bfbdbe5213b933c55ea1a8f0c9a8d6583302f8c032bb69a9
5
5
  SHA512:
6
- metadata.gz: 3d09a3983e553ccbf3f031c2bb2f920b430032ee5d778b1572f9423e2593808e458662f77615fc84a002136b0896a7bfb5ceff0119d880df1fc833f4123dd493
7
- data.tar.gz: ec6da8a717b8ccab314c808bb9ab3218d733790378f3b61dbdf52dffbf4b28fd7fd7d6ae7b484d390809a8396fccd894b0d087a3d1a90926149bc55db5a98848
6
+ metadata.gz: 1e8ace0197fb9b9cd4811cc1eb7774fe98eff67df140400b567a0a636b9bdabcf2e7858586a7b13f96fbc95e101e2eef74518d9940b1f7c4dab168fec119fb3e
7
+ data.tar.gz: 9d7fa653f6b4aeb60b10bfca4a940677bee89c6803a8e50d893e9dc0ce687306691bbb6f92f4af0a59c89245c3583e8b6d64f477d27b44b71f0f1b7fc359bb26
@@ -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
 
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,88 @@
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)
38
- else
39
- type
40
- 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)
12
+ # Main factory method for creating type coercers
13
+ #
14
+ # @param type [Class, Symbol, Array] 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
+ def coerce(type)
29
+ case type
30
+ when :boolean
31
+ boolean
32
+ when :self
33
+ self_referential
34
+ when Array
35
+ if type.length == 1
36
+ array(type.first)
37
+ else
38
+ type
39
+ end
47
40
  else
48
- type
41
+ if type.respond_to?(:parse)
42
+ parseable(type)
43
+ elsif type.respond_to?(:name) && type.name && Kernel.respond_to?(type.name)
44
+ kernel(type)
45
+ else
46
+ type
47
+ end
49
48
  end
50
49
  end
51
- end
52
50
 
53
- private
51
+ private
54
52
 
55
- def boolean
56
- @boolean ||= ->(val) { BOOLEAN_TRUTHY.include?(val) }
57
- end
53
+ def boolean
54
+ @boolean ||= ->(val) { BOOLEAN_TRUTHY.include?(val) }
55
+ end
58
56
 
59
- def self_referential
60
- proc { |val| parse(val) }
61
- end
57
+ def self_referential
58
+ proc { |val| parse(val) }
59
+ end
62
60
 
63
- def kernel(type)
64
- ->(val) { Kernel.send(type.name, val) }
65
- end
61
+ def kernel(type)
62
+ ->(val) { Kernel.send(type.name, val) }
63
+ end
66
64
 
67
- def parseable(type)
68
- ->(val) { type.parse(val) }
69
- end
65
+ def parseable(type)
66
+ ->(val) { type.parse(val) }
67
+ end
70
68
 
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) } }
69
+ def array(element_type)
70
+ if element_type == :self
71
+ proc do |value|
72
+ unless value.respond_to?(:map)
73
+ raise TypeError, "can't convert #{value.class} into Array"
74
+ end
75
+
76
+ value.map { |element| parse(element) }
77
+ end
78
+ else
79
+ element_coercer = coerce(element_type)
80
+ lambda do |value|
81
+ unless value.respond_to?(:map)
82
+ raise TypeError, "can't convert #{value.class} into Array"
83
+ end
84
+
85
+ value.map { |element| element_coercer.call(element) }
86
+ end
87
+ end
77
88
  end
78
89
  end
79
90
  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.5.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
@@ -47,8 +48,9 @@ module Structure
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,11 +1,15 @@
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]
@@ -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,15 @@ 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
8
8
 
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
9
+ private def self.boolean: () -> Proc
10
+ private def self.self_referential: () -> Proc
11
+ private def self.array: (untyped type) -> Proc
12
+ private def self.parseable: (untyped type) -> Proc
13
+ private def self.kernel: (Class type) -> Proc
14
+ private def self.parse: (untyped val) -> untyped
15
15
  end
16
16
  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.5.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: