structure 3.7.0 → 4.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: 272646d259a42b8a13d4c0fbc29072022da5260f5a7948d2f8dbaddb9f625adf
4
- data.tar.gz: a3045255ecb35adc801f827c48a5c620dc90943aee453f474bae6ab85bc8b58d
3
+ metadata.gz: 74f52e94d4ffd40ad55e291a4ab72c108af572be936ee2d1571fef47699af2cb
4
+ data.tar.gz: d2ef8dee35e2ea236237d1c34c68bc6877039145553f7e1b16729ea84e3778c4
5
5
  SHA512:
6
- metadata.gz: 4d5914cb2446f277c61369d156d8286cfc50b866423b7bc86523964216d76a677901848a3135a5f1061f0383569fb3ad03624c3806de9d7841281bd751e5aa16
7
- data.tar.gz: 3684e0a22399da848c493eb6b837d0c8427f0dd6f24857a7f65812914366b448108da8af2b377fc937a08715ea13de633b3f538f6b843a288c65936123dd32b7
6
+ metadata.gz: f0c4192ddde552c7cd4a583aca9bdad31aeb64dc039680e965944696ee7648cc9a423017ab8cea5546c708ae72b7920c21fb2c96e74442932f24a3a85faa6b81
7
+ data.tar.gz: 03d4ea5fda959cd3bcd60b79a323b8dbfaeb5792c22b809179b7f84f76aab36845e7dbcd2f30567cf7922eea9995997cfb730cd69e8dd726cebded2bd076f540
@@ -13,6 +13,7 @@ module Structure
13
13
  @mappings = {}
14
14
  @defaults = {}
15
15
  @types = {}
16
+ @optional = Set.new
16
17
  end
17
18
 
18
19
  # DSL method for defining attributes with optional type coercion
@@ -45,6 +46,25 @@ module Structure
45
46
  end
46
47
  end
47
48
 
49
+ # DSL method for defining optional attributes (key can be missing from input hash)
50
+ #
51
+ # @param name [Symbol] The attribute name
52
+ # @param type [Class, Symbol, Array, nil] Type for coercion (e.g., String, :boolean, [String])
53
+ # @param from [String, nil] Source key in the data hash (defaults to name.to_s)
54
+ # @param default [Object, nil] Default value if attribute is missing
55
+ # @yield [value] Block for custom transformation
56
+ # @raise [ArgumentError] If both type and block are provided
57
+ #
58
+ # @example Optional attribute
59
+ # attribute? :age, Integer
60
+ #
61
+ # @example Optional with default
62
+ # attribute? :status, String, default: "pending"
63
+ def attribute?(name, type = nil, from: nil, default: nil, &block)
64
+ attribute(name, type, from: from, default: default, &block)
65
+ @optional.add(name)
66
+ end
67
+
48
68
  # Defines a callback to run after parsing
49
69
  #
50
70
  # @yield [instance] Block that receives the parsed instance
@@ -59,9 +79,13 @@ module Structure
59
79
  end
60
80
 
61
81
  # @api private
62
- def attributes
63
- @mappings.keys
64
- end
82
+ def attributes = @mappings.keys
83
+
84
+ # @api private
85
+ def optional = @optional.to_a
86
+
87
+ # @api private
88
+ def required = attributes - optional
65
89
 
66
90
  # @api private
67
91
  def coercions(context = nil)
data/lib/structure/rbs.rb CHANGED
@@ -4,6 +4,10 @@ require "fileutils"
4
4
  require "pathname"
5
5
 
6
6
  module Structure
7
+ # Generates RBS type signatures for Structure classes
8
+ #
9
+ # Note: Custom methods defined in Structure blocks are not included and must be manually added to RBS files. This is
10
+ # consistent with how Ruby's RBS tooling handles Data classes.
7
11
  module RBS
8
12
  class << self
9
13
  def emit(klass)
@@ -17,11 +21,13 @@ module Structure
17
21
 
18
22
  attributes = meta[:mappings] ? meta[:mappings].keys : klass.members
19
23
  types = meta.fetch(:types, {}) # steep:ignore
24
+ required = meta.fetch(:required, attributes) # steep:ignore
20
25
 
21
26
  emit_rbs_content(
22
27
  class_name:,
23
28
  attributes:,
24
29
  types:,
30
+ required:,
25
31
  has_structure_modules: meta.any?,
26
32
  )
27
33
  end
@@ -47,7 +53,7 @@ module Structure
47
53
 
48
54
  private
49
55
 
50
- def emit_rbs_content(class_name:, attributes:, types:, has_structure_modules:)
56
+ def emit_rbs_content(class_name:, attributes:, types:, required:, has_structure_modules:)
51
57
  # @type var lines: Array[String]
52
58
  lines = []
53
59
  lines << "class #{class_name} < Data"
