service_core 0.2.0 → 1.0.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/.rubocop.yml +19 -2
- data/.tool-versions +1 -0
- data/Appraisals +19 -0
- data/CHANGELOG.md +79 -1
- data/README.md +186 -220
- data/Rakefile +0 -2
- data/gemfiles/rails_7.2.gemfile +18 -0
- data/gemfiles/rails_8.0.gemfile +18 -0
- data/gemfiles/rails_8.1.gemfile +18 -0
- data/lib/service_core/base.rb +32 -26
- data/lib/service_core/errors.rb +11 -0
- data/lib/service_core/field_set.rb +21 -0
- data/lib/service_core/logger.rb +0 -2
- data/lib/service_core/output.rb +15 -13
- data/lib/service_core/responder.rb +38 -0
- data/lib/service_core/response.rb +95 -22
- data/lib/service_core/step_validation.rb +24 -28
- data/lib/service_core/version.rb +1 -3
- data/lib/service_core.rb +3 -4
- metadata +20 -15
- data/.byebug_history +0 -71
- data/sig/service_core.rbs +0 -4
data/README.md
CHANGED
|
@@ -1,40 +1,50 @@
|
|
|
1
1
|
# ServiceCore
|
|
2
2
|
|
|
3
|
-
ServiceCore
|
|
3
|
+
ServiceCore is a small Ruby gem that gives service objects a shared shape. Every service exposes a single `call` method and returns the same four-key response, regardless of who wrote it. The idea behind the shape is unpacked in [The Shape of a Service Response](https://agnosticlogic.substack.com/p/the-shape-of-a-service-response).
|
|
4
|
+
|
|
5
|
+
- Four-key response contract: **status**, **data**, **message**, **errors**.
|
|
6
|
+
- Field declarations with types, defaults, and ActiveModel validations.
|
|
7
|
+
- Step-by-step validation that survives `valid?` calls.
|
|
8
|
+
- Hash-compatible value objects (`Response`, `FieldSet`) instead of raw hashes.
|
|
9
|
+
- Works on Ruby >= 3.1 and Rails (ActiveModel/ActiveSupport) 6.1 through 8.x.
|
|
4
10
|
|
|
5
11
|
## Installation
|
|
6
|
-
Install the gem and add to the application's Gemfile by executing:
|
|
7
12
|
|
|
8
13
|
```sh
|
|
9
14
|
bundle add service_core
|
|
10
15
|
```
|
|
11
|
-
|
|
16
|
+
|
|
17
|
+
Or add it to your Gemfile:
|
|
12
18
|
|
|
13
19
|
```ruby
|
|
14
20
|
gem "service_core"
|
|
15
21
|
```
|
|
16
22
|
|
|
17
|
-
If
|
|
23
|
+
If you are not using Bundler:
|
|
18
24
|
|
|
19
25
|
```sh
|
|
20
26
|
gem install service_core
|
|
21
27
|
```
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
## The four-key response
|
|
30
|
+
|
|
31
|
+
Every service responds with at most four keys:
|
|
32
|
+
|
|
33
|
+
| Key | Purpose |
|
|
34
|
+
| --------- | ---------------------------------------------------------------------- |
|
|
35
|
+
| `status` | Machine-readable signal (`"success"`, `"error"`, or any custom state). |
|
|
36
|
+
| `data` | The payload the caller asked for. |
|
|
37
|
+
| `message` | High-level human context, distinct from per-field error detail. |
|
|
38
|
+
| `errors` | Structured error detail (Hash, Array, ActiveModel::Errors, ...). |
|
|
29
39
|
|
|
30
|
-
The
|
|
31
|
-
## Usage
|
|
40
|
+
The shape is enforced; the value types are not. Writing a key other than these four raises `ServiceCore::InvalidKey`.
|
|
32
41
|
|
|
33
|
-
|
|
42
|
+
## Defining a service
|
|
43
|
+
|
|
44
|
+
Include `ServiceCore` in your class and implement `perform`:
|
|
34
45
|
|
|
35
|
-
To define a new service, include the `ServiceCore` module in your service class and define your fields and the `perform` method.
|
|
36
46
|
```ruby
|
|
37
|
-
class
|
|
47
|
+
class GreetService
|
|
38
48
|
include ServiceCore
|
|
39
49
|
|
|
40
50
|
field :first_name, :string
|
|
@@ -42,212 +52,137 @@ class MyService
|
|
|
42
52
|
field :active, :boolean, default: true
|
|
43
53
|
|
|
44
54
|
def perform
|
|
45
|
-
success_response(message: "Hello, World", data:
|
|
55
|
+
success_response(message: "Hello, World", data: full_name)
|
|
46
56
|
end
|
|
47
57
|
|
|
48
|
-
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def full_name
|
|
49
61
|
"#{first_name} #{last_name}"
|
|
50
62
|
end
|
|
51
63
|
end
|
|
52
64
|
```
|
|
53
65
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
service = MyService.new(first_name: "John", last_name: "Doe")
|
|
58
|
-
result = service.call
|
|
59
|
-
puts result
|
|
60
|
-
# Output:
|
|
61
|
-
# {
|
|
62
|
-
# status: "success",
|
|
63
|
-
# message: "Hello, World",
|
|
64
|
-
#. data: "John Doe"
|
|
65
|
-
# }
|
|
66
|
-
|
|
67
|
-
puts service.output
|
|
68
|
-
# Output:
|
|
69
|
-
# {
|
|
70
|
-
# status: "success",
|
|
71
|
-
# message: "Hello, World",
|
|
72
|
-
#. data: "John Doe"
|
|
73
|
-
# }
|
|
74
|
-
```
|
|
66
|
+
## Calling a service
|
|
67
|
+
|
|
68
|
+
You can call a service either via `new(...).call` or via the `.call` shortcut on the class:
|
|
75
69
|
|
|
76
|
-
The `call` method can be invoked on the service class and it too will return the object of the service.
|
|
77
70
|
```ruby
|
|
78
|
-
|
|
79
|
-
puts
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# }
|
|
71
|
+
response = GreetService.new(first_name: "John", last_name: "Doe").call
|
|
72
|
+
puts response
|
|
73
|
+
# => {status: "success", message: "Hello, World", data: "John Doe"}
|
|
74
|
+
|
|
75
|
+
service = GreetService.call(first_name: "John", last_name: "Doe")
|
|
76
|
+
service.response
|
|
77
|
+
# => {status: "success", message: "Hello, World", data: "John Doe"}
|
|
86
78
|
```
|
|
87
79
|
|
|
88
|
-
|
|
89
|
-
The `field` method can define primitive types and objects, like hash/array or any object. For objects, there is no need to declare the datatype.
|
|
90
|
-
```ruby
|
|
91
|
-
class MyService
|
|
92
|
-
include ServiceCore
|
|
80
|
+
The instance method returns the response value object. The class-level `.call` returns the service instance, so you can also reach for `service.response` (or `service.output`, which is kept as an alias) after the fact.
|
|
93
81
|
|
|
94
|
-
|
|
95
|
-
field :last_name, :string
|
|
96
|
-
field :payload # can be object/hash/array
|
|
82
|
+
## `Response`: the value object
|
|
97
83
|
|
|
98
|
-
|
|
99
|
-
success_response(message: "Hello, World", data: name)
|
|
100
|
-
end
|
|
84
|
+
`service.response` (and the value returned from `#call`) is a `ServiceCore::Response`. It exposes both named accessors and Hash-style access, and serialises to JSON like the underlying hash:
|
|
101
85
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
86
|
+
```ruby
|
|
87
|
+
response = GreetService.call(first_name: "John", last_name: "Doe").response
|
|
88
|
+
|
|
89
|
+
response.status # => "success"
|
|
90
|
+
response.data # => "John Doe"
|
|
91
|
+
response[:status] # => "success"
|
|
92
|
+
response == { status: "success", message: "Hello, World", data: "John Doe" } # => true
|
|
93
|
+
response.to_json # => '{"status":"success","message":"Hello, World","data":"John Doe"}'
|
|
106
94
|
```
|
|
107
95
|
|
|
108
|
-
|
|
96
|
+
Writing or reading a key other than the four allowed raises `ServiceCore::InvalidKey`.
|
|
97
|
+
|
|
98
|
+
## Declaring fields
|
|
99
|
+
|
|
100
|
+
`field` supports both typed and untyped declarations.
|
|
109
101
|
|
|
110
|
-
The `set_output` method provides a way to set output of a specific key. It is the method used by the response_setters to set specific output value
|
|
111
102
|
```ruby
|
|
112
103
|
class MyService
|
|
113
104
|
include ServiceCore
|
|
114
105
|
|
|
115
|
-
field :first_name, :string
|
|
116
|
-
field :
|
|
117
|
-
field :
|
|
106
|
+
field :first_name, :string # typed (ActiveModel::Attributes)
|
|
107
|
+
field :active, :boolean, default: true # typed with keyword default
|
|
108
|
+
field :enabled, :boolean, false # typed with positional default
|
|
109
|
+
field :payload # untyped, can be any object/hash/array
|
|
110
|
+
end
|
|
111
|
+
```
|
|
118
112
|
|
|
119
|
-
|
|
120
|
-
set_output :message, "Hello, World"
|
|
121
|
-
set_output :data, name
|
|
122
|
-
end
|
|
113
|
+
Typed fields are backed by `ActiveModel::Attributes` and inherit its casting and default support.
|
|
123
114
|
|
|
124
|
-
|
|
125
|
-
"#{first_name} #{last_name}"
|
|
126
|
-
end
|
|
127
|
-
end
|
|
115
|
+
The following names are reserved and cannot be used as field names because they would shadow methods the gem itself defines: `:call`, `:errors`, `:fields`, `:output`, `:perform`, `:response`. Declaring `field :errors` (for example) raises `ServiceCore::ReservedFieldName`.
|
|
128
116
|
|
|
129
|
-
|
|
130
|
-
puts obj.output
|
|
131
|
-
# Output:
|
|
132
|
-
# {
|
|
133
|
-
# status: "success",
|
|
134
|
-
# message: "Hello, World",
|
|
135
|
-
#. data: "John Doe"
|
|
136
|
-
# }
|
|
137
|
-
```
|
|
138
|
-
*NOTE:* If `:status` is not explicitly set in the perform method, the `success` status is returned if `errors` are blank else the `error` status is returned.
|
|
117
|
+
### Field snapshot via `FieldSet`
|
|
139
118
|
|
|
140
|
-
|
|
119
|
+
After construction, `service.fields` exposes an immutable snapshot of the declared fields and their values as a `ServiceCore::FieldSet`. Each declared symbol field is available as a real method; call `to_h` if you need a plain Hash.
|
|
141
120
|
|
|
142
|
-
#### `success_response`
|
|
143
|
-
Use the `success_response` method to return the `success` status, `data` and `message`
|
|
144
121
|
```ruby
|
|
122
|
+
service = GreetService.new(first_name: "John", last_name: "Doe")
|
|
145
123
|
|
|
146
|
-
|
|
147
|
-
|
|
124
|
+
service.fields.first_name # => "John"
|
|
125
|
+
service.fields.to_h # => { first_name: "John", last_name: "Doe", active: true }
|
|
126
|
+
```
|
|
148
127
|
|
|
149
|
-
|
|
150
|
-
field :last_name, :string
|
|
151
|
-
field :active, :boolean, default: true
|
|
128
|
+
The snapshot is taken at `#initialize`, so it reflects the values at construction time. Live values are still available through each declared accessor (e.g. `service.first_name`).
|
|
152
129
|
|
|
153
|
-
|
|
154
|
-
success_response(message: "Hello, World", data: name)
|
|
155
|
-
end
|
|
130
|
+
## Building responses
|
|
156
131
|
|
|
157
|
-
|
|
158
|
-
"#{first_name} #{last_name}"
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
service = MyService.new(first_name: "John", last_name: "Doe")
|
|
163
|
-
result = service.call
|
|
164
|
-
puts result
|
|
165
|
-
# Output:
|
|
166
|
-
# {
|
|
167
|
-
# status: "success",
|
|
168
|
-
# message: "Hello, World",
|
|
169
|
-
#. data: "John Doe"
|
|
170
|
-
# }
|
|
171
|
-
```
|
|
132
|
+
Three helpers cover almost every case.
|
|
172
133
|
|
|
173
|
-
`success_response`
|
|
174
|
-
- message
|
|
175
|
-
- data
|
|
134
|
+
### `success_response`
|
|
176
135
|
|
|
177
|
-
#### `error_response`
|
|
178
|
-
Use the `error_response` method to return the `error` status, `errors` and `message`
|
|
179
136
|
```ruby
|
|
137
|
+
def perform
|
|
138
|
+
success_response(message: "Hello, World", data: full_name)
|
|
139
|
+
end
|
|
140
|
+
# => {status: "success", message: "Hello, World", data: "John Doe"}
|
|
141
|
+
```
|
|
180
142
|
|
|
181
|
-
|
|
182
|
-
include ServiceCore
|
|
143
|
+
Accepts `message` and `data`. Status is set to `"success"`.
|
|
183
144
|
|
|
184
|
-
|
|
185
|
-
field :last_name, :string
|
|
186
|
-
field :active, :boolean, default: true
|
|
145
|
+
### `error_response`
|
|
187
146
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def name
|
|
193
|
-
"#{first_name} #{last_name}"
|
|
194
|
-
end
|
|
147
|
+
```ruby
|
|
148
|
+
def perform
|
|
149
|
+
error_response(message: "validation failure", errors: "last_name can't be blank")
|
|
195
150
|
end
|
|
196
|
-
|
|
197
|
-
service = MyService.new(first_name: "John")
|
|
198
|
-
result = service.call
|
|
199
|
-
puts result
|
|
200
|
-
# Output:
|
|
201
|
-
# {
|
|
202
|
-
# status: "error",
|
|
203
|
-
# message: "validation failure",
|
|
204
|
-
#. errors: "last_name can't be blank"
|
|
205
|
-
# }
|
|
151
|
+
# => {status: "error", message: "validation failure", errors: "last_name can't be blank"}
|
|
206
152
|
```
|
|
207
153
|
|
|
208
|
-
`
|
|
209
|
-
|
|
210
|
-
|
|
154
|
+
Accepts `message` and `errors`. Status is set to `"error"`. `errors` can be a String, Hash, Array, or `ActiveModel::Errors` (which is normalised through `messages`).
|
|
155
|
+
|
|
156
|
+
### `formatted_response`
|
|
157
|
+
|
|
158
|
+
For any status that isn't success or error.
|
|
211
159
|
|
|
212
|
-
#### `formatted_response`
|
|
213
|
-
Use the `formatted_response` method to return any status other than `success` or `error`.
|
|
214
160
|
```ruby
|
|
161
|
+
def perform
|
|
162
|
+
formatted_response(status: "processed", message: "Already done", data: existing_record)
|
|
163
|
+
end
|
|
164
|
+
# => {status: "processed", message: "Already done", data: ...}
|
|
165
|
+
```
|
|
215
166
|
|
|
216
|
-
|
|
217
|
-
include ServiceCore
|
|
167
|
+
Accepts `status`, `message`, `data`, and `errors`. Use this for `"pending"`, `"queued"`, `"processed"`, or any domain-specific status.
|
|
218
168
|
|
|
219
|
-
|
|
220
|
-
field :last_name, :string
|
|
221
|
-
field :active, :boolean, default: true
|
|
169
|
+
### `set_output`
|
|
222
170
|
|
|
223
|
-
|
|
224
|
-
formatted_response(status: 'processed', message: "Hello, World", data: name)
|
|
225
|
-
end
|
|
171
|
+
For finer-grained control, write a single key at a time:
|
|
226
172
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
173
|
+
```ruby
|
|
174
|
+
def perform
|
|
175
|
+
set_output(:message, "Hello, World")
|
|
176
|
+
set_output(:data, full_name)
|
|
230
177
|
end
|
|
231
|
-
|
|
232
|
-
service = MyService.new(first_name: "John", last_name: "Doe")
|
|
233
|
-
result = service.call
|
|
234
|
-
puts result
|
|
235
|
-
# Output:
|
|
236
|
-
# {
|
|
237
|
-
# status: "processed",
|
|
238
|
-
# message: "Hello, World",
|
|
239
|
-
#. data: "John Doe"
|
|
240
|
-
# }
|
|
241
178
|
```
|
|
242
179
|
|
|
243
|
-
`
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
180
|
+
If `status` is not set explicitly, it is auto-assigned to `"success"` when `errors` is blank, and `"error"` otherwise. `nil` is the only value treated as "not set"; `false`, `0`, and `""` are stored as-is.
|
|
181
|
+
|
|
182
|
+
## Validations
|
|
183
|
+
|
|
184
|
+
Standard ActiveModel validations run before `perform`. If they fail, the response is filled in for you.
|
|
248
185
|
|
|
249
|
-
### Validations
|
|
250
|
-
Define validation on the service and those will be invoked before service logic is invoked.
|
|
251
186
|
```ruby
|
|
252
187
|
class MyService
|
|
253
188
|
include ServiceCore
|
|
@@ -260,52 +195,43 @@ class MyService
|
|
|
260
195
|
end
|
|
261
196
|
end
|
|
262
197
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
puts result
|
|
266
|
-
# Output:
|
|
267
|
-
#{
|
|
268
|
-
# status: "error",
|
|
269
|
-
# message: "validation failure",
|
|
270
|
-
# errors: { name: ["can't be blank"] }
|
|
271
|
-
# }
|
|
198
|
+
MyService.new(name: "").call
|
|
199
|
+
# => {status: "error", message: "validation failure", errors: {name: ["can't be blank"]}}
|
|
272
200
|
```
|
|
273
201
|
|
|
274
|
-
### Step
|
|
275
|
-
|
|
202
|
+
### Step validation
|
|
203
|
+
|
|
204
|
+
When the result of one step decides the next, `add_error_and_validate` lets you accumulate errors mid-`perform` without `valid?` wiping them.
|
|
205
|
+
|
|
276
206
|
```ruby
|
|
277
207
|
class MyService
|
|
278
208
|
include ServiceCore
|
|
279
|
-
|
|
209
|
+
|
|
280
210
|
field :first_name, :string
|
|
281
211
|
field :last_name, :string
|
|
282
|
-
field :user
|
|
283
212
|
|
|
284
213
|
validates :first_name, presence: true
|
|
285
|
-
validates :user, presence: true
|
|
286
214
|
|
|
287
215
|
def perform
|
|
288
216
|
if last_name.blank?
|
|
289
217
|
add_error_and_validate(:last_name, "can't be nil")
|
|
290
218
|
return error_response(message: "validation failure", errors: errors)
|
|
291
219
|
end
|
|
292
|
-
|
|
293
|
-
|
|
220
|
+
|
|
221
|
+
success_response(data: { user: { id: 1 } })
|
|
294
222
|
end
|
|
295
223
|
end
|
|
296
224
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
# output:
|
|
300
|
-
# {
|
|
301
|
-
# status: "error",
|
|
302
|
-
# message: "validation failure",
|
|
303
|
-
# errors: { last_name: ["can't be nil"] }
|
|
304
|
-
# }
|
|
225
|
+
MyService.call(first_name: "abc").response
|
|
226
|
+
# => {status: "error", message: "validation failure", errors: {last_name: ["can't be nil"]}}
|
|
305
227
|
```
|
|
306
228
|
|
|
307
|
-
|
|
308
|
-
|
|
229
|
+
`add_error_and_validate(attribute, message, options = {})` forwards `options` to `ActiveModel::Errors#add`, so options like `strict: true` are honoured.
|
|
230
|
+
|
|
231
|
+
## Logging errors
|
|
232
|
+
|
|
233
|
+
`log_error(exception)` writes through the configured `ServiceCore.logger` and tags the message with the service class name.
|
|
234
|
+
|
|
309
235
|
```ruby
|
|
310
236
|
class MyService
|
|
311
237
|
include ServiceCore
|
|
@@ -313,43 +239,83 @@ class MyService
|
|
|
313
239
|
field :name, :string
|
|
314
240
|
|
|
315
241
|
def perform
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
error_response(message: "Failed", errors: { base: [e.message] })
|
|
321
|
-
end
|
|
242
|
+
raise StandardError, "Something went wrong"
|
|
243
|
+
rescue StandardError => e
|
|
244
|
+
log_error(e)
|
|
245
|
+
error_response(message: "Failed", errors: { base: [e.message] })
|
|
322
246
|
end
|
|
323
247
|
end
|
|
248
|
+
```
|
|
324
249
|
|
|
325
|
-
|
|
326
|
-
result = service.call
|
|
327
|
-
puts result
|
|
328
|
-
# Output:
|
|
329
|
-
# {
|
|
330
|
-
# status: "error",
|
|
331
|
-
# message: "Failed",
|
|
332
|
-
# errors: { base: ["Something went wrong"] }
|
|
333
|
-
# }
|
|
250
|
+
## Exceptions
|
|
334
251
|
|
|
252
|
+
All gem-specific exceptions inherit from `ServiceCore::Error`, so a single rescue block can catch anything ServiceCore raises:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
begin
|
|
256
|
+
MyService.call(...)
|
|
257
|
+
rescue ServiceCore::Error => e
|
|
258
|
+
# any gem-raised error
|
|
259
|
+
end
|
|
335
260
|
```
|
|
336
261
|
|
|
337
|
-
|
|
338
|
-
|
|
262
|
+
The current concrete subclasses are:
|
|
263
|
+
|
|
264
|
+
- `ServiceCore::InvalidKey` — raised by `response[:not_allowed]` or `response[:not_allowed] = value` when the key is not one of the four allowed response keys.
|
|
265
|
+
- `ServiceCore::ReservedFieldName` — raised by `field :errors` (or any other reserved name) at class-definition time.
|
|
266
|
+
|
|
267
|
+
Two raises stay on stdlib classes: `Response#fetch` raises `KeyError` to match `Hash#fetch`, and the default `perform` raises `StandardError` until the service overrides it.
|
|
268
|
+
|
|
269
|
+
## Configuration
|
|
270
|
+
|
|
339
271
|
```ruby
|
|
340
272
|
ServiceCore.configure do |config|
|
|
341
|
-
config.logger = Logger.new(
|
|
273
|
+
config.logger = Logger.new($stdout)
|
|
342
274
|
end
|
|
343
275
|
```
|
|
344
276
|
|
|
277
|
+
If you do not configure a logger, `ServiceCore.logger` defaults to `Rails.logger` when available, and otherwise to an `ActiveSupport::Logger` writing to `$stdout`.
|
|
278
|
+
|
|
279
|
+
## Stability
|
|
280
|
+
|
|
281
|
+
ServiceCore follows [Semantic Versioning](https://semver.org/). Starting with 1.0.0, the following are part of the public API and changes to them require a major version bump:
|
|
282
|
+
|
|
283
|
+
- The four-key response contract (`status`, `data`, `message`, `errors`).
|
|
284
|
+
- The Hash-compatible surface of `ServiceCore::Response` (`[]`, `[]=`, `==`, `to_h`, `to_s`, `inspect`, `keys`, `values`, `each`, `dig`, `fetch`, `key?` / `has_key?` / `include?`, `as_json`, `to_json`) and its named accessors (`status`, `data`, `message`, `errors`).
|
|
285
|
+
- The `ServiceCore::FieldSet` API (`to_h` and named accessors per declared symbol field).
|
|
286
|
+
- The service DSL: `include ServiceCore`, `field`, `validates`, `perform`, instance `#call` and class `.call`, `service.fields`, `service.response` / `service.output`.
|
|
287
|
+
- The response builders: `success_response`, `error_response`, `formatted_response`, `set_output`.
|
|
288
|
+
- The step-validation helpers: `add_error`, `add_error_and_validate`.
|
|
289
|
+
- The reserved field names: `:call`, `:errors`, `:fields`, `:output`, `:perform`, `:response`.
|
|
290
|
+
- The exception hierarchy under `ServiceCore::Error`.
|
|
291
|
+
- `ServiceCore.logger` and `ServiceCore.configure`.
|
|
292
|
+
|
|
293
|
+
The internals of `ServiceCore::Output`, the `Responder` mixin shape, and anything not listed above are implementation details and may change between any release.
|
|
294
|
+
|
|
295
|
+
## Compatibility
|
|
296
|
+
|
|
297
|
+
- Ruby: 3.1 minimum; tested against 3.3 and 3.4 (and 4.0 against Rails 8.x).
|
|
298
|
+
- ActiveModel / ActiveSupport: `>= 6.1, < 9.0`; tested against Rails 7.2, 8.0, and 8.1 via [appraisal](https://github.com/thoughtbot/appraisal).
|
|
299
|
+
|
|
300
|
+
## Development
|
|
301
|
+
|
|
302
|
+
```sh
|
|
303
|
+
bin/setup
|
|
304
|
+
bundle exec rspec
|
|
305
|
+
bundle exec rubocop
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
To run the spec suite against every supported Rails version:
|
|
309
|
+
|
|
310
|
+
```sh
|
|
311
|
+
bundle exec appraisal install
|
|
312
|
+
bundle exec appraisal rspec
|
|
313
|
+
```
|
|
314
|
+
|
|
345
315
|
## Contributing
|
|
346
316
|
|
|
347
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/sehgalmayank001/service-core. This project
|
|
317
|
+
Bug reports and pull requests are welcome on GitHub at [github.com/sehgalmayank001/service-core](https://github.com/sehgalmayank001/service-core). This project follows the [Contributor Covenant code of conduct](https://github.com/sehgalmayank001/service-core/blob/main/CODE_OF_CONDUCT.md).
|
|
348
318
|
|
|
349
319
|
## License
|
|
350
320
|
|
|
351
321
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
352
|
-
|
|
353
|
-
## Code of Conduct
|
|
354
|
-
|
|
355
|
-
Everyone interacting in the ServiceCore project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/sehgalmayank001/service-core/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "activemodel", "~> 7.2.0"
|
|
7
|
+
gem "activesupport", "~> 7.2.0"
|
|
8
|
+
|
|
9
|
+
group :development, :test do
|
|
10
|
+
gem "appraisal", "~> 2.5"
|
|
11
|
+
gem "debug", ">= 1.9"
|
|
12
|
+
gem "rspec", "~> 3.13"
|
|
13
|
+
gem "rubocop", "~> 1.86"
|
|
14
|
+
gem "rubocop-rake", "~> 0.7"
|
|
15
|
+
gem "rubocop-rspec", "~> 3.0"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "activemodel", "~> 8.0.0"
|
|
7
|
+
gem "activesupport", "~> 8.0.0"
|
|
8
|
+
|
|
9
|
+
group :development, :test do
|
|
10
|
+
gem "appraisal", "~> 2.5"
|
|
11
|
+
gem "debug", ">= 1.9"
|
|
12
|
+
gem "rspec", "~> 3.13"
|
|
13
|
+
gem "rubocop", "~> 1.86"
|
|
14
|
+
gem "rubocop-rake", "~> 0.7"
|
|
15
|
+
gem "rubocop-rspec", "~> 3.0"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "activemodel", "~> 8.1.0"
|
|
7
|
+
gem "activesupport", "~> 8.1.0"
|
|
8
|
+
|
|
9
|
+
group :development, :test do
|
|
10
|
+
gem "appraisal", "~> 2.5"
|
|
11
|
+
gem "debug", ">= 1.9"
|
|
12
|
+
gem "rspec", "~> 3.13"
|
|
13
|
+
gem "rubocop", "~> 1.86"
|
|
14
|
+
gem "rubocop-rake", "~> 0.7"
|
|
15
|
+
gem "rubocop-rspec", "~> 3.0"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
gemspec path: "../"
|