light-services 3.2.0 โ 3.3.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 +4 -4
- data/CHANGELOG.md +15 -3
- data/Gemfile.lock +1 -1
- data/README.md +1 -0
- data/docs/README.md +1 -0
- data/docs/SUMMARY.md +1 -0
- data/docs/tapioca.md +200 -0
- data/lib/light/services/rubocop/cop/light_services/reserved_name.rb +56 -0
- data/lib/light/services/rubocop.rb +1 -0
- data/lib/light/services/version.rb +1 -1
- data/lib/tapioca/dsl/compilers/light_services.rb +255 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3f1c7874fc2ac069ac686f0b12895567c76c7005523dc0c1b841781f73ab3988
|
|
4
|
+
data.tar.gz: 4b41b830919ac007ec7740b1a84ca7d8a39682d9623fd9fd9085546e384391a3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eac0742e9f253e03b5e1cb58619798fc81b4f3139df62311c9350d4f3f1a13264dc4ad74af14e84033fa66137fbe31b390f95056783a83151ec76527c0590bc3
|
|
7
|
+
data.tar.gz: 45a9a709c76c57032c8ae71bc8fa31e32fee3aebe8e82a8c1265480c400ab2a724cfc9dc9d3572a830ff78a03235cfd94a95f782c4c92750db5d5f0d9f83bd6f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 3.
|
|
3
|
+
## 3.3.0 (2025-12-15)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Sorbet and Tapioca support for `arg` and `output`
|
|
8
|
+
|
|
9
|
+
## 3.2.1 (2025-12-15)
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Add RuboCop cop `ReservedName`
|
|
14
|
+
|
|
15
|
+
## 3.2.0 (2025-12-15)
|
|
4
16
|
|
|
5
17
|
### Added
|
|
6
18
|
|
|
@@ -12,7 +24,7 @@
|
|
|
12
24
|
|
|
13
25
|
- Service runs steps with `always: true` after `fail_immediately!` was called
|
|
14
26
|
|
|
15
|
-
## 3.1.2 (2025-12-
|
|
27
|
+
## 3.1.2 (2025-12-14)
|
|
16
28
|
|
|
17
29
|
### Added
|
|
18
30
|
|
|
@@ -22,7 +34,7 @@
|
|
|
22
34
|
|
|
23
35
|
- Split `config.require_type` into `config.require_arg_type` and `config.require_output_type`
|
|
24
36
|
|
|
25
|
-
## 3.1.1 (2025-12-
|
|
37
|
+
## 3.1.1 (2025-12-14)
|
|
26
38
|
|
|
27
39
|
### Added
|
|
28
40
|
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Light Services is a simple yet powerful way to organize business logic in Ruby a
|
|
|
18
18
|
- ๐งช **RSpec Matchers**: Built-in RSpec matchers for expressive service tests
|
|
19
19
|
- ๐ **Framework Agnostic**: Compatible with Rails, Hanami, or any Ruby framework
|
|
20
20
|
- ๐งฉ **Modularity**: Isolate and test your services with ease
|
|
21
|
+
- ๐ท **Sorbet & Tapioca**: Full support for Sorbet type checking and Tapioca DSL generation
|
|
21
22
|
- โ
**100% Test Coverage**: Thoroughly tested and reliable
|
|
22
23
|
- โ๏ธ **Battle-Tested**: In production use since 2017
|
|
23
24
|
|
data/docs/README.md
CHANGED
|
@@ -16,6 +16,7 @@ Light Services is a simple yet powerful way to organize business logic in Ruby a
|
|
|
16
16
|
- ๐ **RuboCop Integration**: Custom cops to enforce best practices at lint time
|
|
17
17
|
- ๐ **Framework Agnostic**: Compatible with Rails, Hanami, or any Ruby framework
|
|
18
18
|
- ๐งฉ **Modularity**: Isolate and test your services with ease
|
|
19
|
+
- ๐ท **Sorbet & Tapioca**: Full support for Sorbet type checking and Tapioca DSL generation
|
|
19
20
|
- โ
**100% Test Coverage**: Thoroughly tested and reliable
|
|
20
21
|
- โ๏ธ **Battle-Tested**: In production use since 2017
|
|
21
22
|
|
data/docs/SUMMARY.md
CHANGED
data/docs/tapioca.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Tapioca / Sorbet Integration
|
|
2
|
+
|
|
3
|
+
Light Services provides a [Tapioca](https://github.com/Shopify/tapioca) DSL compiler that generates RBI signatures for methods automatically created by the `arg` and `output` DSL macros. This enables full Sorbet type checking for your services.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
When you use the `arg` or `output` keywords, Light Services dynamically generates methods at runtime:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class CreateUser < ApplicationService
|
|
11
|
+
arg :name, type: String
|
|
12
|
+
arg :email, type: String, optional: true
|
|
13
|
+
arg :role, type: [Symbol, String]
|
|
14
|
+
|
|
15
|
+
output :user, type: User
|
|
16
|
+
end
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The Tapioca compiler generates RBI signatures for these methods:
|
|
20
|
+
|
|
21
|
+
```rbi
|
|
22
|
+
# sorbet/rbi/dsl/create_user.rbi
|
|
23
|
+
# typed: true
|
|
24
|
+
|
|
25
|
+
class CreateUser
|
|
26
|
+
sig { returns(String) }
|
|
27
|
+
def name; end
|
|
28
|
+
|
|
29
|
+
sig { returns(T::Boolean) }
|
|
30
|
+
def name?; end
|
|
31
|
+
|
|
32
|
+
sig { returns(T.nilable(String)) }
|
|
33
|
+
def email; end
|
|
34
|
+
|
|
35
|
+
sig { returns(T::Boolean) }
|
|
36
|
+
def email?; end
|
|
37
|
+
|
|
38
|
+
sig { returns(T.any(Symbol, String)) }
|
|
39
|
+
def role; end
|
|
40
|
+
|
|
41
|
+
sig { returns(T::Boolean) }
|
|
42
|
+
def role?; end
|
|
43
|
+
|
|
44
|
+
sig { returns(User) }
|
|
45
|
+
def user; end
|
|
46
|
+
|
|
47
|
+
sig { returns(T::Boolean) }
|
|
48
|
+
def user?; end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
sig { params(value: String).returns(String) }
|
|
53
|
+
def name=(value); end
|
|
54
|
+
|
|
55
|
+
sig { params(value: T.nilable(String)).returns(T.nilable(String)) }
|
|
56
|
+
def email=(value); end
|
|
57
|
+
|
|
58
|
+
sig { params(value: T.any(Symbol, String)).returns(T.any(Symbol, String)) }
|
|
59
|
+
def role=(value); end
|
|
60
|
+
|
|
61
|
+
sig { params(value: User).returns(User) }
|
|
62
|
+
def user=(value); end
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Setup
|
|
67
|
+
|
|
68
|
+
### 1. Install Tapioca
|
|
69
|
+
|
|
70
|
+
Add Tapioca to your Gemfile:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
group :development do
|
|
74
|
+
gem "tapioca", require: false
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then run:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
bundle install
|
|
82
|
+
bundle exec tapioca init
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 2. Generate RBI Files
|
|
86
|
+
|
|
87
|
+
The Light Services compiler is automatically discovered by Tapioca. Generate RBI files with:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
bundle exec tapioca dsl
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
This will create RBI files in `sorbet/rbi/dsl/` for all your services.
|
|
94
|
+
|
|
95
|
+
### 3. Re-generate After Changes
|
|
96
|
+
|
|
97
|
+
After adding or modifying `arg`/`output` declarations, regenerate the RBI files:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
bundle exec tapioca dsl LightServices
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Type Mappings
|
|
104
|
+
|
|
105
|
+
### Ruby Types
|
|
106
|
+
|
|
107
|
+
Standard Ruby types are mapped directly:
|
|
108
|
+
|
|
109
|
+
| Ruby Type | Sorbet Type |
|
|
110
|
+
|-----------|-------------|
|
|
111
|
+
| `String` | `::String` |
|
|
112
|
+
| `Integer` | `::Integer` |
|
|
113
|
+
| `Float` | `::Float` |
|
|
114
|
+
| `Hash` | `::Hash` |
|
|
115
|
+
| `Array` | `::Array` |
|
|
116
|
+
| `Symbol` | `::Symbol` |
|
|
117
|
+
| `User` (custom) | `::User` |
|
|
118
|
+
|
|
119
|
+
### Boolean Types
|
|
120
|
+
|
|
121
|
+
Boolean types are mapped to `T::Boolean`:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
arg :active, type: [TrueClass, FalseClass]
|
|
125
|
+
# Generates: sig { returns(T::Boolean) }
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Union Types
|
|
129
|
+
|
|
130
|
+
Multiple types create union types:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
arg :id, type: [String, Integer]
|
|
134
|
+
# Generates: sig { returns(T.any(::String, ::Integer)) }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Optional Types
|
|
138
|
+
|
|
139
|
+
Optional arguments/outputs are wrapped in `T.nilable`:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
arg :nickname, type: String, optional: true
|
|
143
|
+
# Generates: sig { returns(T.nilable(::String)) }
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Dry-Types
|
|
147
|
+
|
|
148
|
+
If you use [dry-types](https://dry-rb.org/gems/dry-types/), they are mapped to their primitive Ruby types:
|
|
149
|
+
|
|
150
|
+
| Dry Type | Sorbet Type |
|
|
151
|
+
|----------|-------------|
|
|
152
|
+
| `Types::String` | `::String` |
|
|
153
|
+
| `Types::Strict::String` | `::String` |
|
|
154
|
+
| `Types::Integer` | `::Integer` |
|
|
155
|
+
| `Types::Bool` | `T::Boolean` |
|
|
156
|
+
| `Types::Array` | `::Array` |
|
|
157
|
+
| `Types::Hash` | `::Hash` |
|
|
158
|
+
| `Types::Date` | `::Date` |
|
|
159
|
+
| `Types::Time` | `::Time` |
|
|
160
|
+
| `Types::DateTime` | `::DateTime` |
|
|
161
|
+
| `Types::Decimal` | `::BigDecimal` |
|
|
162
|
+
| `Types::Any` | `T.untyped` |
|
|
163
|
+
|
|
164
|
+
Parameterized dry-types (e.g., `Types::Array.of(String)`) are mapped to their base type.
|
|
165
|
+
|
|
166
|
+
## Generated Methods
|
|
167
|
+
|
|
168
|
+
For each `arg` or `output`, three methods are generated:
|
|
169
|
+
|
|
170
|
+
| Method | Return Type | Visibility |
|
|
171
|
+
|--------|-------------|------------|
|
|
172
|
+
| `name` | The declared type | public |
|
|
173
|
+
| `name?` | `T::Boolean` | public |
|
|
174
|
+
| `name=` | The declared type | **private** |
|
|
175
|
+
|
|
176
|
+
## Inheritance
|
|
177
|
+
|
|
178
|
+
The compiler handles inherited arguments and outputs. If a child service inherits from a parent, the RBI will include methods for both parent and child fields.
|
|
179
|
+
|
|
180
|
+
## Troubleshooting
|
|
181
|
+
|
|
182
|
+
### RBI files not generated
|
|
183
|
+
|
|
184
|
+
Ensure Light Services is properly loaded in your application. The compiler only runs if `Light::Services::Base` is defined.
|
|
185
|
+
|
|
186
|
+
### Types showing as `T.untyped`
|
|
187
|
+
|
|
188
|
+
This happens when:
|
|
189
|
+
- No `type:` option is specified for the argument/output
|
|
190
|
+
- The type cannot be resolved (e.g., undefined constant)
|
|
191
|
+
|
|
192
|
+
### Custom type mappings
|
|
193
|
+
|
|
194
|
+
If you need custom dry-types mappings, you can extend the `DRY_TYPE_MAPPINGS` constant in the compiler or open an issue to add common mappings.
|
|
195
|
+
|
|
196
|
+
## See Also
|
|
197
|
+
|
|
198
|
+
- [Ruby LSP Integration](ruby-lsp.md) - Editor integration without Sorbet
|
|
199
|
+
- [Arguments](arguments.md) - Full `arg` DSL documentation
|
|
200
|
+
- [Outputs](outputs.md) - Full `output` DSL documentation
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../../light/services/constants"
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module LightServices
|
|
8
|
+
# Ensures that `arg`, `step`, and `output` declarations do not use reserved names
|
|
9
|
+
# that would conflict with Light::Services methods.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# # bad
|
|
13
|
+
# arg :errors, type: Array
|
|
14
|
+
# arg :outputs, type: Hash
|
|
15
|
+
# step :call
|
|
16
|
+
# output :success?, type: [TrueClass, FalseClass]
|
|
17
|
+
#
|
|
18
|
+
# # good
|
|
19
|
+
# arg :validation_errors, type: Array
|
|
20
|
+
# arg :result_outputs, type: Hash
|
|
21
|
+
# step :execute
|
|
22
|
+
# output :succeeded, type: [TrueClass, FalseClass]
|
|
23
|
+
#
|
|
24
|
+
class ReservedName < Base
|
|
25
|
+
include RuboCop::Cop::RangeHelp
|
|
26
|
+
|
|
27
|
+
MSG = "`%<name>s` is a reserved name and cannot be used as %<field_type>s. " \
|
|
28
|
+
"It conflicts with Light::Services methods."
|
|
29
|
+
|
|
30
|
+
SEVERITY = :error
|
|
31
|
+
|
|
32
|
+
RESTRICT_ON_SEND = [:arg, :step, :output].freeze
|
|
33
|
+
|
|
34
|
+
FIELD_TYPE_NAMES = {
|
|
35
|
+
arg: "an argument",
|
|
36
|
+
step: "a step",
|
|
37
|
+
output: "an output",
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
# @!method dsl_call?(node)
|
|
41
|
+
def_node_matcher :dsl_call?, <<~PATTERN
|
|
42
|
+
(send nil? ${:arg :step :output} (sym $_) ...)
|
|
43
|
+
PATTERN
|
|
44
|
+
|
|
45
|
+
def on_send(node)
|
|
46
|
+
dsl_call?(node) do |method_name, name|
|
|
47
|
+
return unless Light::Services::ReservedNames::ALL.include?(name)
|
|
48
|
+
|
|
49
|
+
field_type = FIELD_TYPE_NAMES[method_name]
|
|
50
|
+
add_offense(node, message: format(MSG, name: name, field_type: field_type), severity: SEVERITY)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -10,4 +10,5 @@ require_relative "rubocop/cop/light_services/missing_private_keyword"
|
|
|
10
10
|
require_relative "rubocop/cop/light_services/no_direct_instantiation"
|
|
11
11
|
require_relative "rubocop/cop/light_services/output_type_required"
|
|
12
12
|
require_relative "rubocop/cop/light_services/prefer_fail_method"
|
|
13
|
+
require_relative "rubocop/cop/light_services/reserved_name"
|
|
13
14
|
require_relative "rubocop/cop/light_services/step_method_exists"
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
return unless defined?(Tapioca::Dsl::Compiler)
|
|
4
|
+
|
|
5
|
+
module Tapioca
|
|
6
|
+
module Dsl
|
|
7
|
+
module Compilers
|
|
8
|
+
# Tapioca DSL compiler for Light::Services
|
|
9
|
+
#
|
|
10
|
+
# Generates RBI signatures for methods automatically defined by the
|
|
11
|
+
# `arg`/`argument` and `output` DSL macros in light-services.
|
|
12
|
+
#
|
|
13
|
+
# For each argument and output, three methods are generated:
|
|
14
|
+
# - Getter: `def name` - returns the value
|
|
15
|
+
# - Predicate: `def name?` - returns boolean
|
|
16
|
+
# - Setter: `def name=` (private) - sets the value
|
|
17
|
+
#
|
|
18
|
+
# @example Service definition
|
|
19
|
+
# class CreateUser < Light::Services::Base
|
|
20
|
+
# arg :name, type: String
|
|
21
|
+
# arg :email, type: String, optional: true
|
|
22
|
+
# arg :role, type: [Symbol, String]
|
|
23
|
+
#
|
|
24
|
+
# output :user, type: User
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# @example Generated RBI
|
|
28
|
+
# class CreateUser
|
|
29
|
+
# sig { returns(String) }
|
|
30
|
+
# def name; end
|
|
31
|
+
#
|
|
32
|
+
# sig { returns(T::Boolean) }
|
|
33
|
+
# def name?; end
|
|
34
|
+
#
|
|
35
|
+
# sig { returns(T.nilable(String)) }
|
|
36
|
+
# def email; end
|
|
37
|
+
#
|
|
38
|
+
# sig { returns(T::Boolean) }
|
|
39
|
+
# def email?; end
|
|
40
|
+
#
|
|
41
|
+
# sig { returns(T.any(Symbol, String)) }
|
|
42
|
+
# def role; end
|
|
43
|
+
#
|
|
44
|
+
# sig { returns(T::Boolean) }
|
|
45
|
+
# def role?; end
|
|
46
|
+
#
|
|
47
|
+
# sig { returns(User) }
|
|
48
|
+
# def user; end
|
|
49
|
+
#
|
|
50
|
+
# sig { returns(T::Boolean) }
|
|
51
|
+
# def user?; end
|
|
52
|
+
#
|
|
53
|
+
# private
|
|
54
|
+
#
|
|
55
|
+
# sig { params(value: String).returns(String) }
|
|
56
|
+
# def name=(value); end
|
|
57
|
+
#
|
|
58
|
+
# # ... other setters
|
|
59
|
+
# end
|
|
60
|
+
class LightServices < Compiler
|
|
61
|
+
extend T::Sig
|
|
62
|
+
|
|
63
|
+
# Default type mappings for common dry-types to their underlying Ruby types
|
|
64
|
+
DRY_TYPE_MAPPINGS = {
|
|
65
|
+
"Types::String" => "::String",
|
|
66
|
+
"Types::Strict::String" => "::String",
|
|
67
|
+
"Types::Coercible::String" => "::String",
|
|
68
|
+
"Types::Integer" => "::Integer",
|
|
69
|
+
"Types::Strict::Integer" => "::Integer",
|
|
70
|
+
"Types::Coercible::Integer" => "::Integer",
|
|
71
|
+
"Types::Float" => "::Float",
|
|
72
|
+
"Types::Strict::Float" => "::Float",
|
|
73
|
+
"Types::Coercible::Float" => "::Float",
|
|
74
|
+
"Types::Decimal" => "::BigDecimal",
|
|
75
|
+
"Types::Strict::Decimal" => "::BigDecimal",
|
|
76
|
+
"Types::Coercible::Decimal" => "::BigDecimal",
|
|
77
|
+
"Types::Bool" => "T::Boolean",
|
|
78
|
+
"Types::Strict::Bool" => "T::Boolean",
|
|
79
|
+
"Types::True" => "::TrueClass",
|
|
80
|
+
"Types::Strict::True" => "::TrueClass",
|
|
81
|
+
"Types::False" => "::FalseClass",
|
|
82
|
+
"Types::Strict::False" => "::FalseClass",
|
|
83
|
+
"Types::Array" => "::Array",
|
|
84
|
+
"Types::Strict::Array" => "::Array",
|
|
85
|
+
"Types::Hash" => "::Hash",
|
|
86
|
+
"Types::Strict::Hash" => "::Hash",
|
|
87
|
+
"Types::Symbol" => "::Symbol",
|
|
88
|
+
"Types::Strict::Symbol" => "::Symbol",
|
|
89
|
+
"Types::Coercible::Symbol" => "::Symbol",
|
|
90
|
+
"Types::Date" => "::Date",
|
|
91
|
+
"Types::Strict::Date" => "::Date",
|
|
92
|
+
"Types::DateTime" => "::DateTime",
|
|
93
|
+
"Types::Strict::DateTime" => "::DateTime",
|
|
94
|
+
"Types::Time" => "::Time",
|
|
95
|
+
"Types::Strict::Time" => "::Time",
|
|
96
|
+
"Types::Nil" => "::NilClass",
|
|
97
|
+
"Types::Strict::Nil" => "::NilClass",
|
|
98
|
+
"Types::Any" => "T.untyped",
|
|
99
|
+
}.freeze
|
|
100
|
+
|
|
101
|
+
ConstantType = type_member { { fixed: T.class_of(::Light::Services::Base) } }
|
|
102
|
+
|
|
103
|
+
class << self
|
|
104
|
+
extend T::Sig
|
|
105
|
+
|
|
106
|
+
sig { override.returns(T::Enumerable[Module]) }
|
|
107
|
+
def gather_constants
|
|
108
|
+
all_classes.select do |klass|
|
|
109
|
+
klass < ::Light::Services::Base && klass.name && klass != ::Light::Services::Base
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
sig { override.void }
|
|
115
|
+
def decorate
|
|
116
|
+
arguments = constant.arguments
|
|
117
|
+
outputs = constant.outputs
|
|
118
|
+
|
|
119
|
+
return if arguments.empty? && outputs.empty?
|
|
120
|
+
|
|
121
|
+
root.create_path(constant) do |klass|
|
|
122
|
+
# Generate argument methods
|
|
123
|
+
arguments.each_value do |field|
|
|
124
|
+
generate_field_methods(klass, field)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Generate output methods
|
|
128
|
+
outputs.each_value do |field|
|
|
129
|
+
generate_field_methods(klass, field)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
sig { params(klass: RBI::Scope, field: ::Light::Services::Settings::Field).void }
|
|
137
|
+
def generate_field_methods(klass, field)
|
|
138
|
+
name = field.name.to_s
|
|
139
|
+
ruby_type = resolve_type(field)
|
|
140
|
+
return_type = field.optional ? as_nilable_type(ruby_type) : ruby_type
|
|
141
|
+
|
|
142
|
+
# Getter
|
|
143
|
+
klass.create_method(name, return_type: return_type)
|
|
144
|
+
|
|
145
|
+
# Predicate
|
|
146
|
+
klass.create_method("#{name}?", return_type: "T::Boolean")
|
|
147
|
+
|
|
148
|
+
# Setter (private)
|
|
149
|
+
klass.create_method(
|
|
150
|
+
"#{name}=",
|
|
151
|
+
parameters: [create_param("value", type: return_type)],
|
|
152
|
+
return_type: return_type,
|
|
153
|
+
visibility: RBI::Private.new,
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
sig { params(field: ::Light::Services::Settings::Field).returns(String) }
|
|
158
|
+
def resolve_type(field)
|
|
159
|
+
type = field.instance_variable_get(:@type)
|
|
160
|
+
return "T.untyped" unless type
|
|
161
|
+
|
|
162
|
+
if type.is_a?(Array)
|
|
163
|
+
resolve_array_type(type)
|
|
164
|
+
elsif dry_type?(type)
|
|
165
|
+
resolve_dry_type(type)
|
|
166
|
+
elsif type.is_a?(Class) || type.is_a?(Module)
|
|
167
|
+
ruby_type_for_class(type)
|
|
168
|
+
else
|
|
169
|
+
"T.untyped"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
sig { params(types: T::Array[T.untyped]).returns(String) }
|
|
174
|
+
def resolve_array_type(types)
|
|
175
|
+
resolved_types = types.map do |t|
|
|
176
|
+
if t.is_a?(Class) || t.is_a?(Module)
|
|
177
|
+
ruby_type_for_class(t)
|
|
178
|
+
elsif dry_type?(t)
|
|
179
|
+
resolve_dry_type(t)
|
|
180
|
+
else
|
|
181
|
+
"T.untyped"
|
|
182
|
+
end
|
|
183
|
+
end.uniq
|
|
184
|
+
|
|
185
|
+
return resolved_types.first if resolved_types.size == 1
|
|
186
|
+
|
|
187
|
+
# Check if this is a boolean type (TrueClass + FalseClass)
|
|
188
|
+
if resolved_types.sort == ["::FalseClass", "::TrueClass"]
|
|
189
|
+
"T::Boolean"
|
|
190
|
+
else
|
|
191
|
+
"T.any(#{resolved_types.join(', ')})"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
sig { params(klass: T.any(Class, Module)).returns(String) }
|
|
196
|
+
def ruby_type_for_class(klass)
|
|
197
|
+
name = klass.name
|
|
198
|
+
return "T.untyped" unless name
|
|
199
|
+
|
|
200
|
+
# Handle boolean types specially
|
|
201
|
+
if klass == TrueClass
|
|
202
|
+
"::TrueClass"
|
|
203
|
+
elsif klass == FalseClass
|
|
204
|
+
"::FalseClass"
|
|
205
|
+
else
|
|
206
|
+
"::#{name}"
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
sig { params(type: T.untyped).returns(T::Boolean) }
|
|
211
|
+
def dry_type?(type)
|
|
212
|
+
return false unless defined?(Dry::Types::Type)
|
|
213
|
+
|
|
214
|
+
type.is_a?(Dry::Types::Type)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
sig { params(type: T.untyped).returns(String) }
|
|
218
|
+
def resolve_dry_type(type)
|
|
219
|
+
type_string = type.to_s
|
|
220
|
+
|
|
221
|
+
# Direct mapping lookup
|
|
222
|
+
return DRY_TYPE_MAPPINGS[type_string] if DRY_TYPE_MAPPINGS.key?(type_string)
|
|
223
|
+
|
|
224
|
+
# Handle parameterized types: Types::Array.of(...) โ Types::Array
|
|
225
|
+
base_type = type_string.split(".").first
|
|
226
|
+
return DRY_TYPE_MAPPINGS[base_type] if DRY_TYPE_MAPPINGS.key?(base_type)
|
|
227
|
+
|
|
228
|
+
# Try to infer from primitive
|
|
229
|
+
infer_from_primitive(type)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
sig { params(type: T.untyped).returns(String) }
|
|
233
|
+
def infer_from_primitive(type)
|
|
234
|
+
return "T.untyped" unless type.respond_to?(:primitive)
|
|
235
|
+
|
|
236
|
+
primitive = type.primitive
|
|
237
|
+
return "T.untyped" unless primitive.is_a?(Class) || primitive.is_a?(Module)
|
|
238
|
+
|
|
239
|
+
ruby_type_for_class(primitive)
|
|
240
|
+
rescue StandardError
|
|
241
|
+
"T.untyped"
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
sig { params(type: String).returns(String) }
|
|
245
|
+
def as_nilable_type(type)
|
|
246
|
+
# Don't double-wrap nilable types
|
|
247
|
+
return type if type.start_with?("T.nilable(")
|
|
248
|
+
return type if type == "T.untyped"
|
|
249
|
+
|
|
250
|
+
"T.nilable(#{type})"
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: light-services
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Kodkod
|
|
@@ -55,6 +55,7 @@ files:
|
|
|
55
55
|
- docs/ruby-lsp.md
|
|
56
56
|
- docs/service-rendering.md
|
|
57
57
|
- docs/steps.md
|
|
58
|
+
- docs/tapioca.md
|
|
58
59
|
- docs/testing.md
|
|
59
60
|
- lib/generators/light_services/install/USAGE
|
|
60
61
|
- lib/generators/light_services/install/install_generator.rb
|
|
@@ -99,6 +100,7 @@ files:
|
|
|
99
100
|
- lib/light/services/rubocop/cop/light_services/no_direct_instantiation.rb
|
|
100
101
|
- lib/light/services/rubocop/cop/light_services/output_type_required.rb
|
|
101
102
|
- lib/light/services/rubocop/cop/light_services/prefer_fail_method.rb
|
|
103
|
+
- lib/light/services/rubocop/cop/light_services/reserved_name.rb
|
|
102
104
|
- lib/light/services/rubocop/cop/light_services/step_method_exists.rb
|
|
103
105
|
- lib/light/services/settings/field.rb
|
|
104
106
|
- lib/light/services/settings/step.rb
|
|
@@ -107,6 +109,7 @@ files:
|
|
|
107
109
|
- lib/ruby_lsp/light_services/addon.rb
|
|
108
110
|
- lib/ruby_lsp/light_services/definition.rb
|
|
109
111
|
- lib/ruby_lsp/light_services/indexing_enhancement.rb
|
|
112
|
+
- lib/tapioca/dsl/compilers/light_services.rb
|
|
110
113
|
- light-services.gemspec
|
|
111
114
|
homepage: https://light-services-docs.vercel.app/
|
|
112
115
|
licenses:
|