structure 3.3.0 → 3.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: 69bf1e759bbf993b28fb70235866a6c36774b92a57ee47764458d24bb062a55b
4
- data.tar.gz: 3e9a55913e14fb630ece6c4fff62df1d8bfff69d61efd1034a406cf404423f96
3
+ metadata.gz: 2a7bd562939e000315d1904c5f766c0293c570413aa69930e522a01e70849715
4
+ data.tar.gz: '0649cee7d47286ad9ba6e36721cc350f20a9ba40c971214e0edb91bde8bec7d9'
5
5
  SHA512:
6
- metadata.gz: c8b75ee56372f7af3ffd9398474ce25bcffed641314a77293a735cc5e5269f68ee7ba645ed44456eea5c84d1c2b551d0181be35519a61b553466ceda85287f8a
7
- data.tar.gz: 3f736db422a6abe5dae0b59d37194d8f2242c2970a506f40efe04b379f00b7cbc14dd2a3437023d0a33bd0d80376f6418c1421659d446c546d56ccbb1352abf5
6
+ metadata.gz: 3d09a3983e553ccbf3f031c2bb2f920b430032ee5d778b1572f9423e2593808e458662f77615fc84a002136b0896a7bfb5ceff0119d880df1fc833f4123dd493
7
+ data.tar.gz: ec6da8a717b8ccab314c808bb9ab3218d733790378f3b61dbdf52dffbf4b28fd7fd7d6ae7b484d390809a8396fccd894b0d087a3d1a90926149bc55db5a98848
@@ -42,7 +42,7 @@ module Structure
42
42
  elsif block
43
43
  @types[name] = block
44
44
  elsif type
45
- @types[name] = Types.coerce(type)
45
+ @types[name] = type
46
46
  end
47
47
  end
48
48
 
@@ -63,13 +63,16 @@ module Structure
63
63
  @mappings.keys
64
64
  end
65
65
 
66
+ def coercions
67
+ @types.transform_values { |type| Types.coerce(type) }
68
+ end
69
+
66
70
  def predicate_methods
67
- @types.filter_map do |name, type_lambda|
68
- if Types.boolean?(type_lambda) && !name.to_s.end_with?("?")
69
- predicate_name = "#{name}?"
70
- [predicate_name.to_sym, name]
71
+ @types.filter_map do |name, type|
72
+ if type == :boolean
73
+ ["#{name}?".to_sym, name] unless name.to_s.end_with?("?")
71
74
  end
72
- end.to_h
75
+ end.compact.to_h
73
76
  end
74
77
  end
75
78
  end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "pathname"
5
+
6
+ module Structure
7
+ module RBS
8
+ class << self
9
+ def emit(klass)
10
+ return unless klass < Data
11
+
12
+ class_name = klass.name
13
+ return unless class_name
14
+
15
+ meta = klass.respond_to?(:__structure_meta__) ? klass.__structure_meta__ : {}
16
+
17
+ emit_rbs_content(
18
+ class_name: class_name,
19
+ attributes: meta.fetch(:attributes, klass.members),
20
+ types: meta.fetch(:types, {}),
21
+ has_structure_modules: meta.any?,
22
+ )
23
+ end
24
+
25
+ def write(klass, dir: "sig")
26
+ rbs_content = emit(klass)
27
+ return unless rbs_content
28
+
29
+ # User::Address -> user/address.rbs
30
+ path_segments = klass.name.split("::").map(&:downcase)
31
+ filename = "#{path_segments.pop}.rbs"
32
+
33
+ # full path
34
+ dir_path = Pathname.new(dir)
35
+ dir_path = dir_path.join(*path_segments) unless path_segments.empty?
36
+ FileUtils.mkdir_p(dir_path)
37
+
38
+ file_path = dir_path.join(filename)
39
+ File.write(file_path, rbs_content)
40
+
41
+ file_path.to_s
42
+ end
43
+
44
+ private
45
+
46
+ def emit_rbs_content(class_name:, attributes:, types:, has_structure_modules:)
47
+ lines = []
48
+ lines << "class #{class_name} < Data"
49
+ lines << " extend Structure::ClassMethods" if has_structure_modules
50
+ lines << " include Structure::InstanceMethods"
51
+ lines << ""
52
+
53
+ unless attributes.empty?
54
+ # map types to rbs
55
+ rbs_types = attributes.map do |attr|
56
+ type = types.fetch(attr, nil)
57
+ rbs_type = map_type_to_rbs(type, class_name)
58
+
59
+ [attr, rbs_type != "untyped" ? "#{rbs_type}?" : rbs_type]
60
+ end.to_h
61
+
62
+ keyword_params = attributes.map { |attr| "#{attr}: #{rbs_types[attr]}" }.join(", ")
63
+ positional_params = attributes.map { |attr| rbs_types[attr] }.join(", ")
64
+
65
+ lines << " def self.new: (#{keyword_params}) -> instance"
66
+ lines << " | (#{positional_params}) -> instance"
67
+ lines << ""
68
+
69
+ attributes.each do |attr|
70
+ lines << " attr_reader #{attr}: #{rbs_types[attr]}"
71
+ end
72
+ lines << ""
73
+
74
+ types.each do |attr, type|
75
+ if type == :boolean && !attr.to_s.end_with?("?")
76
+ lines << " def #{attr}?: () -> bool"
77
+ end
78
+ end
79
+
80
+ hash_type = attributes.map { |attr| "#{attr}: #{rbs_types[attr]}" }.join(", ")
81
+ lines << " def to_h: () -> { #{hash_type} }"
82
+ end
83
+
84
+ lines << "end"
85
+ lines.join("\n")
86
+ end
87
+
88
+ def map_type_to_rbs(type, class_name)
89
+ case type
90
+ when Class
91
+ type.name || "untyped"
92
+ when :boolean
93
+ "bool"
94
+ when :self
95
+ class_name || "untyped"
96
+ when Array
97
+ if type.size == 2 && type.first == :array
98
+ element_type = map_type_to_rbs(type.last, class_name)
99
+ "Array[#{element_type}]"
100
+ elsif type == [:self]
101
+ "Array[#{class_name || "untyped"}]"
102
+ else
103
+ "untyped"
104
+ end
105
+ else
106
+ "untyped"
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -10,10 +10,6 @@ module Structure
10
10
  BOOLEAN_TRUTHY = [true, 1, "1", "t", "T", "true", "TRUE", "on", "ON"].freeze
