composable-service 0.0.13 → 0.0.14
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 +4 -0
- data/README.md +123 -22
- data/lib/composable/service/command.rb +4 -8
- data/lib/composable/service/gem_version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b3c690febbfbbd5921084d97b28acd8b0c1a501f6f9a317d72f58309ccd9019f
|
|
4
|
+
data.tar.gz: 0242eaf874208b7f5c57818fc934c09a98fef18ec869a9f47f66a476487c42d9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b7a4752d7a9228d8177abab42e2d03157535e9e504f46683eb3007aa0764fe3267b0bb67780e3d919ef2289d33296909dbde4704ae4020bfbb21fae7e72caad3
|
|
7
|
+
data.tar.gz: bf76c2769e5e29f5ec310dc0eba0430bf82ea69a6fd98791f080115bd71247db522e002400f1c284338b1037b50e33791531d75f4e86727dd43109bfa6847939
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
- Preserve results across nested subclasses and mark aborted callbacks as failures.
|
|
4
|
+
- Document service lifecycle and transaction boundaries.
|
|
5
|
+
- Add behavioral tests with complete line/branch coverage.
|
|
6
|
+
|
|
3
7
|
## [0.0.1] - 2022-01-24
|
|
4
8
|
|
|
5
9
|
- Initial release
|
data/README.md
CHANGED
|
@@ -1,43 +1,144 @@
|
|
|
1
|
-
#
|
|
1
|
+
# composable-service
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Validated service objects with attributes, record composition, callbacks and an
|
|
4
|
+
explicit result. Requires Ruby 3.2+ and ActiveModel/ActiveSupport 7.2+.
|
|
5
|
+
Install `gem "composable-service"`; require `composable/service`.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
## Implement an operation
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
```ruby
|
|
10
|
+
require "composable/service"
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
class BuildGreeting < Composable::Service::Command
|
|
13
|
+
attribute :name, type: :string
|
|
14
|
+
validates :name, presence: true
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def save
|
|
19
|
+
"Hello, #{name}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
operation = BuildGreeting.call(name: "Example")
|
|
24
|
+
operation.success? # => true
|
|
25
|
+
operation.result # => "Hello, Example"
|
|
26
|
+
BuildGreeting.call(name: "").failure? # => true
|
|
13
27
|
```
|
|
14
28
|
|
|
15
|
-
|
|
29
|
+
Implement `save`, whose return value becomes `result`. Call `.call` on the class
|
|
30
|
+
or initialize an instance and call it. Validation runs before the save callback
|
|
31
|
+
chain and persistence. `.call!` raises `Composable::Core::Error` on failure.
|
|
32
|
+
Before execution, `success?` and `failure?` both return false.
|
|
16
33
|
|
|
17
|
-
|
|
34
|
+
Use `before_save`, `around_save`, and `after_save` for operation lifecycle hooks.
|
|
35
|
+
An aborted callback or a save returning `false` records failure; nil is a valid
|
|
36
|
+
result. `rescue_from` handles expected `StandardError` subclasses for `.call`;
|
|
37
|
+
unhandled errors and all exceptions from `.call!` propagate. Handlers should add
|
|
38
|
+
an appropriate error without including secrets.
|
|
18
39
|
|
|
19
|
-
|
|
40
|
+
## Compose records
|
|
20
41
|
|
|
21
|
-
|
|
42
|
+
`attribute`, `composable`, `sync`, typing, defaults and conditions come from
|
|
43
|
+
[core](../composable-core/README.md). Declare explicit mappings for allowed
|
|
44
|
+
writes. Record assignment initializes mapped attributes; explicit operation
|
|
45
|
+
attributes override them. Eligible records are saved before `save`, inside nested
|
|
46
|
+
record transactions. ActiveRecord validation errors become operation errors.
|
|
22
47
|
|
|
23
|
-
|
|
48
|
+
Exceptions roll back changes on a shared database connection. Adding errors alone
|
|
49
|
+
does not undo writes. External API calls and work across multiple databases are
|
|
50
|
+
not transactional; use application-level compensation or an outbox when needed.
|
|
51
|
+
Service objects do not authorize resources or manage job retries automatically.
|
|
24
52
|
|
|
25
|
-
|
|
53
|
+
Use a fresh operation for each execution. Read failures before consuming the
|
|
54
|
+
result. See [testing](../docs/testing.md) for running this gem's suite.
|
|
26
55
|
|
|
27
|
-
##
|
|
56
|
+
## Inspect failure and use the raising interface
|
|
28
57
|
|
|
29
|
-
|
|
58
|
+
```ruby
|
|
59
|
+
operation = BuildGreeting.new(name: "")
|
|
60
|
+
operation.success? # => false: it has not run yet
|
|
61
|
+
operation.failure? # => false
|
|
62
|
+
|
|
63
|
+
operation.call
|
|
64
|
+
operation.failure? # => true
|
|
65
|
+
operation.errors.full_messages # => ["Name can't be blank"]
|
|
66
|
+
operation.result # => nil
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
BuildGreeting.call!(name: "")
|
|
70
|
+
rescue Composable::Core::Error => error
|
|
71
|
+
# Present a suitable application error; do not assume a result is available.
|
|
72
|
+
end
|
|
73
|
+
```
|
|
30
74
|
|
|
31
|
-
|
|
75
|
+
## Add callbacks and handle expected exceptions
|
|
32
76
|
|
|
33
|
-
|
|
77
|
+
This example takes an application-provided delivery adapter. The adapter must
|
|
78
|
+
implement `deliver(message)` and return a receipt; it may raise `DeliveryUnavailable`.
|
|
34
79
|
|
|
35
|
-
|
|
80
|
+
```ruby
|
|
81
|
+
class DeliveryUnavailable < StandardError; end
|
|
82
|
+
|
|
83
|
+
class SendNotice < Composable::Service::Command
|
|
84
|
+
attribute :message, type: :string
|
|
85
|
+
attribute :delivery
|
|
86
|
+
validates :message, :delivery, presence: true
|
|
87
|
+
|
|
88
|
+
before_save do
|
|
89
|
+
self.message = message.strip
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
rescue_from DeliveryUnavailable do
|
|
93
|
+
errors.add(:base, "Delivery is temporarily unavailable")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def save
|
|
99
|
+
delivery.deliver(message)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# adapter is supplied by the application.
|
|
104
|
+
operation = SendNotice.call(message: " Example notice ", delivery: adapter)
|
|
105
|
+
if operation.success?
|
|
106
|
+
receipt = operation.result
|
|
107
|
+
else
|
|
108
|
+
messages = operation.errors.full_messages
|
|
109
|
+
end
|
|
110
|
+
```
|
|
36
111
|
|
|
37
|
-
|
|
112
|
+
Only the expected exception is translated. Other exceptions propagate, and
|
|
113
|
+
`SendNotice.call!` propagates `DeliveryUnavailable` without calling the handler.
|
|
114
|
+
The callback runs after validation; use setters when normalization must affect
|
|
115
|
+
validation itself.
|
|
38
116
|
|
|
39
|
-
|
|
117
|
+
## Update a composed record
|
|
40
118
|
|
|
41
|
-
|
|
119
|
+
Assume the application defines a `Project` model with a `name` column. The caller
|
|
120
|
+
must obtain an authorized project before invoking the service.
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
class RenameProject < Composable::Service::Command
|
|
124
|
+
attribute :name, type: :string
|
|
125
|
+
validates :name, presence: true
|
|
126
|
+
|
|
127
|
+
composable :project do
|
|
128
|
+
sync :name
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def save
|
|
134
|
+
project
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
operation = RenameProject.call(project: authorized_project, name: "New name")
|
|
139
|
+
operation.result # => the saved project on success
|
|
140
|
+
```
|
|
42
141
|
|
|
43
|
-
|
|
142
|
+
Implement `save` in subclasses, including subclasses of your application service
|
|
143
|
+
base. Overriding `call` can bypass the inherited command wrapper. Use a fresh
|
|
144
|
+
instance for each operation instead of calling the same instance repeatedly.
|
|
@@ -6,20 +6,16 @@ module Composable
|
|
|
6
6
|
include Core::AttributeDSL
|
|
7
7
|
include Core::ComposableDSL
|
|
8
8
|
include Core::Callbacks
|
|
9
|
-
|
|
10
|
-
class << self
|
|
11
|
-
def inherited(subclass)
|
|
12
|
-
super
|
|
13
|
-
subclass.prepend(Core::Command)
|
|
14
|
-
end
|
|
15
|
-
end
|
|
9
|
+
prepend Core::Command
|
|
16
10
|
|
|
17
11
|
def call
|
|
18
12
|
return unless valid?
|
|
19
13
|
|
|
20
|
-
run_callbacks :save do
|
|
14
|
+
completed = run_callbacks :save do
|
|
21
15
|
@result = save_composables { save }
|
|
22
16
|
end
|
|
17
|
+
errors.add(:base, :invalid) if completed == false
|
|
18
|
+
@result
|
|
23
19
|
end
|
|
24
20
|
|
|
25
21
|
private
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: composable-service
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.14
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jairo Vazquez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-10-07 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: composable-core
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.0.
|
|
18
|
+
version: 0.0.14
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.0.
|
|
25
|
+
version: 0.0.14
|
|
26
26
|
description: Service composable object to work with ruby services
|
|
27
27
|
email:
|
|
28
28
|
- jairovm20@gmail.com
|
|
@@ -51,7 +51,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
51
51
|
requirements:
|
|
52
52
|
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: 2.
|
|
54
|
+
version: 3.2.0
|
|
55
55
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
56
|
requirements:
|
|
57
57
|
- - ">="
|