@@ -61,46 +67,66 @@ module Structure
61
67
  [attr, rbs_type != "untyped" ? "#{rbs_type}?" : rbs_type]
62
68
  end.to_h
63
69
 
64
- keyword_params = attributes.map { |attr| "#{attr}: #{rbs_types[attr]}" }.join(", ")
70
+ # Sort keyword params: required first, then optional (with ? prefix)
71
+ # Within each group, maintain declaration order
72
+ required_params = required.map { |attr| "#{attr}: #{rbs_types[attr]}" }
73
+ optional_params = (attributes - required).map { |attr| "?#{attr}: #{rbs_types[attr]}" }
74
+ keyword_params = (required_params + optional_params).join(", ")
65
75
  positional_params = attributes.map { |attr| rbs_types[attr] }.join(", ")
66
76
 
67
- lines << " def self.new: (#{keyword_params}) -> #{class_name}"
68
- lines << " | (#{positional_params}) -> #{class_name}"
69
- lines << ""
70
-
71
77
  needs_parse_data = types.any? do |_attr, type|
72
78
  type == :self || type == [:self]
73
79
  end
74
80
 
81
+ # Generate type alias first if needed (RBS::Sorter puts types at top)
75
82
  if needs_parse_data
76
- lines << " type parse_data = {"
77
- attributes.each do |attr|
83
+ lines << " type parse_data = { " + attributes.map { |attr|
78
84
  type = types.fetch(attr, nil)
79
85
  parse_type = parse_data_type(type, class_name)
80
- lines << " ?#{attr}: #{parse_type},"
81
- end
82
- lines[-1] = lines[-1].chomp(",")
83
- lines << " }"
84
- lines << ""
86
+ "?#{attr}: #{parse_type}"
87
+ }.join(", ") + " }"
88
+ end
89
+
90
+ lines << " def self.new: (#{keyword_params}) -> #{class_name}"
91
+ lines << " | (#{positional_params}) -> #{class_name}"
92
+ lines << ""
93
+ lines << " def self.[]: (#{keyword_params}) -> #{class_name}"
94
+ lines << " | (#{positional_params}) -> #{class_name}"
95
+ lines << ""
96
+
97
+ # Generate members tuple type
98
+ members_tuple = attributes.map { |attr| ":#{attr}" }.join(", ")
99
+ lines << " def self.members: () -> [ #{members_tuple} ]"
100
+ lines << ""
101
+
102
+ # Generate parse method signatures
103
+ if needs_parse_data
85
104
  lines << " def self.parse: (?parse_data data) -> #{class_name}"
86
105
  lines << " | (?Hash[String, untyped] data) -> #{class_name}"
87
106
  else
88
- # For structures without special types, just use Hash
89
- lines << " def self.parse: (?(Hash[String | Symbol, untyped]), **untyped) -> #{class_name}"
107
+ # Remove optional parentheses to match RBS::Sorter style
108
+ lines << " def self.parse: (?Hash[String | Symbol, untyped], **untyped) -> #{class_name}"
90
109
  end
91
110
  lines << ""
92
111
 
93
- attributes.each do |attr|
112
+ # Sort attr_reader lines alphabetically (RBS::Sorter does this)
113
+ attributes.sort.each do |attr|
94
114
  lines << " attr_reader #{attr}: #{rbs_types[attr]}"
95
115
  end
96
- lines << ""
97
116
 
98
- types.each do |attr, type|
99
- if type == :boolean && !attr.to_s.end_with?("?")
117
+ # Add boolean predicates
118
+ boolean_predicates = types.sort.select { |attr, type| type == :boolean && !attr.to_s.end_with?("?") }
119
+ unless boolean_predicates.empty?
120
+ lines << ""
121
+ boolean_predicates.each do |attr, _type|
100
122
  lines << " def #{attr}?: () -> bool"
101
123
  end
102
124
  end
103
125
 
126
+ # Instance members method comes after attr_readers and predicates
127
+ lines << " def members: () -> [ #{members_tuple} ]"
128
+ lines << ""
129
+
104
130
  hash_type = attributes.map { |attr| "#{attr}: #{rbs_types[attr]}" }.join(", ")
105
131
  lines << " def to_h: () -> { #{hash_type} }"
106
132
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Structure
4
- VERSION = "3.7.0"
4
+ VERSION = "4.1.0"
5
5
  end
data/lib/structure.rb CHANGED
@@ -26,6 +26,37 @@ module Structure
26
26
  # @type var klass: untyped
27
27
  klass = Data.define(*builder.attributes)
28
28
 