11
11
  private_constant :BOOLEAN_TRUTHY
12
12
 
13
- def boolean?(type)
14
- type == boolean
15
- end
16
-
17
13
  # Main factory method for creating type coercers
18
14
  #
19
15
  # @param type [Class, Symbol, Array] Type specification
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Structure
4
- VERSION = "3.3.0"
4
+ VERSION = "3.4.0"
5
5
  end
data/lib/structure.rb CHANGED
@@ -23,78 +23,80 @@ module Structure
23
23
  builder = Builder.new
24
24
  builder.instance_eval(&block) if block
25
25
 
26
- data_class = Data.define(*builder.attributes)
26
+ klass = Data.define(*builder.attributes)
27
27
 
28
- # Generate predicate methods
29
- builder.predicate_methods.each do |predicate_name, attribute_name|
30
- data_class.define_method(predicate_name) do
31
- send(attribute_name)
32
- end
33
- end
28
+ # capture metadata and attach to class
29
+ meta = {
30
+ attributes: builder.attributes.freeze,
31
+ types: builder.types.freeze,
32
+ defaults: builder.defaults.freeze,
33
+ }.freeze
34
+ klass.instance_variable_set(:@__structure_meta__, meta)
35
+ klass.singleton_class.attr_reader(:__structure_meta__)
34
36
 
35
- # Capture builder data in closure for parse method
37
+ # capture locals for method generation
36
38
  mappings = builder.mappings
37
- types = builder.types
38
- defaults = builder.defaults
39
- attributes = builder.attributes
40
- after_parse_callback = builder.after_parse_callback
39
+ coercions = builder.coercions
40
+ predicates = builder.predicate_methods
41
+ after = builder.after_parse_callback
41
42
 
42
- # Override to_h to recursively convert nested objects with to_h
43
- data_class.define_method(:to_h) do
44
- result = {}
45
- self.class.members.each do |member|
46
- value = send(member)
47
- result[member] = case value
48
- when Array
49
- value.map { |item| item.respond_to?(:to_h) && item ? item.to_h : item }
50
- when Hash
51
- value
52
- when ->(v) { v.respond_to?(:to_h) && v }
53
- value.to_h
54
- else
55
- value
56
- end
43
+ # Define predicate methods
44
+ predicates.each do |pred, attr|
45
+ klass.define_method(pred) { public_send(attr) }
46
+ end
47
+
48
+ # recursive to_h
49
+ klass.define_method(:to_h) do
50
+ #: Hash[Symbol, untyped]
51
+ self.class.members.each_with_object({}) do |m, h|
52
+ v = public_send(m)
53
+ h[m] =
54
+ case v
55
+ when Array then v.map { |x| x.respond_to?(:to_h) && x ? x.to_h : x }
56
+ when ->(x) { x.respond_to?(:to_h) && x } then v.to_h
57
+ else v
58
+ end
57
59
  end
58
- result
59
60
  end
60
61
 
