light-services 3.2.1 โ 3.3.1
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 +12 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +3 -1
- data/README.md +1 -0
- data/docs/README.md +1 -0
- data/docs/SUMMARY.md +2 -0
- data/docs/arguments.md +32 -0
- data/docs/outputs.md +16 -0
- data/docs/sorbet-runtime.md +277 -0
- data/docs/tapioca.md +200 -0
- data/lib/light/services/settings/field.rb +34 -1
- 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: 814307f260796e4439cd7e9602159281b4457003c9b9a70f5596ecb81a80c7bc
|
|
4
|
+
data.tar.gz: e9ac19b493f3559b0e2ba1b978c7dd3e51615614c1c1f239f1326412e6126950
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ba524199e7078ad1dbbf0a4df34ef4bb50f59c8950695e74e8a2b9f6691ae0ca673b16127700e2a7d0bdc66a1762fd886d4a89efe4e531840a092a49b0490b3
|
|
7
|
+
data.tar.gz: ecacfe8dd84b3fb1b1ac944b46e4ec6ed08d15eb6857f9fec82f070f90a87a51c97855d7ab46cc62f007f32c77f528fb178b9829c46603121978a5752905cb49
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.3.1 (2025-12-16)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Sorbet runtime type support for `arg` and `output` (validation only, no coercion)
|
|
8
|
+
|
|
9
|
+
## 3.3.0 (2025-12-15)
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Sorbet and Tapioca support for `arg` and `output`
|
|
14
|
+
|
|
3
15
|
## 3.2.1 (2025-12-15)
|
|
4
16
|
|
|
5
17
|
### Added
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
light-services (3.
|
|
4
|
+
light-services (3.3.1)
|
|
5
5
|
|
|
6
6
|
GEM
|
|
7
7
|
remote: https://rubygems.org/
|
|
@@ -126,6 +126,7 @@ GEM
|
|
|
126
126
|
simplecov_json_formatter (~> 0.1)
|
|
127
127
|
simplecov-html (0.13.2)
|
|
128
128
|
simplecov_json_formatter (0.1.4)
|
|
129
|
+
sorbet-runtime (0.6.12848)
|
|
129
130
|
sqlite3 (2.8.1)
|
|
130
131
|
mini_portile2 (~> 2.8.0)
|
|
131
132
|
timeout (0.5.0)
|
|
@@ -154,6 +155,7 @@ DEPENDENCIES
|
|
|
154
155
|
rubocop-rake
|
|
155
156
|
rubocop-rspec
|
|
156
157
|
simplecov
|
|
158
|
+
sorbet-runtime
|
|
157
159
|
sqlite3
|
|
158
160
|
|
|
159
161
|
BUNDLED WITH
|
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/arguments.md
CHANGED
|
@@ -124,6 +124,38 @@ service = User::Create.run(name: "John", age: "25")
|
|
|
124
124
|
service.age # => 25 (Integer, not String)
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
+
### Sorbet Runtime Types
|
|
128
|
+
|
|
129
|
+
Light Services also supports [Sorbet runtime types](https://sorbet.org/docs/runtime) for type validation. Unlike dry-types, Sorbet types **only validate** and do not coerce values.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
require "sorbet-runtime"
|
|
133
|
+
|
|
134
|
+
class User::Create < ApplicationService
|
|
135
|
+
# Basic types using T::Utils.coerce
|
|
136
|
+
arg :name, type: T::Utils.coerce(String)
|
|
137
|
+
arg :age, type: T::Utils.coerce(Integer)
|
|
138
|
+
|
|
139
|
+
# Nilable types
|
|
140
|
+
arg :email, type: T.nilable(String), optional: true
|
|
141
|
+
|
|
142
|
+
# Union types
|
|
143
|
+
arg :status, type: T.any(String, Symbol)
|
|
144
|
+
|
|
145
|
+
# Typed arrays
|
|
146
|
+
arg :tags, type: T::Array[String]
|
|
147
|
+
|
|
148
|
+
# Boolean type
|
|
149
|
+
arg :active, type: T::Boolean, default: true
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
{% hint style="warning" %}
|
|
154
|
+
**Sorbet types do NOT coerce values.** If you pass `"25"` where an `Integer` is expected, it will raise an error instead of converting the string to an integer. Use dry-types if you need automatic coercion.
|
|
155
|
+
{% endhint %}
|
|
156
|
+
|
|
157
|
+
See the [Sorbet Runtime Types documentation](sorbet-runtime.md) for more details.
|
|
158
|
+
|
|
127
159
|
## Required Arguments
|
|
128
160
|
|
|
129
161
|
By default, arguments are required. You can make them optional by setting `optional` to `true`.
|
data/docs/outputs.md
CHANGED
|
@@ -124,6 +124,22 @@ class AI::Chat < ApplicationService
|
|
|
124
124
|
end
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
+
### Sorbet Runtime Types
|
|
128
|
+
|
|
129
|
+
Outputs also support [Sorbet runtime types](https://sorbet.org/docs/runtime) for type validation:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
require "sorbet-runtime"
|
|
133
|
+
|
|
134
|
+
class AI::Chat < ApplicationService
|
|
135
|
+
output :messages, type: T::Array[Hash]
|
|
136
|
+
output :total_tokens, type: T::Utils.coerce(Integer)
|
|
137
|
+
output :metadata, type: T.nilable(Hash), optional: true
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
See the [Sorbet Runtime Types documentation](sorbet-runtime.md) for more details.
|
|
142
|
+
|
|
127
143
|
## Default Values
|
|
128
144
|
|
|
129
145
|
Set default values for outputs using the `default` option. The default value will be automatically set before the execution of steps.
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# Sorbet Runtime Types
|
|
2
|
+
|
|
3
|
+
Light Services supports [Sorbet runtime types](https://sorbet.org/docs/runtime) for type validation of arguments and outputs. This provides runtime type checking using Sorbet's type system.
|
|
4
|
+
|
|
5
|
+
{% hint style="info" %}
|
|
6
|
+
This page covers **runtime type checking** with `sorbet-runtime`. For **static type analysis** with Sorbet and RBI file generation, see [Tapioca / Sorbet Integration](tapioca.md).
|
|
7
|
+
{% endhint %}
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Add `sorbet-runtime` to your Gemfile:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem "sorbet-runtime"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then run:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bundle install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Basic Usage
|
|
24
|
+
|
|
25
|
+
When `sorbet-runtime` is loaded, plain Ruby classes are automatically validated using Sorbet's type system:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
require "sorbet-runtime"
|
|
29
|
+
|
|
30
|
+
class User::Create < ApplicationService
|
|
31
|
+
# Basic types - plain Ruby classes work directly!
|
|
32
|
+
arg :name, type: String
|
|
33
|
+
arg :age, type: Integer
|
|
34
|
+
|
|
35
|
+
# Nilable types (allows nil)
|
|
36
|
+
arg :email, type: T.nilable(String), optional: true
|
|
37
|
+
|
|
38
|
+
# Union types (multiple allowed types)
|
|
39
|
+
arg :status, type: T.any(String, Symbol), default: "pending"
|
|
40
|
+
|
|
41
|
+
# Typed arrays
|
|
42
|
+
arg :tags, type: T::Array[String], optional: true
|
|
43
|
+
|
|
44
|
+
# Boolean type
|
|
45
|
+
arg :active, type: T::Boolean, default: true
|
|
46
|
+
|
|
47
|
+
# Outputs with Sorbet types - plain classes work here too
|
|
48
|
+
output :user, type: User
|
|
49
|
+
output :metadata, type: Hash
|
|
50
|
+
|
|
51
|
+
step :create_user
|
|
52
|
+
step :build_metadata
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def create_user
|
|
57
|
+
self.user = User.create!(
|
|
58
|
+
name: name,
|
|
59
|
+
age: age,
|
|
60
|
+
email: email,
|
|
61
|
+
status: status,
|
|
62
|
+
tags: tags || [],
|
|
63
|
+
active: active
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def build_metadata
|
|
68
|
+
self.metadata = { created_at: Time.current }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Type Reference
|
|
74
|
+
|
|
75
|
+
### Basic Types
|
|
76
|
+
|
|
77
|
+
When `sorbet-runtime` is loaded, plain Ruby classes are automatically coerced to Sorbet types:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
arg :name, type: String
|
|
81
|
+
arg :count, type: Integer
|
|
82
|
+
arg :price, type: Float
|
|
83
|
+
arg :data, type: Hash
|
|
84
|
+
arg :items, type: Array
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
{% hint style="info" %}
|
|
88
|
+
You can also use `T::Utils.coerce(String)` explicitly, but it's not required - Light Services handles the coercion automatically.
|
|
89
|
+
{% endhint %}
|
|
90
|
+
|
|
91
|
+
### Nilable Types
|
|
92
|
+
|
|
93
|
+
Allow `nil` values with `T.nilable`:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
arg :nickname, type: T.nilable(String), optional: true
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Union Types
|
|
100
|
+
|
|
101
|
+
Allow multiple types with `T.any`:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
arg :identifier, type: T.any(String, Integer)
|
|
105
|
+
arg :status, type: T.any(String, Symbol)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Typed Arrays
|
|
109
|
+
|
|
110
|
+
Validate array element types with `T::Array`:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
arg :tags, type: T::Array[String]
|
|
114
|
+
arg :numbers, type: T::Array[Integer]
|
|
115
|
+
arg :users, type: T::Array[User]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
{% hint style="warning" %}
|
|
119
|
+
**Generic Type Erasure:** Sorbet's `T::Array[String]` only validates that the value is an `Array` at runtime. The type parameter (`String`) is **erased** and not checked. For strict array element validation, use dry-types: `Types::Array.of(Types::Strict::String)`.
|
|
120
|
+
{% endhint %}
|
|
121
|
+
|
|
122
|
+
### Boolean Type
|
|
123
|
+
|
|
124
|
+
Use `T::Boolean` for true/false values:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
arg :active, type: T::Boolean
|
|
128
|
+
arg :verified, type: T::Boolean, default: false
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Complex Types
|
|
132
|
+
|
|
133
|
+
Combine types for more complex validations:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
# Nilable array
|
|
137
|
+
arg :tags, type: T.nilable(T::Array[String]), optional: true
|
|
138
|
+
|
|
139
|
+
# Array of union types
|
|
140
|
+
arg :identifiers, type: T::Array[T.any(String, Integer)]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Key Differences from dry-types
|
|
144
|
+
|
|
145
|
+
{% hint style="warning" %}
|
|
146
|
+
**Sorbet types do NOT coerce values.** Unlike dry-types which can automatically convert values (e.g., `"123"` โ `123`), Sorbet runtime types only validate that values match the expected type.
|
|
147
|
+
{% endhint %}
|
|
148
|
+
|
|
149
|
+
### Coercion Behavior Comparison
|
|
150
|
+
|
|
151
|
+
**With dry-types (coerces):**
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
# dry-types with coercible integer
|
|
155
|
+
arg :age, type: Types::Coercible::Integer
|
|
156
|
+
|
|
157
|
+
service = MyService.run(age: "25")
|
|
158
|
+
service.age # => 25 (Integer - coerced from String)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**With Sorbet runtime (validates only):**
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
# Sorbet runtime type (plain class or T::Utils.coerce)
|
|
165
|
+
arg :age, type: Integer
|
|
166
|
+
|
|
167
|
+
service = MyService.run(age: "25")
|
|
168
|
+
# => Raises ArgTypeError: expected Integer, got String
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### When to Use Each
|
|
172
|
+
|
|
173
|
+
| Use Case | Recommended |
|
|
174
|
+
|----------|-------------|
|
|
175
|
+
| Strict type validation | Sorbet runtime |
|
|
176
|
+
| Automatic value coercion | dry-types |
|
|
177
|
+
| Static type analysis | Sorbet + Tapioca |
|
|
178
|
+
| Input from external sources (APIs, forms) | dry-types (for coercion) |
|
|
179
|
+
| Internal service-to-service calls | Sorbet runtime |
|
|
180
|
+
|
|
181
|
+
## Combining with Tapioca
|
|
182
|
+
|
|
183
|
+
For full Sorbet support, you can use both:
|
|
184
|
+
|
|
185
|
+
1. **Runtime types** (`sorbet-runtime`) - Validates types at runtime
|
|
186
|
+
2. **Static types** (Tapioca) - Generates RBI files for static analysis
|
|
187
|
+
|
|
188
|
+
See [Tapioca / Sorbet Integration](tapioca.md) for setting up static type analysis.
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
# typed: strict
|
|
192
|
+
|
|
193
|
+
class User::Create < ApplicationService
|
|
194
|
+
# Runtime validation with Sorbet types
|
|
195
|
+
arg :name, type: String
|
|
196
|
+
arg :age, type: Integer
|
|
197
|
+
|
|
198
|
+
output :user, type: User
|
|
199
|
+
|
|
200
|
+
# ...
|
|
201
|
+
end
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
With Tapioca configured, you get:
|
|
205
|
+
- **Runtime validation** from `sorbet-runtime`
|
|
206
|
+
- **IDE autocompletion** from generated RBI files
|
|
207
|
+
- **Static type checking** from `srb tc`
|
|
208
|
+
|
|
209
|
+
## Error Messages
|
|
210
|
+
|
|
211
|
+
When type validation fails, Light Services raises `ArgTypeError` with a descriptive message:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
service = User::Create.run(name: 123, age: 25)
|
|
215
|
+
# => Light::Services::ArgTypeError: User::Create argument `name` expected String, but got Integer with value: 123
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Full Example
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
require "sorbet-runtime"
|
|
222
|
+
|
|
223
|
+
class Order::Create < ApplicationService
|
|
224
|
+
# Required arguments - plain classes work!
|
|
225
|
+
arg :customer, type: Customer
|
|
226
|
+
arg :items, type: T::Array[OrderItem]
|
|
227
|
+
arg :total, type: T.any(Integer, Float)
|
|
228
|
+
|
|
229
|
+
# Optional arguments
|
|
230
|
+
arg :notes, type: T.nilable(String), optional: true
|
|
231
|
+
arg :priority, type: T::Boolean, default: false
|
|
232
|
+
arg :tags, type: T::Array[String], optional: true
|
|
233
|
+
|
|
234
|
+
# Outputs
|
|
235
|
+
output :order, type: Order
|
|
236
|
+
output :confirmation_number, type: String
|
|
237
|
+
|
|
238
|
+
step :validate_items
|
|
239
|
+
step :create_order
|
|
240
|
+
step :generate_confirmation
|
|
241
|
+
|
|
242
|
+
private
|
|
243
|
+
|
|
244
|
+
def validate_items
|
|
245
|
+
fail!("Order must have at least one item") if items.empty?
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def create_order
|
|
249
|
+
self.order = Order.create!(
|
|
250
|
+
customer: customer,
|
|
251
|
+
items: items,
|
|
252
|
+
total: total,
|
|
253
|
+
notes: notes,
|
|
254
|
+
priority: priority,
|
|
255
|
+
tags: tags || []
|
|
256
|
+
)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def generate_confirmation
|
|
260
|
+
self.confirmation_number = "ORD-#{order.id}-#{SecureRandom.hex(4).upcase}"
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Usage
|
|
265
|
+
result = Order::Create.run(
|
|
266
|
+
customer: current_user,
|
|
267
|
+
items: [item1, item2],
|
|
268
|
+
total: 99.99,
|
|
269
|
+
priority: true
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
if result.success?
|
|
273
|
+
puts "Order created: #{result.confirmation_number}"
|
|
274
|
+
else
|
|
275
|
+
puts "Failed: #{result.errors.full_messages}"
|
|
276
|
+
end
|
|
277
|
+
```
|
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
|
|
@@ -46,7 +46,7 @@ module Light
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
# Validate a value against the field's type definition.
|
|
49
|
-
# Supports
|
|
49
|
+
# Supports Ruby class types, dry-types, and Sorbet runtime types.
|
|
50
50
|
#
|
|
51
51
|
# @param value [Object] the value to validate
|
|
52
52
|
# @return [Object] the value (possibly coerced by dry-types)
|
|
@@ -56,6 +56,8 @@ module Light
|
|
|
56
56
|
|
|
57
57
|
if dry_type?(@type)
|
|
58
58
|
coerce_and_validate_dry_type!(value)
|
|
59
|
+
elsif sorbet_type?(@type) || (sorbet_available? && plain_class_type?(@type))
|
|
60
|
+
validate_sorbet_type!(value)
|
|
59
61
|
else
|
|
60
62
|
validate_ruby_type!(value)
|
|
61
63
|
value
|
|
@@ -64,6 +66,16 @@ module Light
|
|
|
64
66
|
|
|
65
67
|
private
|
|
66
68
|
|
|
69
|
+
# Check if sorbet-runtime is available
|
|
70
|
+
def sorbet_available?
|
|
71
|
+
defined?(T::Types::Base)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Check if the type is a plain Ruby class (not dry-types or Sorbet type)
|
|
75
|
+
def plain_class_type?(type)
|
|
76
|
+
type.is_a?(Class) || type.is_a?(Module)
|
|
77
|
+
end
|
|
78
|
+
|
|
67
79
|
# Check if the type is a dry-types type
|
|
68
80
|
def dry_type?(type)
|
|
69
81
|
return false unless defined?(Dry::Types::Type)
|
|
@@ -71,6 +83,13 @@ module Light
|
|
|
71
83
|
type.is_a?(Dry::Types::Type)
|
|
72
84
|
end
|
|
73
85
|
|
|
86
|
+
# Check if the type is a Sorbet runtime type
|
|
87
|
+
def sorbet_type?(type)
|
|
88
|
+
return false unless defined?(T::Types::Base)
|
|
89
|
+
|
|
90
|
+
type.is_a?(T::Types::Base)
|
|
91
|
+
end
|
|
92
|
+
|
|
74
93
|
# Validate and coerce value against dry-types
|
|
75
94
|
# Returns the coerced value
|
|
76
95
|
def coerce_and_validate_dry_type!(value)
|
|
@@ -80,6 +99,20 @@ module Light
|
|
|
80
99
|
"#{@service_class} #{@field_type} `#{@name}` #{e.message}"
|
|
81
100
|
end
|
|
82
101
|
|
|
102
|
+
# Validate value against Sorbet runtime types
|
|
103
|
+
# Note: Sorbet types only validate, they do not coerce values
|
|
104
|
+
# Automatically coerces plain Ruby classes to Sorbet types when needed
|
|
105
|
+
# @return [Object] the original value if valid
|
|
106
|
+
# @raise [ArgTypeError] if the value doesn't match the expected type
|
|
107
|
+
def validate_sorbet_type!(value)
|
|
108
|
+
sorbet_type = sorbet_type?(@type) ? @type : T::Utils.coerce(@type)
|
|
109
|
+
return value if sorbet_type.valid?(value)
|
|
110
|
+
|
|
111
|
+
raise Light::Services::ArgTypeError,
|
|
112
|
+
"#{@service_class} #{@field_type} `#{@name}` expected #{sorbet_type.name}, " \
|
|
113
|
+
"but got #{value.class} with value: #{value.inspect}"
|
|
114
|
+
end
|
|
115
|
+
|
|
83
116
|
# Validate value against Ruby class types
|
|
84
117
|
def validate_ruby_type!(value)
|
|
85
118
|
return if [*@type].any? { |type| value.is_a?(type) }
|
|
@@ -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.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Kodkod
|
|
@@ -54,7 +54,9 @@ files:
|
|
|
54
54
|
- docs/rubocop.md
|
|
55
55
|
- docs/ruby-lsp.md
|
|
56
56
|
- docs/service-rendering.md
|
|
57
|
+
- docs/sorbet-runtime.md
|
|
57
58
|
- docs/steps.md
|
|
59
|
+
- docs/tapioca.md
|
|
58
60
|
- docs/testing.md
|
|
59
61
|
- lib/generators/light_services/install/USAGE
|
|
60
62
|
- lib/generators/light_services/install/install_generator.rb
|
|
@@ -108,6 +110,7 @@ files:
|
|
|
108
110
|
- lib/ruby_lsp/light_services/addon.rb
|
|
109
111
|
- lib/ruby_lsp/light_services/definition.rb
|
|
110
112
|
- lib/ruby_lsp/light_services/indexing_enhancement.rb
|
|
113
|
+
- lib/tapioca/dsl/compilers/light_services.rb
|
|
111
114
|
- light-services.gemspec
|
|
112
115
|
homepage: https://light-services-docs.vercel.app/
|
|
113
116
|
licenses:
|