29
+ # Enable custom method definitions by evaluating block on the class
30
+ if block
31
+ # Provide temporary dummy DSL methods to prevent NoMethodError during class_eval
32
+ klass.define_singleton_method(:attribute) { |*args, **kwargs, &blk| }
33
+ klass.define_singleton_method(:attribute?) { |*args, **kwargs, &blk| }
34
+ klass.define_singleton_method(:after_parse) { |&blk| }
35
+
36
+ # Evaluate block in class context for method definitions
37
+ klass.class_eval(&block)
38
+
39
+ # Remove temporary DSL methods
40
+ klass.singleton_class.send(:remove_method, :attribute)
41
+ klass.singleton_class.send(:remove_method, :attribute?)
42
+ klass.singleton_class.send(:remove_method, :after_parse)
43
+ end
44
+
45
+ # Override initialize to make optional attributes truly optional
46
+ optional_attrs = builder.optional
47
+ unless optional_attrs.empty?
48
+ klass.class_eval do
49
+ alias_method(:__data_initialize__, :initialize)
50
+
51
+ define_method(:initialize) do |**kwargs| # steep:ignore
52
+ optional_attrs.each do |attr|
53
+ kwargs[attr] = nil unless kwargs.key?(attr)
54
+ end
55
+ __data_initialize__(**kwargs) # steep:ignore
56
+ end
57
+ end
58
+ end
59
+
29
60
  builder.predicate_methods.each do |pred, attr|
30
61
  klass.define_method(pred) { !!public_send(attr) }
31
62
  end
@@ -37,6 +68,7 @@ module Structure
37
68
  mappings: builder.mappings,
38
69
  coercions: builder.coercions(klass),
39
70
  after_parse: builder.after_parse_callback,
71
+ required: builder.required,
40
72
  }.freeze
41
73
  klass.instance_variable_set(:@__structure_meta__, meta)
42
74
  klass.singleton_class.attr_reader(:__structure_meta__)
@@ -72,6 +104,15 @@ module Structure
72
104
  mappings = __structure_meta__[:mappings]
73
105
  defaults = __structure_meta__[:defaults]
74
106
  after_parse = __structure_meta__[:after_parse]
107
+ required = __structure_meta__[:required]
108
+
109
+ # Check for missing required attributes
110
+ required.each do |attr|
111
+ from = mappings[attr]
112
+ next if data.key?(from) || data.key?(from.to_sym) || defaults.key?(attr)
113
+
114
+ raise ArgumentError, "missing keyword: :#{attr}"
115
+ end
75
116
 
76
117
  mappings.each do |attr, from|
77
118
  value = data.fetch(from) do
@@ -4,10 +4,14 @@ module Structure
4
4
  @types: Hash[Symbol, untyped]
5
5
  @defaults: Hash[Symbol, untyped]
6
6
  @after_parse_callback: Proc?
7
+ @optional: Set[Symbol]
7
8
 
8
9
  def attribute: (Symbol name, untyped type, ?from: String?, ?default: untyped) ?{ (untyped) -> untyped } -> void
9
10
  | (Symbol name, ?from: String, ?default: untyped) ?{ (untyped) -> untyped } -> void
10
11
 
12
+ def attribute?: (Symbol name, untyped type, ?from: String?, ?default: untyped) ?{ (untyped) -> untyped } -> void
13
+ | (Symbol name, ?from: String, ?default: untyped) ?{ (untyped) -> untyped } -> void
14
+
11
15
  def after_parse: () { (Data) -> void } -> void
12
16
 
13
17
  def attributes: () -> Array[Symbol]
@@ -16,6 +20,8 @@ module Structure
16
20
  def defaults: () -> Hash[Symbol, untyped]
17
21
  def coercions: (?untyped? context) -> Hash[Symbol, Proc]
18
22
  def predicate_methods: () -> Hash[Symbol, Symbol]
23
+ def optional: () -> Array[Symbol]
24
+ def required: () -> Array[Symbol]
19
25
  def after_parse_callback: () -> (Proc | nil)
20
26
  end
21
27
  end
@@ -3,7 +3,7 @@ module Structure
3
3
  def self.emit: (untyped klass) -> String?
4
4
  def self.write: (untyped klass, ?dir: String) -> String?
5
5
 
6
- private def self.emit_rbs_content: (class_name: String, attributes: Array[Symbol], types: Hash[Symbol, untyped], has_structure_modules: bool) -> String
6
+ private def self.emit_rbs_content: (class_name: String, attributes: Array[Symbol], types: Hash[Symbol, untyped], required: Array[Symbol], has_structure_modules: bool) -> String
7
7
  private def self.parse_data_type: (untyped type, String class_name) -> String
8
8
  private def self.map_type_to_rbs: (untyped type, String class_name) -> String
9
9
  end
data/sig/structure.rbs CHANGED
@@ -5,7 +5,8 @@ module Structure
5
5
  defaults: Hash[Symbol, untyped],
6
6
  mappings: Hash[Symbol, String],
7
7
  coercions: Hash[Symbol, untyped],
8
- after_parse: untyped
8
+ after_parse: untyped,
9
+ required: Array[Symbol]
9
10
  }
10
11
  end
11
12
 
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.7.0
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hakan Ensari