61
- data_class.define_singleton_method(:parse) do |data = {}, **kwargs|
62
- # Merge kwargs into data - kwargs take priority as overrides
63
- # Convert kwargs symbol keys to strings to match source_key lookups
62
+ # parse accepts JSON-ish hashes + kwargs override
63
+ klass.define_singleton_method(:parse) do |data = {}, **kwargs|
64
64
  string_kwargs = kwargs.transform_keys(&:to_s)
65
65
  data = data.merge(string_kwargs)
66
66
 
67
- final_kwargs = {}
68
- attributes.each do |attr|
69
- source_key = mappings[attr]
70
- value = if data.key?(source_key)
71
- data[source_key]
72
- elsif data.key?(source_key.to_sym)
73
- data[source_key.to_sym]
74
- elsif defaults.key?(attr)
75
- defaults[attr]
76
- end
67
+ #: Hash[Symbol, untyped]
68
+ final = {}
69
+ meta = __structure_meta__
77
70
 
78
- # Apply type coercion or transformation
79
- if types[attr] && !value.nil?
80
- type_or_proc = types[attr]
81
- # Use instance_exec for non-lambda procs (self-referential types)
82
- value = if type_or_proc.is_a?(Proc) && !type_or_proc.lambda?
83
- instance_exec(value, &type_or_proc)
84
- else
85
- type_or_proc.call(value)
71
+ meta[:attributes].each do |attr|
72
+ source = mappings[attr] || attr.to_s
73
+ value =
74
+ if data.key?(source) then data[source]
75
+ elsif data.key?(source.to_sym) then data[source.to_sym]
76
+ elsif meta[:defaults].key?(attr) then meta[:defaults][attr]
86
77
  end
78
+
79
+ coercion = coercions[attr]
80
+ if coercion && !value.nil?
81
+ # self-referential types need class context to call parse
82
+ value =
83
+ if coercion.is_a?(Proc) && !coercion.lambda?
84
+ instance_exec(value, &coercion)
85
+ else
86
+ coercion.call(value)
87
+ end
87
88
  end
88
89
 
89
- final_kwargs[attr] = value
90
+ final[attr] = value
90
91
  end
91
92
 
92
- instance = new(**final_kwargs)
93
- after_parse_callback&.call(instance)
94
- instance
93
+ #: untyped
94
+ obj = new(**final)
95
+ after&.call(obj)
96
+ obj
95
97
  end
96
98
 
97
- data_class
99
+ klass
98
100
  end
99
101
  end
100
102
  end
@@ -0,0 +1,17 @@
1
+ module Structure
2
+ class Builder
3
+ def attribute: (Symbol name, untyped type, ?from: String, ?default: untyped) ?{ (untyped) -> untyped } -> void
4
+ | (Symbol name, ?from: String, ?default: untyped) ?{ (untyped) -> untyped } -> void
5
+
6
+ def after_parse: () { (Data) -> void } -> void
7
+
8
+ # Methods used by the factory after the block
9
+ def attributes: () -> Array[Symbol]
10
+ def mappings: () -> Hash[Symbol, String]
11
+ def types: () -> Hash[Symbol, untyped]
12
+ def defaults: () -> Hash[Symbol, untyped]
13
+ def coercions: () -> Hash[Symbol, Proc]
14
+ def predicate_methods: () -> Hash[Symbol, Symbol]
15
+ def after_parse_callback: () -> (Proc | nil)
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ module Structure
2
+ module RBS
3
+ def self.emit: (Class klass) -> String?
4
+ def self.write: (Class klass, ?dir: String) -> String?
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
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ module Structure
2
+ module Types
3
+ BOOLEAN_TRUTHY: Array[untyped]
4
+
5
+ def self.coerce: (untyped type) -> Proc
6
+
7
+ private
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
15
+ end
16
+ end
data/sig/structure.rbs ADDED
@@ -0,0 +1,21 @@
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]
7
+ def __structure_meta__: () -> {
8
+ attributes: Array[Symbol],
9
+ types: Hash[Symbol, untyped],
10
+ defaults: Hash[Symbol, untyped],
11
+ from: Hash[Symbol, String]
12
+ }
13
+ end
14
+
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
21
+ 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.3.0
4
+ version: 3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hakan Ensari
@@ -17,8 +17,13 @@ extra_rdoc_files: []
17
17
  files:
18
18
  - lib/structure.rb
19
19
  - lib/structure/builder.rb
20
+ - lib/structure/rbs.rb
20
21
  - lib/structure/types.rb
21
22
  - lib/structure/version.rb
23
+ - sig/structure.rbs
24
+ - sig/structure/builder.rbs
25
+ - sig/structure/rbs.rbs
26
+ - sig/structure/types.rbs
22
27
  licenses:
23
28
  - MIT
24
29
  metadata: