Skip to content

Commit 20eda11

Browse files
committed
Add proper types for arguments and outputs
1 parent 5eb2429 commit 20eda11

File tree

3 files changed

+105
-10
lines changed

3 files changed

+105
-10
lines changed

lib/tapioca/dsl/compilers/operandi.rb

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ module Compilers
1515
# - Predicate: `def name?` - returns boolean
1616
# - Setter: `def name=` (private) - sets the value
1717
#
18+
# Additionally, typed inner classes are generated:
19+
# - `Arguments` - T::Struct representing all service arguments
20+
# - `Outputs` - T::Struct representing all service outputs
21+
#
1822
# @example Service definition
1923
# class CreateUser < Operandi::Base
2024
# arg :name, type: String
@@ -26,6 +30,22 @@ module Compilers
2630
#
2731
# @example Generated RBI
2832
# class CreateUser
33+
# class Arguments < T::Struct
34+
# prop :name, ::String
35+
# prop :email, T.nilable(::String), default: nil
36+
# prop :role, T.any(::Symbol, ::String)
37+
# end
38+
#
39+
# class Outputs < T::Struct
40+
# prop :user, ::User
41+
# end
42+
#
43+
# sig { returns(Arguments) }
44+
# def arguments; end
45+
#
46+
# sig { returns(Outputs) }
47+
# def outputs; end
48+
#
2949
# sig { returns(String) }
3050
# def name; end
3151
#
@@ -81,6 +101,10 @@ def decorate
81101
# Generate class methods (.run, .run!, .with)
82102
generate_class_methods(klass)
83103

104+
# Generate typed inner classes for arguments and outputs
105+
generate_arguments_type(klass)
106+
generate_outputs_type(klass)
107+
84108
# Generate argument methods
85109
constant.arguments.each_value do |field|
86110
generate_field_methods(klass, field)
@@ -102,6 +126,22 @@ def generate_class_methods(klass)
102126
generate_with_method(klass)
103127
end
104128

129+
sig { params(klass: RBI::Scope).void }
130+
def generate_arguments_type(klass)
131+
return if constant.arguments.empty?
132+
133+
generate_struct_class(klass, "Arguments", constant.arguments)
134+
klass.create_method("arguments", return_type: "Arguments")
135+
end
136+
137+
sig { params(klass: RBI::Scope).void }
138+
def generate_outputs_type(klass)
139+
return if constant.outputs.empty?
140+
141+
generate_struct_class(klass, "Outputs", constant.outputs)
142+
klass.create_method("outputs", return_type: "Outputs")
143+
end
144+
105145
sig { params(klass: RBI::Scope).void }
106146
def generate_run_method(klass)
107147
klass.create_method(
@@ -248,6 +288,33 @@ def as_nilable_type(type)
248288

249289
"T.nilable(#{type})"
250290
end
291+
292+
sig do
293+
params(
294+
klass: RBI::Scope,
295+
class_name: String,
296+
fields: T::Hash[Symbol, ::Operandi::Settings::Field],
297+
).void
298+
end
299+
def generate_struct_class(klass, class_name, fields)
300+
return if fields.empty?
301+
302+
klass.create_class(class_name, superclass_name: "T::Struct") do |struct_klass|
303+
fields.each_value do |field|
304+
generate_struct_prop(struct_klass, field)
305+
end
306+
end
307+
end
308+
309+
sig { params(struct_klass: RBI::Scope, field: ::Operandi::Settings::Field).void }
310+
def generate_struct_prop(struct_klass, field)
311+
name = field.name.to_s
312+
ruby_type = resolve_type(field)
313+
prop_type = field.optional ? as_nilable_type(ruby_type) : ruby_type
314+
default_value = format_default_value(field) if field.optional || field.default_exists
315+
316+
struct_klass << RBI::TStructProp.new(name, prop_type, default: default_value)
317+
end
251318
end
252319
end
253320
end

rbi/operandi.rbi

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,6 @@
77
module Operandi
88
class Base
99
# Attributes
10-
sig { returns(Operandi::Collection::Base) }
11-
def outputs; end
12-
13-
sig { returns(Operandi::Collection::Base) }
14-
def arguments; end
15-
1610
sig { returns(Operandi::Messages) }
1711
def errors; end
1812

spec/tapioca/dsl/compilers/operandi_spec.rb

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,28 @@ def create_kw_opt_param(name, type:, default:)
6767
# Mock RBI module
6868
module RBI
6969
class Scope
70-
attr_reader :methods
70+
attr_reader :methods, :classes, :nodes
7171

7272
def initialize
7373
@methods = []
74+
@classes = []
75+
@nodes = []
7476
end
7577

7678
def create_method(name, parameters: [], return_type: nil, visibility: nil, class_method: false)
7779
@methods << MockMethod.new(name, parameters, return_type, visibility, class_method)
7880
end
81+
82+
def create_class(name, superclass_name: nil)
83+
child_scope = Scope.new
84+
yield child_scope if block_given?
85+
@classes << MockClass.new(name, superclass_name, child_scope)
86+
child_scope
87+
end
88+
89+
def <<(node)
90+
@nodes << node
91+
end
7992
end
8093

8194
class Tree
@@ -91,6 +104,27 @@ def to_s
91104
"private"
92105
end
93106
end
107+
108+
class TStructProp
109+
attr_reader :name, :type, :default
110+
111+
def initialize(name, type, default: nil)
112+
@name = name
113+
@type = type
114+
@default = default
115+
end
116+
end
117+
end
118+
119+
# Mock class for generated inner classes
120+
class MockClass
121+
attr_reader :name, :superclass_name, :scope
122+
123+
def initialize(name, superclass_name, scope)
124+
@name = name
125+
@superclass_name = superclass_name
126+
@scope = scope
127+
end
94128
end
95129

96130
# Mock classes for testing
@@ -310,11 +344,11 @@ def self.name
310344
expect(find_method(scope, "output")).not_to be_nil
311345
end
312346

313-
it "generates 9 methods total (3 per field + 3 class methods)" do
347+
it "generates 11 methods total (3 per field + 3 class methods + 2 collection accessors)" do
314348
scope = compiler.decorate
315349

316-
# 3 class methods (run, run!, with) + 6 field methods (3 per field)
317-
expect(scope.methods.size).to eq(9)
350+
# 3 class methods (run, run!, with) + 6 field methods (3 per field) + 2 collection accessors (arguments, outputs)
351+
expect(scope.methods.size).to eq(11)
318352
end
319353
end
320354

0 commit comments

Comments
 (0)