structure 4.3.0 → 4.4.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: f9c2544c619e8f11dcafb8765dd9ebfee9c931927bf71d44aed73c7640d98915
4
- data.tar.gz: 6156fbc60895981a25c1a5816fcdb6c367855552ac6a1397f19a7c22b602761c
3
+ metadata.gz: 70c0f08018c0b2adccf83a0fd310719e21e79e2f0945d753fef9df5a08226537
4
+ data.tar.gz: e62bbdc33e63db5845d816ba84bbde60433e4137d95a7466dbbcd1376891eccf
5
5
  SHA512:
6
- metadata.gz: '09728c8023e6d0102df31ab627d76e98aa6212a14dc7d98151f24dadeb7a46ff12c796a2990bb27a69fa78828dfe1b451669d3547041c3da2175d9dbb6bfebb4'
7
- data.tar.gz: ee06e66c737f47ecc9c18a17470b995c6b5abe0c7708e875fbcb11e9b718371eea991d76aa6b31d2695048fc2203fb06b7d91258c8031b747e111b1fed907a9a
6
+ metadata.gz: 4acad52488ad58e454cf052b14727aa70f75bfb921ee5ffb92310287194b2be4019caa98c5afd995df64a9a6356eafde4f6ffcb33680dc504355a79c575ba801
7
+ data.tar.gz: 9ed2e262bdf3488c5b074b9b1e8148049908883a92deaec90cd969715ff34879231017bbe7061b0a6d3ac9c812dc999497884f383754987e7bee9a64661e0027
@@ -68,7 +68,7 @@ module Structure
68
68
  # @example Optional with default
69
69
  # attribute? :status, String, default: "pending"
70
70
  #
71
- # @example Optional but non-nullable when present
71
+ # @example Optional but non-nullable when present (e.g. GraphQL String!)
72
72
  # attribute? :name, String, null: false
73
73
  def attribute?(name, type = nil, from: nil, default: nil, null: true, &block)
74
74
  attribute(name, type, from: from, default: default, null: null, &block)
data/lib/structure/rbs.rb CHANGED
@@ -28,6 +28,7 @@ module Structure
28
28
  types = meta.fetch(:types, default_types)
29
29
  # @type var required: Array[Symbol]
30
30
  required = meta.fetch(:required, attributes)
31
+ non_nullable = meta.fetch(:non_nullable, [])
31
32
  custom_methods = meta.fetch(:custom_methods, EMPTY_CUSTOM_METHODS)
32
33
 
33
34
  emit_rbs_content(
@@ -35,6 +36,7 @@ module Structure
35
36
  attributes:,
36
37
  types:,
37
38
  required:,
39
+ non_nullable:,
38
40
  has_structure_modules: meta.any?,
39
41
  custom_methods:,
40
42
  )
@@ -59,9 +61,33 @@ module Structure
59
61
  file_path
60
62
  end
61
63
 
64
+ # Write RBS files for multiple classes or all Structure classes in a module
65
+ #
66
+ # @param classes [Array<Class>, Module] Classes to generate RBS for, or a module to scan
67
+ # @param dir [String] Output directory (default: "sig")
68
+ # @return [Array<String>] Paths of written RBS files
69
+ #
70
+ # @example With array of classes
71
+ # Structure::RBS.write_all([Person, Address, Order])
72
+ #
73
+ # @example With module namespace
74
+ # Structure::RBS.write_all(Peddler::Models)
75
+ def write_all(classes, dir: "sig")
76
+ classes = structure_classes_in(classes) if classes.is_a?(Module)
77
+
78
+ classes.filter_map { |klass| write(klass, dir: dir) }
79
+ end
80
+
62
81
  private
63
82
 
64
- def emit_rbs_content(class_name:, attributes:, types:, required:, has_structure_modules:, custom_methods:)
83
+ def structure_classes_in(mod)
84
+ mod.constants.filter_map do |const_name|
85
+ const = mod.const_get(const_name)
86
+ const if const.is_a?(Class) && const < Data && const.respond_to?(:__structure_meta__)
87
+ end
88
+ end
89
+
90
+ def emit_rbs_content(class_name:, attributes:, types:, required:, non_nullable:, has_structure_modules:, custom_methods:)
65
91
  # @type var lines: Array[String]
66
92
  lines = []
67
93
  lines << "class #{class_name} < Data"
@@ -71,8 +97,9 @@ module Structure
71
97
  rbs_types = attributes.map do |attr|
72
98
  type = types.fetch(attr, nil)
73
99
  rbs_type = map_type_to_rbs(type, class_name)
100
+ nullable = !(required.include?(attr) && non_nullable.include?(attr))
74
101
 
75
- [attr, rbs_type != "untyped" ? "#{rbs_type}?" : rbs_type]
102
+ [attr, rbs_type != "untyped" && nullable ? "#{rbs_type}?" : rbs_type]
76
103
  end.to_h
77
104
 
78
105
  # Sort keyword params: required first, then optional (with ? prefix)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Structure
4
- VERSION = "4.3.0"
4
+ VERSION = "4.4.0"
5
5
  end
@@ -0,0 +1,200 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ return unless defined?(Tapioca::Dsl::Compiler)
5
+
6
+ module Tapioca
7
+ module Dsl
8
+ module Compilers
9
+ # Generates RBI files for Structure-based Data classes.
10
+ #
11
+ # For example, given:
12
+ #
13
+ # Person = Structure.new do
14
+ # attribute :name, String
15
+ # attribute? :age, Integer
16
+ # end
17
+ #
18
+ # This compiler will generate:
19
+ #
20
+ # class Person < Data
21
+ # sig { params(name: String, age: T.nilable(Integer)).returns(Person) }
22
+ # sig { params(name: String, age: T.nilable(Integer)).void }
23
+ # def initialize(name:, age: nil); end
24
+ #
25
+ # sig { returns(String) }
26
+ # def name; end
27
+ #
28
+ # sig { returns(T.nilable(Integer)) }
29
+ # def age; end
30
+ #
31
+ # sig { params(data: T::Hash[T.any(String, Symbol), T.untyped]).returns(Person) }
32
+ # def self.parse(data = {}); end
33
+ # end
34
+ #: [ConstantType = singleton(::Data)]
35
+ class Structure < Compiler
36
+ extend T::Sig
37
+
38
+ class << self
39
+ extend T::Sig
40
+
41
+ sig { override.returns(T::Enumerable[Module]) }
42
+ def gather_constants
43
+ all_classes.select do |klass|
44
+ klass < ::Data && klass.respond_to?(:__structure_meta__)
45
+ end
46
+ end
47
+ end
48
+
49
+ sig { override.void }
50
+ def decorate
51
+ meta = constant.__structure_meta__
52
+ return unless meta
53
+
54
+ attributes = meta[:mappings]&.keys || constant.members
55
+ types = meta.fetch(:types, {})
56
+ required = meta.fetch(:required, attributes)
57
+ non_nullable = meta.fetch(:non_nullable, [])
58
+
59
+ root.create_path(constant) do |klass|
60
+ generate_new_and_brackets(klass, attributes, types, required, non_nullable)
61
+ generate_parse(klass)
62
+ generate_load_dump(klass)
63
+ generate_members(klass, attributes)
64
+ generate_attr_readers(klass, attributes, types, non_nullable, required)
65
+ generate_to_h(klass, attributes, types, non_nullable, required)
66
+ generate_boolean_predicates(klass, types)
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ sig { params(klass: RBI::Scope, attributes: T::Array[Symbol], types: T::Hash[Symbol, T.untyped], required: T::Array[Symbol], non_nullable: T::Array[Symbol]).void }
73
+ def generate_new_and_brackets(klass, attributes, types, required, non_nullable)
74
+ params = attributes.map do |attr|
75
+ type = map_type_to_sorbet(types[attr], nullable: !(required.include?(attr) && non_nullable.include?(attr)))
76
+ is_required = required.include?(attr)
77
+ if is_required
78
+ create_kw_param(attr.to_s, type: type)
79
+ else
80
+ create_kw_opt_param(attr.to_s, type: type, default: "nil")
81
+ end
82
+ end
83
+
84
+ klass.create_method("initialize", parameters: params, return_type: "void")
85
+ klass.create_method("new", parameters: params, return_type: constant.name.to_s, class_method: true)
86
+ klass.create_method("[]", parameters: params, return_type: constant.name.to_s, class_method: true)
87
+ end
88
+
89
+ sig { params(klass: RBI::Scope).void }
90
+ def generate_parse(klass)
91
+ klass.create_method(
92
+ "parse",
93
+ parameters: [
94
+ create_opt_param("data", type: "T::Hash[T.any(String, Symbol), T.untyped]", default: "{}"),
95
+ create_opt_param("overrides", type: "T.nilable(T::Hash[Symbol, T.untyped])", default: "nil"),
96
+ ],
97
+ return_type: constant.name.to_s,
98
+ class_method: true,
99
+ )
100
+ end
101
+
102
+ sig { params(klass: RBI::Scope).void }
103
+ def generate_load_dump(klass)
104
+ klass.create_method(
105
+ "load",
106
+ parameters: [create_param("data", type: "T.nilable(T::Hash[T.any(String, Symbol), T.untyped])")],
107
+ return_type: "T.nilable(#{constant.name})",
108
+ class_method: true,
109
+ )
110
+ klass.create_method(
111
+ "dump",
112
+ parameters: [create_param("value", type: "T.nilable(#{constant.name})")],
113
+ return_type: "T.nilable(T::Hash[Symbol, T.untyped])",
114
+ class_method: true,
115
+ )
116
+ end
117
+
118
+ sig { params(klass: RBI::Scope, attributes: T::Array[Symbol]).void }
119
+ def generate_members(klass, attributes)
120
+ members_type = "[#{attributes.map { |a| ":#{a}" }.join(", ")}]"
121
+ klass.create_method("members", return_type: members_type, class_method: true)
122
+ klass.create_method("members", return_type: members_type)
123
+ end
124
+
125
+ sig { params(klass: RBI::Scope, attributes: T::Array[Symbol], types: T::Hash[Symbol, T.untyped], non_nullable: T::Array[Symbol], required: T::Array[Symbol]).void }
126
+ def generate_attr_readers(klass, attributes, types, non_nullable, required)
127
+ attributes.each do |attr|
128
+ type = map_type_to_sorbet(types[attr], nullable: !(required.include?(attr) && non_nullable.include?(attr)))
129
+ klass.create_method(attr.to_s, return_type: type)
130
+ end
131
+ end
132
+
133
+ sig { params(klass: RBI::Scope, attributes: T::Array[Symbol], types: T::Hash[Symbol, T.untyped], non_nullable: T::Array[Symbol], required: T::Array[Symbol]).void }
134
+ def generate_to_h(klass, attributes, types, non_nullable, required)
135
+ hash_pairs = attributes.map do |attr|
136
+ type = map_type_to_sorbet(types[attr], nullable: !(required.include?(attr) && non_nullable.include?(attr)))
137
+ "#{attr}: #{type}"
138
+ end.join(", ")
139
+
140
+ klass.create_method("to_h", return_type: "{ #{hash_pairs} }")
141
+ end
142
+
143
+ sig { params(klass: RBI::Scope, types: T::Hash[Symbol, T.untyped]).void }
144
+ def generate_boolean_predicates(klass, types)
145
+ types.each do |attr, type|
146
+ next unless type == :boolean
147
+ next if attr.to_s.end_with?("?")
148
+
149
+ klass.create_method("#{attr}?", return_type: "T::Boolean")
150
+ end
151
+ end
152
+
153
+ sig { params(type: T.untyped, nullable: T::Boolean).returns(String) }
154
+ def map_type_to_sorbet(type, nullable: true)
155
+ raw = case type
156
+ when Class
157
+ if type == Array
158
+ "T::Array[T.untyped]"
159
+ elsif type == Hash
160
+ "T::Hash[T.untyped, T.untyped]"
161
+ else
162
+ type.name || "T.untyped"
163
+ end
164
+ when :boolean
165
+ "T::Boolean"
166
+ when :self
167
+ constant.name.to_s
168
+ when Array
169
+ if type.size == 1
170
+ element_type = map_type_to_sorbet_element(type.first)
171
+ "T::Array[#{element_type}]"
172
+ else
173
+ "T.untyped"
174
+ end
175
+ when Proc
176
+ "T.untyped"
177
+ else
178
+ "T.untyped"
179
+ end
180
+
181
+ nullable ? "T.nilable(#{raw})" : raw
182
+ end
183
+
184
+ sig { params(type: T.untyped).returns(String) }
185
+ def map_type_to_sorbet_element(type)
186
+ case type
187
+ when Class
188
+ type.name || "T.untyped"
189
+ when :boolean
190
+ "T::Boolean"
191
+ when :self
192
+ constant.name.to_s
193
+ else
194
+ "T.untyped"
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -4,8 +4,10 @@ module Structure
4
4
 
5
5
  def self.emit: (untyped klass) -> String?
6
6
  def self.write: (untyped klass, ?dir: String) -> String?
7
+ def self.write_all: (Array[untyped] | Module classes, ?dir: String) -> Array[String]
7
8
 
8
- private def self.emit_rbs_content: (class_name: String, attributes: Array[Symbol], types: Hash[Symbol, untyped], required: Array[Symbol], has_structure_modules: bool, custom_methods: untyped) -> String
9
+ private def self.structure_classes_in: (Module mod) -> Array[untyped]
10
+ private def self.emit_rbs_content: (class_name: String, attributes: Array[Symbol], types: Hash[Symbol, untyped], required: Array[Symbol], non_nullable: Array[Symbol], has_structure_modules: bool, custom_methods: untyped) -> String
9
11
  private def self.parse_data_type: (untyped type, String class_name) -> String
10
12
  private def self.map_type_to_rbs: (untyped type, String class_name) -> String
11
13
  private def self.format_method_signature: (Hash[Symbol, untyped] method_meta, singleton: bool) -> String
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: 4.3.0
4
+ version: 4.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hakan Ensari
@@ -20,6 +20,7 @@ files:
20
20
  - lib/structure/rbs.rb
21
21
  - lib/structure/types.rb
22
22
  - lib/structure/version.rb
23
+ - lib/tapioca/dsl/compilers/structure.rb
23
24
  - sig/structure.rbs
24
25
  - sig/structure/builder.rbs
25
26
  - sig/structure/rbs.rbs
@@ -43,7 +44,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
43
44
  - !ruby/object:Gem::Version
44
45
  version: '0'
45
46
  requirements: []
46
- rubygems_version: 3.7.2
47
+ rubygems_version: 4.0.3
47
48
  specification_version: 4
48
49
  summary: Structure your data
49
50
  test_files: []