servus 0.1.3 → 0.1.5
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/.claude/commands/check-docs.md +1 -0
- data/.claude/commands/consistency-check.md +1 -0
- data/.claude/commands/fine-tooth-comb.md +1 -0
- data/.claude/commands/red-green-refactor.md +5 -0
- data/.claude/settings.json +15 -0
- data/.rubocop.yml +18 -2
- data/.yardopts +6 -0
- data/CHANGELOG.md +47 -0
- data/CLAUDE.md +10 -0
- data/IDEAS.md +5 -0
- data/READme.md +300 -47
- data/Rakefile +33 -0
- data/builds/servus-0.1.3.gem +0 -0
- data/builds/servus-0.1.4.gem +0 -0
- data/builds/servus-0.1.5.gem +0 -0
- data/docs/core/1_overview.md +77 -0
- data/docs/core/2_architecture.md +120 -0
- data/docs/core/3_service_objects.md +121 -0
- data/docs/current_focus.md +569 -0
- data/docs/features/1_schema_validation.md +119 -0
- data/docs/features/2_error_handling.md +121 -0
- data/docs/features/3_async_execution.md +81 -0
- data/docs/features/4_logging.md +64 -0
- data/docs/features/5_event_bus.md +244 -0
- data/docs/guides/1_common_patterns.md +90 -0
- data/docs/guides/2_migration_guide.md +175 -0
- data/docs/integration/1_configuration.md +104 -0
- data/docs/integration/2_testing.md +287 -0
- data/docs/integration/3_rails_integration.md +99 -0
- data/docs/yard/Servus/Base.html +1645 -0
- data/docs/yard/Servus/Config.html +582 -0
- data/docs/yard/Servus/Extensions/Async/Call.html +400 -0
- data/docs/yard/Servus/Extensions/Async/Errors/AsyncError.html +140 -0
- data/docs/yard/Servus/Extensions/Async/Errors/JobEnqueueError.html +154 -0
- data/docs/yard/Servus/Extensions/Async/Errors/ServiceNotFoundError.html +154 -0
- data/docs/yard/Servus/Extensions/Async/Errors.html +128 -0
- data/docs/yard/Servus/Extensions/Async/Ext.html +119 -0
- data/docs/yard/Servus/Extensions/Async/Job.html +310 -0
- data/docs/yard/Servus/Extensions/Async.html +141 -0
- data/docs/yard/Servus/Extensions.html +117 -0
- data/docs/yard/Servus/Generators/ServiceGenerator.html +261 -0
- data/docs/yard/Servus/Generators.html +115 -0
- data/docs/yard/Servus/Helpers/ControllerHelpers.html +457 -0
- data/docs/yard/Servus/Helpers.html +115 -0
- data/docs/yard/Servus/Railtie.html +134 -0
- data/docs/yard/Servus/Support/Errors/AuthenticationError.html +287 -0
- data/docs/yard/Servus/Support/Errors/BadRequestError.html +283 -0
- data/docs/yard/Servus/Support/Errors/ForbiddenError.html +284 -0
- data/docs/yard/Servus/Support/Errors/InternalServerError.html +283 -0
- data/docs/yard/Servus/Support/Errors/NotFoundError.html +284 -0
- data/docs/yard/Servus/Support/Errors/ServiceError.html +489 -0
- data/docs/yard/Servus/Support/Errors/ServiceUnavailableError.html +290 -0
- data/docs/yard/Servus/Support/Errors/UnauthorizedError.html +200 -0
- data/docs/yard/Servus/Support/Errors/UnprocessableEntityError.html +288 -0
- data/docs/yard/Servus/Support/Errors/ValidationError.html +200 -0
- data/docs/yard/Servus/Support/Errors.html +140 -0
- data/docs/yard/Servus/Support/Logger.html +856 -0
- data/docs/yard/Servus/Support/Rescuer/BlockContext.html +585 -0
- data/docs/yard/Servus/Support/Rescuer/CallOverride.html +257 -0
- data/docs/yard/Servus/Support/Rescuer/ClassMethods.html +343 -0
- data/docs/yard/Servus/Support/Rescuer.html +267 -0
- data/docs/yard/Servus/Support/Response.html +574 -0
- data/docs/yard/Servus/Support/Validator.html +1150 -0
- data/docs/yard/Servus/Support.html +119 -0
- data/docs/yard/Servus/Testing/ExampleBuilders.html +523 -0
- data/docs/yard/Servus/Testing/ExampleExtractor.html +578 -0
- data/docs/yard/Servus/Testing.html +142 -0
- data/docs/yard/Servus.html +343 -0
- data/docs/yard/_index.html +535 -0
- data/docs/yard/class_list.html +54 -0
- data/docs/yard/css/common.css +1 -0
- data/docs/yard/css/full_list.css +58 -0
- data/docs/yard/css/style.css +503 -0
- data/docs/yard/file.1_common_patterns.html +154 -0
- data/docs/yard/file.1_configuration.html +115 -0
- data/docs/yard/file.1_overview.html +142 -0
- data/docs/yard/file.1_schema_validation.html +188 -0
- data/docs/yard/file.2_architecture.html +157 -0
- data/docs/yard/file.2_error_handling.html +190 -0
- data/docs/yard/file.2_migration_guide.html +242 -0
- data/docs/yard/file.2_testing.html +227 -0
- data/docs/yard/file.3_async_execution.html +145 -0
- data/docs/yard/file.3_rails_integration.html +160 -0
- data/docs/yard/file.3_service_objects.html +191 -0
- data/docs/yard/file.4_logging.html +135 -0
- data/docs/yard/file.ErrorHandling.html +190 -0
- data/docs/yard/file.READme.html +674 -0
- data/docs/yard/file.architecture.html +157 -0
- data/docs/yard/file.async_execution.html +145 -0
- data/docs/yard/file.common_patterns.html +154 -0
- data/docs/yard/file.configuration.html +115 -0
- data/docs/yard/file.error_handling.html +190 -0
- data/docs/yard/file.logging.html +135 -0
- data/docs/yard/file.migration_guide.html +242 -0
- data/docs/yard/file.overview.html +142 -0
- data/docs/yard/file.rails_integration.html +160 -0
- data/docs/yard/file.schema_validation.html +188 -0
- data/docs/yard/file.service_objects.html +191 -0
- data/docs/yard/file.testing.html +227 -0
- data/docs/yard/file_list.html +119 -0
- data/docs/yard/frames.html +22 -0
- data/docs/yard/index.html +674 -0
- data/docs/yard/js/app.js +344 -0
- data/docs/yard/js/full_list.js +242 -0
- data/docs/yard/js/jquery.js +4 -0
- data/docs/yard/method_list.html +542 -0
- data/docs/yard/top-level-namespace.html +110 -0
- data/lib/generators/servus/event_handler/event_handler_generator.rb +59 -0
- data/lib/generators/servus/event_handler/templates/handler.rb.erb +86 -0
- data/lib/generators/servus/event_handler/templates/handler_spec.rb.erb +48 -0
- data/lib/generators/servus/service/service_generator.rb +68 -1
- data/lib/generators/servus/service/templates/arguments.json.erb +19 -10
- data/lib/generators/servus/service/templates/result.json.erb +8 -2
- data/lib/generators/servus/service/templates/service.rb.erb +102 -5
- data/lib/generators/servus/service/templates/service_spec.rb.erb +67 -6
- data/lib/servus/base.rb +275 -58
- data/lib/servus/config.rb +83 -17
- data/lib/servus/event_handler.rb +275 -0
- data/lib/servus/events/bus.rb +137 -0
- data/lib/servus/events/emitter.rb +162 -0
- data/lib/servus/events/errors.rb +10 -0
- data/lib/servus/extensions/async/call.rb +50 -18
- data/lib/servus/extensions/async/errors.rb +23 -3
- data/lib/servus/extensions/async/ext.rb +10 -2
- data/lib/servus/extensions/async/job.rb +30 -9
- data/lib/servus/helpers/controller_helpers.rb +73 -37
- data/lib/servus/railtie.rb +16 -0
- data/lib/servus/support/errors.rb +135 -45
- data/lib/servus/support/rescuer.rb +189 -36
- data/lib/servus/support/response.rb +49 -7
- data/lib/servus/support/validator.rb +147 -19
- data/lib/servus/testing/example_builders.rb +133 -0
- data/lib/servus/testing/example_extractor.rb +309 -0
- data/lib/servus/testing/matchers.rb +88 -0
- data/lib/servus/testing.rb +19 -0
- data/lib/servus/version.rb +1 -1
- data/lib/servus.rb +6 -0
- metadata +135 -19
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>
|
|
7
|
+
File: READme
|
|
8
|
+
|
|
9
|
+
— Servus | Service Object Framework
|
|
10
|
+
|
|
11
|
+
</title>
|
|
12
|
+
|
|
13
|
+
<link rel="stylesheet" href="css/style.css" type="text/css" />
|
|
14
|
+
|
|
15
|
+
<link rel="stylesheet" href="css/common.css" type="text/css" />
|
|
16
|
+
|
|
17
|
+
<script type="text/javascript">
|
|
18
|
+
pathId = "READme";
|
|
19
|
+
relpath = '';
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
<script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
|
|
24
|
+
|
|
25
|
+
<script type="text/javascript" charset="utf-8" src="js/app.js"></script>
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<div class="nav_wrap">
|
|
31
|
+
<iframe id="nav" src="class_list.html?1"></iframe>
|
|
32
|
+
<div id="resizer"></div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div id="main" tabindex="-1">
|
|
36
|
+
<div id="header">
|
|
37
|
+
<div id="menu">
|
|
38
|
+
|
|
39
|
+
<a href="_index.html">Index</a> »
|
|
40
|
+
<span class="title">File: READme</span>
|
|
41
|
+
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div id="search">
|
|
45
|
+
|
|
46
|
+
<a class="full_list_link" id="class_list_link"
|
|
47
|
+
href="class_list.html">
|
|
48
|
+
|
|
49
|
+
<svg width="24" height="24">
|
|
50
|
+
<rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
|
|
51
|
+
<rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
|
|
52
|
+
<rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
|
|
53
|
+
</svg>
|
|
54
|
+
</a>
|
|
55
|
+
|
|
56
|
+
</div>
|
|
57
|
+
<div class="clear"></div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div id="content"><div id='filecontents'><h2 id="servus-gem">Servus Gem</h2>
|
|
61
|
+
|
|
62
|
+
<p>Servus is a gem for creating and managing service objects. It includes:</p>
|
|
63
|
+
|
|
64
|
+
<ul>
|
|
65
|
+
<li>A base class for service objects</li>
|
|
66
|
+
<li>Generators for core service objects and specs</li>
|
|
67
|
+
<li>Support for schema validation</li>
|
|
68
|
+
<li>Support for error handling</li>
|
|
69
|
+
<li>Support for logging</li>
|
|
70
|
+
</ul>
|
|
71
|
+
|
|
72
|
+
<h2 id="generators">Generators</h2>
|
|
73
|
+
|
|
74
|
+
<p>Service objects can be easily created using the <code>rails g servus:service namespace/service_name [*params]</code> command. For sake of consistency, use this command when generating new service objects.</p>
|
|
75
|
+
|
|
76
|
+
<h3 id="generate-service">Generate Service</h3>
|
|
77
|
+
|
|
78
|
+
<pre class="code bash"><code class="bash">$ rails g servus:service namespace/do_something_helpful user
|
|
79
|
+
=> create app/services/namespace/do_something_helpful/service.rb
|
|
80
|
+
create spec/services/namespace/do_something_helpful/service_spec.rb
|
|
81
|
+
create app/schemas/services/namespace/do_something_helpful/result.json
|
|
82
|
+
create app/schemas/services/namespace/do_something_helpful/arguments.json
|
|
83
|
+
</code></pre>
|
|
84
|
+
|
|
85
|
+
<h3 id="destroy-service">Destroy Service</h3>
|
|
86
|
+
|
|
87
|
+
<pre class="code bash"><code class="bash">$ rails d servus:service namespace/do_something_helpful
|
|
88
|
+
=> remove app/services/namespace/do_something_helpful/service.rb
|
|
89
|
+
remove spec/services/namespace/do_something_helpful/service_spec.rb
|
|
90
|
+
remove app/schemas/services/namespace/do_something_helpful/result.json
|
|
91
|
+
remove app/schemas/services/namespace/do_something_helpful/arguments.json
|
|
92
|
+
</code></pre>
|
|
93
|
+
|
|
94
|
+
<h2 id="arguments">Arguments</h2>
|
|
95
|
+
|
|
96
|
+
<p>Service objects should use keyword arguments rather than positional arguments for improved clarity and more meaningful error messages.</p>
|
|
97
|
+
|
|
98
|
+
<pre class="code ruby"><code class="ruby"><span class='comment'># Good ✅
|
|
99
|
+
</span><span class='kw'>class</span> <span class='const'>Services</span><span class='op'>::</span><span class='const'>ProcessPayment</span><span class='op'>::</span><span class='const'>Service</span> <span class='op'><</span> <span class='const'><span class='object_link'><a href="Servus.html" title="Servus (module)">Servus</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Servus/Base.html" title="Servus::Base (class)">Base</a></span></span>
|
|
100
|
+
<span class='kw'>def</span> <span class='id identifier rubyid_initialize'>initialize</span><span class='lparen'>(</span><span class='label'>user:</span><span class='comma'>,</span> <span class='label'>amount:</span><span class='comma'>,</span> <span class='label'>payment_method:</span><span class='rparen'>)</span>
|
|
101
|
+
<span class='ivar'>@user</span> <span class='op'>=</span> <span class='id identifier rubyid_user'>user</span>
|
|
102
|
+
<span class='ivar'>@amount</span> <span class='op'>=</span> <span class='id identifier rubyid_amount'>amount</span>
|
|
103
|
+
<span class='ivar'>@payment_method</span> <span class='op'>=</span> <span class='id identifier rubyid_payment_method'>payment_method</span>
|
|
104
|
+
<span class='kw'>end</span>
|
|
105
|
+
<span class='kw'>end</span>
|
|
106
|
+
|
|
107
|
+
<span class='comment'># Bad ❌
|
|
108
|
+
</span><span class='kw'>class</span> <span class='const'>Services</span><span class='op'>::</span><span class='const'>ProcessPayment</span><span class='op'>::</span><span class='const'>Service</span> <span class='op'><</span> <span class='const'><span class='object_link'><a href="Servus.html" title="Servus (module)">Servus</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Servus/Base.html" title="Servus::Base (class)">Base</a></span></span>
|
|
109
|
+
<span class='kw'>def</span> <span class='id identifier rubyid_initialize'>initialize</span><span class='lparen'>(</span><span class='id identifier rubyid_user'>user</span><span class='comma'>,</span> <span class='id identifier rubyid_amount'>amount</span><span class='comma'>,</span> <span class='id identifier rubyid_payment_method'>payment_method</span><span class='rparen'>)</span>
|
|
110
|
+
<span class='ivar'>@user</span> <span class='op'>=</span> <span class='id identifier rubyid_user'>user</span>
|
|
111
|
+
<span class='ivar'>@amount</span> <span class='op'>=</span> <span class='id identifier rubyid_amount'>amount</span>
|
|
112
|
+
<span class='ivar'>@payment_method</span> <span class='op'>=</span> <span class='id identifier rubyid_payment_method'>payment_method</span>
|
|
113
|
+
<span class='kw'>end</span>
|
|
114
|
+
<span class='kw'>end</span>
|
|
115
|
+
</code></pre>
|
|
116
|
+
|
|
117
|
+
<h2 id="directory-structure">Directory Structure</h2>
|
|
118
|
+
|
|
119
|
+
<p>Each service belongs in its own namespace with this structure:</p>
|
|
120
|
+
|
|
121
|
+
<ul>
|
|
122
|
+
<li><code>app/services/service_name/service.rb</code> - Main class/entry point</li>
|
|
123
|
+
<li><code>app/services/service_name/support/</code> - Service-specific supporting classes</li>
|
|
124
|
+
</ul>
|
|
125
|
+
|
|
126
|
+
<p>Supporting classes should never be used outside their parent service.</p>
|
|
127
|
+
|
|
128
|
+
<pre class="code ruby"><code class="ruby">app/services/
|
|
129
|
+
├── process_payment/
|
|
130
|
+
│ ├── service.rb
|
|
131
|
+
│ └── support/
|
|
132
|
+
│ ├── payment_validator.rb
|
|
133
|
+
│ └── receipt_generator.rb
|
|
134
|
+
├── generate_report/
|
|
135
|
+
│ ├── service.rb
|
|
136
|
+
│ └── support/
|
|
137
|
+
│ ├── report_formatter.rb
|
|
138
|
+
│ └── data_collector.rb
|
|
139
|
+
</code></pre>
|
|
140
|
+
|
|
141
|
+
<h2 id="methods"><strong>Methods</strong></h2>
|
|
142
|
+
|
|
143
|
+
<p>Every service object must implement:</p>
|
|
144
|
+
|
|
145
|
+
<ul>
|
|
146
|
+
<li>An <code>initialize</code> method that sets instance variables</li>
|
|
147
|
+
<li>A parameter-less <code>call</code> instance method that executes the service logic</li>
|
|
148
|
+
</ul>
|
|
149
|
+
|
|
150
|
+
<pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'>Services</span><span class='op'>::</span><span class='const'>GenerateReport</span><span class='op'>::</span><span class='const'>Service</span> <span class='op'><</span> <span class='const'><span class='object_link'><a href="Servus.html" title="Servus (module)">Servus</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Servus/Base.html" title="Servus::Base (class)">Base</a></span></span>
|
|
151
|
+
<span class='kw'>def</span> <span class='id identifier rubyid_initialize'>initialize</span><span class='lparen'>(</span><span class='label'>user:</span><span class='comma'>,</span> <span class='label'>report_type:</span><span class='comma'>,</span> <span class='label'>date_range:</span><span class='rparen'>)</span>
|
|
152
|
+
<span class='ivar'>@user</span> <span class='op'>=</span> <span class='id identifier rubyid_user'>user</span>
|
|
153
|
+
<span class='ivar'>@report_type</span> <span class='op'>=</span> <span class='id identifier rubyid_report_type'>report_type</span>
|
|
154
|
+
<span class='ivar'>@date_range</span> <span class='op'>=</span> <span class='id identifier rubyid_date_range'>date_range</span>
|
|
155
|
+
<span class='kw'>end</span>
|
|
156
|
+
|
|
157
|
+
<span class='kw'>def</span> <span class='id identifier rubyid_call'>call</span>
|
|
158
|
+
<span class='id identifier rubyid_data'>data</span> <span class='op'>=</span> <span class='id identifier rubyid_collect_data'>collect_data</span>
|
|
159
|
+
<span class='kw'>if</span> <span class='id identifier rubyid_data'>data</span><span class='period'>.</span><span class='id identifier rubyid_empty?'>empty?</span>
|
|
160
|
+
<span class='kw'>return</span> <span class='id identifier rubyid_failure'>failure</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>No data available for the selected date range</span><span class='tstring_end'>"</span></span><span class='rparen'>)</span>
|
|
161
|
+
<span class='kw'>end</span>
|
|
162
|
+
|
|
163
|
+
<span class='id identifier rubyid_formatted_report'>formatted_report</span> <span class='op'>=</span> <span class='id identifier rubyid_format_report'>format_report</span><span class='lparen'>(</span><span class='id identifier rubyid_data'>data</span><span class='rparen'>)</span>
|
|
164
|
+
<span class='id identifier rubyid_success'>success</span><span class='lparen'>(</span><span class='id identifier rubyid_formatted_report'>formatted_report</span><span class='rparen'>)</span>
|
|
165
|
+
<span class='kw'>end</span>
|
|
166
|
+
|
|
167
|
+
<span class='id identifier rubyid_private'>private</span>
|
|
168
|
+
|
|
169
|
+
<span class='kw'>def</span> <span class='id identifier rubyid_collect_data'>collect_data</span>
|
|
170
|
+
<span class='comment'># Implementation details...
|
|
171
|
+
</span> <span class='kw'>end</span>
|
|
172
|
+
|
|
173
|
+
<span class='kw'>def</span> <span class='id identifier rubyid_format_report'>format_report</span><span class='lparen'>(</span><span class='id identifier rubyid_data'>data</span><span class='rparen'>)</span>
|
|
174
|
+
<span class='comment'># Implementation details...
|
|
175
|
+
</span> <span class='kw'>end</span>
|
|
176
|
+
<span class='kw'>end</span>
|
|
177
|
+
|
|
178
|
+
</code></pre>
|
|
179
|
+
|
|
180
|
+
<p>Here’s a section you can add to your README for the new <code>.call_async</code> feature, matching the style of your existing <code>## Inheritance</code> section:</p>
|
|
181
|
+
|
|
182
|
+
<hr>
|
|
183
|
+
|
|
184
|
+
<h2 id="asynchronous-execution"><strong>Asynchronous Execution</strong></h2>
|
|
185
|
+
|
|
186
|
+
<p>You can asynchronously execute any service class that inherits from <code>Servus::Base</code> using <code>.call_async</code>. This uses <code>ActiveJob</code> under the hood and supports standard job options (<code>wait</code>, <code>queue</code>, <code>priority</code>, etc.). Only available in environments where <code>ActiveJob</code> is loaded (e.g., Rails apps)</p>
|
|
187
|
+
|
|
188
|
+
<pre class="code ruby"><code class="ruby"><span class='comment'># Good ✅
|
|
189
|
+
</span><span class='const'>Services</span><span class='op'>::</span><span class='const'>NotifyUser</span><span class='op'>::</span><span class='const'>Service</span><span class='period'>.</span><span class='id identifier rubyid_call_async'>call_async</span><span class='lparen'>(</span>
|
|
190
|
+
<span class='label'>user_id:</span> <span class='id identifier rubyid_current_user'>current_user</span><span class='period'>.</span><span class='id identifier rubyid_id'>id</span><span class='comma'>,</span>
|
|
191
|
+
<span class='label'>wait:</span> <span class='int'>5</span><span class='period'>.</span><span class='id identifier rubyid_minutes'>minutes</span><span class='comma'>,</span>
|
|
192
|
+
<span class='label'>queue:</span> <span class='symbol'>:low_priority</span><span class='comma'>,</span>
|
|
193
|
+
<span class='label'>job_options:</span> <span class='lbrace'>{</span> <span class='label'>tags:</span> <span class='lbracket'>[</span><span class='tstring'><span class='tstring_beg'>'</span><span class='tstring_content'>notifications</span><span class='tstring_end'>'</span></span><span class='rbracket'>]</span> <span class='rbrace'>}</span>
|
|
194
|
+
<span class='rparen'>)</span>
|
|
195
|
+
|
|
196
|
+
<span class='comment'># Bad ❌
|
|
197
|
+
</span><span class='const'>Services</span><span class='op'>::</span><span class='const'>NotifyUser</span><span class='op'>::</span><span class='const'>Support</span><span class='op'>::</span><span class='const'>MessageBuilder</span><span class='period'>.</span><span class='id identifier rubyid_call_async'>call_async</span><span class='lparen'>(</span>
|
|
198
|
+
<span class='comment'># Invalid: support classes don't inherit from Servus::Base
|
|
199
|
+
</span><span class='rparen'>)</span>
|
|
200
|
+
</code></pre>
|
|
201
|
+
|
|
202
|
+
<h2 id="inheritance"><strong>Inheritance</strong></h2>
|
|
203
|
+
|
|
204
|
+
<ul>
|
|
205
|
+
<li>Every main service class (<code>service.rb</code>) must inherit from <code>Servus::Base</code></li>
|
|
206
|
+
<li>Supporting classes should NOT inherit from <code>Servus::Base</code></li>
|
|
207
|
+
</ul>
|
|
208
|
+
|
|
209
|
+
<pre class="code ruby"><code class="ruby"><span class='comment'># Good ✅
|
|
210
|
+
</span><span class='kw'>class</span> <span class='const'>Services</span><span class='op'>::</span><span class='const'>NotifyUser</span><span class='op'>::</span><span class='const'>Service</span> <span class='op'><</span> <span class='const'><span class='object_link'><a href="Servus.html" title="Servus (module)">Servus</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Servus/Base.html" title="Servus::Base (class)">Base</a></span></span>
|
|
211
|
+
<span class='comment'># Service implementation
|
|
212
|
+
</span><span class='kw'>end</span>
|
|
213
|
+
|
|
214
|
+
<span class='kw'>class</span> <span class='const'>Services</span><span class='op'>::</span><span class='const'>NotifyUser</span><span class='op'>::</span><span class='const'>Support</span><span class='op'>::</span><span class='const'>MessageBuilder</span>
|
|
215
|
+
<span class='comment'># Support class implementation (does NOT inherit from BaseService)
|
|
216
|
+
</span><span class='kw'>end</span>
|
|
217
|
+
|
|
218
|
+
<span class='comment'># Bad ❌
|
|
219
|
+
</span><span class='kw'>class</span> <span class='const'>Services</span><span class='op'>::</span><span class='const'>NotifyUser</span><span class='op'>::</span><span class='const'>Support</span><span class='op'>::</span><span class='const'>MessageBuilder</span> <span class='op'><</span> <span class='const'><span class='object_link'><a href="Servus.html" title="Servus (module)">Servus</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Servus/Base.html" title="Servus::Base (class)">Base</a></span></span>
|
|
220
|
+
<span class='comment'># Incorrect: support classes should not inherit from Base class
|
|
221
|
+
</span><span class='kw'>end</span>
|
|
222
|
+
</code></pre>
|
|
223
|
+
|
|
224
|
+
<h2 id="call-chain"><strong>Call Chain</strong></h2>
|
|
225
|
+
|
|
226
|
+
<p>Always use the class method <code>call</code> instead of manual instantiation. The <code>call</code> method:</p>
|
|
227
|
+
|
|
228
|
+
<ol>
|
|
229
|
+
<li>Initializes an instance of the service using provided keyword arguments</li>
|
|
230
|
+
<li>Calls the instance-level <code>call</code> method</li>
|
|
231
|
+
<li>Handles schema validation of inputs and outputs</li>
|
|
232
|
+
<li>Handles logging of inputs and results</li>
|
|
233
|
+
<li>Automatically benchmarks execution time for performance monitoring</li>
|
|
234
|
+
</ol>
|
|
235
|
+
|
|
236
|
+
<pre class="code ruby"><code class="ruby"><span class='comment'># Good ✅
|
|
237
|
+
</span><span class='id identifier rubyid_result'>result</span> <span class='op'>=</span> <span class='const'>Services</span><span class='op'>::</span><span class='const'>ProcessPayment</span><span class='op'>::</span><span class='const'>Service</span><span class='period'>.</span><span class='id identifier rubyid_call'>call</span><span class='lparen'>(</span>
|
|
238
|
+
<span class='label'>amount:</span> <span class='int'>50</span><span class='comma'>,</span>
|
|
239
|
+
<span class='label'>user_id:</span> <span class='int'>123</span><span class='comma'>,</span>
|
|
240
|
+
<span class='label'>payment_method:</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>credit_card</span><span class='tstring_end'>"</span></span>
|
|
241
|
+
<span class='rparen'>)</span>
|
|
242
|
+
|
|
243
|
+
<span class='comment'># Bad ❌ - bypasses logging and other class-level functionality
|
|
244
|
+
</span><span class='id identifier rubyid_service'>service</span> <span class='op'>=</span> <span class='const'>Services</span><span class='op'>::</span><span class='const'>ProcessPayment</span><span class='op'>::</span><span class='const'>Service</span><span class='period'>.</span><span class='id identifier rubyid_new'>new</span><span class='lparen'>(</span>
|
|
245
|
+
<span class='label'>amount:</span> <span class='int'>50</span><span class='comma'>,</span>
|
|
246
|
+
<span class='label'>user_id:</span> <span class='int'>123</span><span class='comma'>,</span>
|
|
247
|
+
<span class='label'>payment_method:</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>credit_card</span><span class='tstring_end'>"</span></span>
|
|
248
|
+
<span class='rparen'>)</span>
|
|
249
|
+
<span class='id identifier rubyid_result'>result</span> <span class='op'>=</span> <span class='id identifier rubyid_service'>service</span><span class='period'>.</span><span class='id identifier rubyid_call'>call</span>
|
|
250
|
+
|
|
251
|
+
</code></pre>
|
|
252
|
+
|
|
253
|
+
<p>When services call other services, always use the class-level <code>call</code> method:</p>
|
|
254
|
+
|
|
255
|
+
<pre class="code ruby"><code class="ruby"><span class='kw'>def</span> <span class='id identifier rubyid_process_order'>process_order</span>
|
|
256
|
+
<span class='comment'># Good ✅
|
|
257
|
+
</span> <span class='id identifier rubyid_payment_result'>payment_result</span> <span class='op'>=</span> <span class='const'>Services</span><span class='op'>::</span><span class='const'>ProcessPayment</span><span class='op'>::</span><span class='const'>Service</span><span class='period'>.</span><span class='id identifier rubyid_call'>call</span><span class='lparen'>(</span>
|
|
258
|
+
<span class='label'>amount:</span> <span class='ivar'>@order</span><span class='period'>.</span><span class='id identifier rubyid_total'>total</span><span class='comma'>,</span>
|
|
259
|
+
<span class='label'>payment_method:</span> <span class='ivar'>@payment_details</span>
|
|
260
|
+
<span class='rparen'>)</span>
|
|
261
|
+
|
|
262
|
+
<span class='comment'># Bad ❌
|
|
263
|
+
</span> <span class='id identifier rubyid_payment_service'>payment_service</span> <span class='op'>=</span> <span class='const'>Services</span><span class='op'>::</span><span class='const'>ProcessPayment</span><span class='op'>::</span><span class='const'>Service</span><span class='period'>.</span><span class='id identifier rubyid_new'>new</span><span class='lparen'>(</span>
|
|
264
|
+
<span class='label'>amount:</span> <span class='ivar'>@order</span><span class='period'>.</span><span class='id identifier rubyid_total'>total</span><span class='comma'>,</span>
|
|
265
|
+
<span class='label'>payment_method:</span> <span class='ivar'>@payment_details</span>
|
|
266
|
+
<span class='rparen'>)</span>
|
|
267
|
+
<span class='id identifier rubyid_payment_result'>payment_result</span> <span class='op'>=</span> <span class='id identifier rubyid_payment_service'>payment_service</span><span class='period'>.</span><span class='id identifier rubyid_call'>call</span>
|
|
268
|
+
<span class='kw'>end</span>
|
|
269
|
+
|
|
270
|
+
</code></pre>
|
|
271
|
+
|
|
272
|
+
<h2 id="responses"><strong>Responses</strong></h2>
|
|
273
|
+
|
|
274
|
+
<p>The <code>Servus::Base</code> provides standardized response methods:</p>
|
|
275
|
+
|
|
276
|
+
<ul>
|
|
277
|
+
<li><code>success(data)</code> - Returns success with data as a single argument</li>
|
|
278
|
+
<li><code>failure(message, **options)</code> - Logs error and returns failure response</li>
|
|
279
|
+
<li><code>error!(message)</code> - Logs error and raises exception</li>
|
|
280
|
+
</ul>
|
|
281
|
+
|
|
282
|
+
<pre class="code ruby"><code class="ruby"><span class='kw'>def</span> <span class='id identifier rubyid_call'>call</span>
|
|
283
|
+
<span class='comment'># Return failure with message
|
|
284
|
+
</span> <span class='kw'>return</span> <span class='id identifier rubyid_failure'>failure</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>Order is not in a pending state</span><span class='tstring_end'>"</span></span><span class='rparen'>)</span> <span class='kw'>unless</span> <span class='ivar'>@order</span><span class='period'>.</span><span class='id identifier rubyid_pending?'>pending?</span>
|
|
285
|
+
|
|
286
|
+
<span class='comment'># Do something important
|
|
287
|
+
</span>
|
|
288
|
+
<span class='comment'># Process and return success with single data object
|
|
289
|
+
</span> <span class='id identifier rubyid_success'>success</span><span class='lparen'>(</span><span class='lbrace'>{</span>
|
|
290
|
+
<span class='label'>order_id:</span> <span class='ivar'>@order</span><span class='period'>.</span><span class='id identifier rubyid_id'>id</span><span class='comma'>,</span>
|
|
291
|
+
<span class='label'>status:</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>processed</span><span class='tstring_end'>"</span></span><span class='comma'>,</span>
|
|
292
|
+
<span class='label'>timestamp:</span> <span class='const'>Time</span><span class='period'>.</span><span class='id identifier rubyid_now'>now</span>
|
|
293
|
+
<span class='rbrace'>}</span><span class='rparen'>)</span>
|
|
294
|
+
<span class='kw'>end</span>
|
|
295
|
+
</code></pre>
|
|
296
|
+
|
|
297
|
+
<p>All responses are <code>Servus::Support::Response</code> objects with a <code>success?</code> boolean attribute and either <code>data</code> (for success) or <code>error</code> (for error) attributes.</p>
|
|
298
|
+
|
|
299
|
+
<h3 id="service-error-returns-and-handling">Service Error Returns and Handling</h3>
|
|
300
|
+
|
|
301
|
+
<p>By default, the <code>failure(...)</code> method creates an instance of <code>ServiceError</code> and adds it to the response type's <code>error</code> attribute. Standard and custom error types should inherit from the <code>ServiceError</code> class and optionally implement a custom <code>api_error</code> method. This enables developers to choose between using an API-specific error or generic error message in the calling context.</p>
|
|
302
|
+
|
|
303
|
+
<pre class="code ruby"><code class="ruby"><span class='comment'># Called from within a Service Object
|
|
304
|
+
</span><span class='kw'>class</span> <span class='const'>SomeServiceObject</span><span class='op'>::</span><span class='const'>Service</span> <span class='op'><</span> <span class='const'><span class='object_link'><a href="Servus.html" title="Servus (module)">Servus</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Servus/Base.html" title="Servus::Base (class)">Base</a></span></span>
|
|
305
|
+
<span class='kw'>def</span> <span class='id identifier rubyid_call'>call</span>
|
|
306
|
+
<span class='comment'># Return default ServiceError with custom message
|
|
307
|
+
</span> <span class='id identifier rubyid_failure'>failure</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>That didn't work for some reason</span><span class='tstring_end'>"</span></span><span class='rparen'>)</span>
|
|
308
|
+
<span class='comment'>#=> Response(false, nil, Servus::Support::Errors::ServiceError("That didn't work for some reason"))
|
|
309
|
+
</span> <span class='comment'>#
|
|
310
|
+
</span> <span class='comment'># OR
|
|
311
|
+
</span> <span class='comment'>#
|
|
312
|
+
</span> <span class='comment'># Specify ServiceError type with custom message
|
|
313
|
+
</span> <span class='id identifier rubyid_failure'>failure</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>Custom message</span><span class='tstring_end'>"</span></span><span class='comma'>,</span> <span class='label'>type:</span> <span class='const'><span class='object_link'><a href="Servus.html" title="Servus (module)">Servus</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Servus/Support.html" title="Servus::Support (module)">Support</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Servus/Support/Errors.html" title="Servus::Support::Errors (module)">Errors</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Servus/Support/Errors/NotFoundError.html" title="Servus::Support::Errors::NotFoundError (class)">NotFoundError</a></span></span><span class='rparen'>)</span>
|
|
314
|
+
<span class='comment'>#=> Response(false, nil, Servus::Support::Errors::NotFoundError("Custom message"))
|
|
315
|
+
</span> <span class='comment'>#
|
|
316
|
+
</span> <span class='comment'># OR
|
|
317
|
+
</span> <span class='comment'>#
|
|
318
|
+
</span> <span class='comment'># Specify ServiceError type with default message
|
|
319
|
+
</span> <span class='id identifier rubyid_failure'>failure</span><span class='lparen'>(</span><span class='label'>type:</span> <span class='const'><span class='object_link'><a href="Servus.html" title="Servus (module)">Servus</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Servus/Support.html" title="Servus::Support (module)">Support</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Servus/Support/Errors.html" title="Servus::Support::Errors (module)">Errors</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Servus/Support/Errors/NotFoundError.html" title="Servus::Support::Errors::NotFoundError (class)">NotFoundError</a></span></span><span class='rparen'>)</span>
|
|
320
|
+
<span class='comment'>#=> Response(false, nil, Servus::Support::Errors::NotFoundError("Not found"))
|
|
321
|
+
</span> <span class='comment'>#
|
|
322
|
+
</span> <span class='comment'># OR
|
|
323
|
+
</span> <span class='comment'>#
|
|
324
|
+
</span> <span class='comment'># Accept all defaults
|
|
325
|
+
</span> <span class='id identifier rubyid_failure'>failure</span>
|
|
326
|
+
<span class='comment'>#=> Response(false, nil, Servus::Support::Errors::ServiceError("An error occurred"))
|
|
327
|
+
</span> <span class='kw'>end</span>
|
|
328
|
+
<span class='kw'>end</span>
|
|
329
|
+
|
|
330
|
+
<span class='comment'># Error handling in parent context
|
|
331
|
+
</span><span class='kw'>class</span> <span class='const'>SomeController</span> <span class='op'><</span> <span class='const'>AppController</span>
|
|
332
|
+
<span class='kw'>def</span> <span class='id identifier rubyid_controller_action'>controller_action</span>
|
|
333
|
+
<span class='id identifier rubyid_result'>result</span> <span class='op'>=</span> <span class='const'>SomeServiceObject</span><span class='op'>::</span><span class='const'>Service</span><span class='period'>.</span><span class='id identifier rubyid_call'>call</span><span class='lparen'>(</span><span class='label'>arg:</span> <span class='int'>1</span><span class='rparen'>)</span>
|
|
334
|
+
|
|
335
|
+
<span class='kw'>return</span> <span class='kw'>if</span> <span class='id identifier rubyid_result'>result</span><span class='period'>.</span><span class='id identifier rubyid_success?'>success?</span>
|
|
336
|
+
|
|
337
|
+
<span class='comment'># If you just want the error message
|
|
338
|
+
</span> <span class='id identifier rubyid_bad_request'>bad_request</span><span class='lparen'>(</span><span class='id identifier rubyid_result'>result</span><span class='period'>.</span><span class='id identifier rubyid_error'>error</span><span class='period'>.</span><span class='id identifier rubyid_message'>message</span><span class='rparen'>)</span>
|
|
339
|
+
|
|
340
|
+
<span class='comment'># If you want the API error
|
|
341
|
+
</span> <span class='id identifier rubyid_service_object_error'>service_object_error</span><span class='lparen'>(</span><span class='id identifier rubyid_result'>result</span><span class='period'>.</span><span class='id identifier rubyid_error'>error</span><span class='period'>.</span><span class='id identifier rubyid_api_error'>api_error</span><span class='rparen'>)</span>
|
|
342
|
+
<span class='kw'>end</span>
|
|
343
|
+
<span class='kw'>end</span>
|
|
344
|
+
</code></pre>
|
|
345
|
+
|
|
346
|
+
<h3 id="rescue_from-for-service-errors"><code>rescue_from</code> for service errors</h3>
|
|
347
|
+
|
|
348
|
+
<p>Services can configure default error handling using the <code>rescue_from</code> method.</p>
|
|
349
|
+
|
|
350
|
+
<pre class="code ruby"><code class="ruby">class SomeServiceObject::Service < Servus::Base
|
|
351
|
+
class SomethingBroke < StandardError; end
|
|
352
|
+
class SomethingGlitched < StandardError; end
|
|
353
|
+
|
|
354
|
+
# Rescue from standard errors and use custom error
|
|
355
|
+
rescue_from
|
|
356
|
+
SomethingBroke,
|
|
357
|
+
SomethingGlitched,
|
|
358
|
+
use: Servus::Support::Errors::ServiceUnavailableError # this is optional
|
|
359
|
+
|
|
360
|
+
def call
|
|
361
|
+
do_something
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
private
|
|
365
|
+
|
|
366
|
+
def do_something
|
|
367
|
+
make_and_api_call
|
|
368
|
+
rescue Net::HTTPError => e
|
|
369
|
+
raise SomethingGlitched, "Whoaaaa, something went wrong! #{e.message}"
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
</code></pre>
|
|
374
|
+
|
|
375
|
+
<pre class="code sh"><code class="sh">result = SomeServiceObject::Service.call
|
|
376
|
+
# Failure response
|
|
377
|
+
result.error.class
|
|
378
|
+
=> Servus::Support::Errors::ServiceUnavailableError
|
|
379
|
+
result.error.message
|
|
380
|
+
=> "[SomeServiceObject::Service::SomethingGlitched]: Whoaaaa, something went wrong! Net::HTTPError (503)"
|
|
381
|
+
result.error.api_error
|
|
382
|
+
=> { code: :service_unavailable, message: "[SomeServiceObject::Service::SomethingGlitched]: Whoaaaa, something went wrong! Net::HTTPError (503)" }
|
|
383
|
+
</code></pre>
|
|
384
|
+
|
|
385
|
+
<p>The <code>rescue_from</code> method will rescue from the specified errors and use the specified error type to create a failure response object with
|
|
386
|
+
the custom error. It helps eliminate the need to manually rescue many errors and create failure responses within the call method of
|
|
387
|
+
a service object.</p>
|
|
388
|
+
|
|
389
|
+
<p>You can also provide a block for custom error handling:</p>
|
|
390
|
+
|
|
391
|
+
<pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'>SomeServiceObject</span><span class='op'>::</span><span class='const'>Service</span> <span class='op'><</span> <span class='const'><span class='object_link'><a href="Servus.html" title="Servus (module)">Servus</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Servus/Base.html" title="Servus::Base (class)">Base</a></span></span>
|
|
392
|
+
<span class='comment'># Custom error handling with a block
|
|
393
|
+
</span> <span class='id identifier rubyid_rescue_from'>rescue_from</span> <span class='const'>ActiveRecord</span><span class='op'>::</span><span class='const'>RecordInvalid</span> <span class='kw'>do</span> <span class='op'>|</span><span class='id identifier rubyid_exception'>exception</span><span class='op'>|</span>
|
|
394
|
+
<span class='id identifier rubyid_failure'>failure</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>Validation failed: </span><span class='embexpr_beg'>#{</span><span class='id identifier rubyid_exception'>exception</span><span class='period'>.</span><span class='id identifier rubyid_message'>message</span><span class='embexpr_end'>}</span><span class='tstring_end'>"</span></span><span class='comma'>,</span> <span class='label'>type:</span> <span class='const'>ValidationError</span><span class='rparen'>)</span>
|
|
395
|
+
<span class='kw'>end</span>
|
|
396
|
+
|
|
397
|
+
<span class='id identifier rubyid_rescue_from'>rescue_from</span> <span class='const'>Net</span><span class='op'>::</span><span class='const'>HTTPError</span> <span class='kw'>do</span> <span class='op'>|</span><span class='id identifier rubyid_exception'>exception</span><span class='op'>|</span>
|
|
398
|
+
<span class='comment'># Can even return success to recover from errors
|
|
399
|
+
</span> <span class='id identifier rubyid_success'>success</span><span class='lparen'>(</span><span class='label'>recovered:</span> <span class='kw'>true</span><span class='comma'>,</span> <span class='label'>error_message:</span> <span class='id identifier rubyid_exception'>exception</span><span class='period'>.</span><span class='id identifier rubyid_message'>message</span><span class='rparen'>)</span>
|
|
400
|
+
<span class='kw'>end</span>
|
|
401
|
+
|
|
402
|
+
<span class='kw'>def</span> <span class='id identifier rubyid_call'>call</span>
|
|
403
|
+
<span class='comment'># Service logic
|
|
404
|
+
</span> <span class='kw'>end</span>
|
|
405
|
+
<span class='kw'>end</span>
|
|
406
|
+
</code></pre>
|
|
407
|
+
|
|
408
|
+
<p>The block receives the exception and has access to <code>success</code> and <code>failure</code> methods for creating the response.</p>
|
|
409
|
+
|
|
410
|
+
<h2 id="controller-helpers">Controller Helpers</h2>
|
|
411
|
+
|
|
412
|
+
<p>Service objects can be called from controllers using the <code>run_service</code> and <code>render_service_object_error</code> helpers.</p>
|
|
413
|
+
|
|
414
|
+
<h3 id="run_service">run_service</h3>
|
|
415
|
+
|
|
416
|
+
<p><code>run_service</code> calls the service object with the provided parameters and set's an instance variable <code>@result</code> to the
|
|
417
|
+
result of the service object if the result is successful. If the result is not successful, it will pass the result
|
|
418
|
+
to error to the <code>render_service_object_error</code> helper. This allows for easy error handling in the controller for
|
|
419
|
+
repetetive usecases.</p>
|
|
420
|
+
|
|
421
|
+
<pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'>SomeController</span> <span class='op'><</span> <span class='const'>AppController</span>
|
|
422
|
+
<span class='comment'># Before
|
|
423
|
+
</span> <span class='kw'>def</span> <span class='id identifier rubyid_controller_action'>controller_action</span>
|
|
424
|
+
<span class='id identifier rubyid_result'>result</span> <span class='op'>=</span> <span class='const'>Services</span><span class='op'>::</span><span class='const'>SomeServiceObject</span><span class='op'>::</span><span class='const'>Service</span><span class='period'>.</span><span class='id identifier rubyid_call'>call</span><span class='lparen'>(</span><span class='id identifier rubyid_my_params'>my_params</span><span class='rparen'>)</span>
|
|
425
|
+
<span class='kw'>return</span> <span class='kw'>if</span> <span class='id identifier rubyid_result'>result</span><span class='period'>.</span><span class='id identifier rubyid_success?'>success?</span>
|
|
426
|
+
<span class='id identifier rubyid_render_service_object_error'>render_service_object_error</span><span class='lparen'>(</span><span class='id identifier rubyid_result'>result</span><span class='period'>.</span><span class='id identifier rubyid_error'>error</span><span class='period'>.</span><span class='id identifier rubyid_api_error'>api_error</span><span class='rparen'>)</span>
|
|
427
|
+
<span class='kw'>end</span>
|
|
428
|
+
|
|
429
|
+
<span class='comment'># After
|
|
430
|
+
</span> <span class='kw'>def</span> <span class='id identifier rubyid_controller_action_refactored'>controller_action_refactored</span>
|
|
431
|
+
<span class='id identifier rubyid_run_service'>run_service</span> <span class='const'>Services</span><span class='op'>::</span><span class='const'>SomeServiceObject</span><span class='op'>::</span><span class='const'>Service</span><span class='comma'>,</span> <span class='id identifier rubyid_my_params'>my_params</span>
|
|
432
|
+
<span class='kw'>end</span>
|
|
433
|
+
<span class='kw'>end</span>
|
|
434
|
+
</code></pre>
|
|
435
|
+
|
|
436
|
+
<h3 id="render_service_object_error">render_service_object_error</h3>
|
|
437
|
+
|
|
438
|
+
<p><code>render_service_object_error</code> renders the error of a service object. It expects a hash with a <code>message</code> key and a <code>code</code> key from
|
|
439
|
+
the api_error method of the service error. This is all setup by default for a JSON API response, thought the method can be
|
|
440
|
+
overridden if needed to handle different usecases.</p>
|
|
441
|
+
|
|
442
|
+
<pre class="code ruby"><code class="ruby"><span class='comment'># Behind the scenes, render_service_object_error calls the following:
|
|
443
|
+
</span><span class='comment'>#
|
|
444
|
+
</span><span class='comment'># error = result.error.api_error
|
|
445
|
+
</span><span class='comment'># => { message: "Error message", code: 400 }
|
|
446
|
+
</span><span class='comment'>#
|
|
447
|
+
</span><span class='comment'># render json: { message: error[:message], code: error[:code] }, status: error[:code]
|
|
448
|
+
</span>
|
|
449
|
+
<span class='kw'>class</span> <span class='const'>SomeController</span> <span class='op'><</span> <span class='const'>AppController</span>
|
|
450
|
+
<span class='kw'>def</span> <span class='id identifier rubyid_controller_action'>controller_action</span>
|
|
451
|
+
<span class='id identifier rubyid_result'>result</span> <span class='op'>=</span> <span class='const'>Services</span><span class='op'>::</span><span class='const'>SomeServiceObject</span><span class='op'>::</span><span class='const'>Service</span><span class='period'>.</span><span class='id identifier rubyid_call'>call</span><span class='lparen'>(</span><span class='id identifier rubyid_my_params'>my_params</span><span class='rparen'>)</span>
|
|
452
|
+
<span class='kw'>return</span> <span class='kw'>if</span> <span class='id identifier rubyid_result'>result</span><span class='period'>.</span><span class='id identifier rubyid_success?'>success?</span>
|
|
453
|
+
|
|
454
|
+
<span class='id identifier rubyid_render_service_object_error'>render_service_object_error</span><span class='lparen'>(</span><span class='id identifier rubyid_result'>result</span><span class='period'>.</span><span class='id identifier rubyid_error'>error</span><span class='period'>.</span><span class='id identifier rubyid_api_error'>api_error</span><span class='rparen'>)</span>
|
|
455
|
+
<span class='kw'>end</span>
|
|
456
|
+
<span class='kw'>end</span>
|
|
457
|
+
</code></pre>
|
|
458
|
+
|
|
459
|
+
<h2 id="schema-validation"><strong>Schema Validation</strong></h2>
|
|
460
|
+
|
|
461
|
+
<p>Service objects support two methods for schema validation: JSON Schema files and inline schema declarations.</p>
|
|
462
|
+
|
|
463
|
+
<h3 id="1-file-based-schema-validation">1. File-based Schema Validation</h3>
|
|
464
|
+
|
|
465
|
+
<p>Every service can have corresponding schema files in the centralized schema directory:</p>
|
|
466
|
+
|
|
467
|
+
<ul>
|
|
468
|
+
<li><code>app/schemas/services/service_name/arguments.json</code> - Validates input arguments</li>
|
|
469
|
+
<li><code>app/schemas/services/service_name/result.json</code> - Validates success response data</li>
|
|
470
|
+
</ul>
|
|
471
|
+
|
|
472
|
+
<p>Example <code>arguments.json</code>:</p>
|
|
473
|
+
|
|
474
|
+
<pre class="code json"><code class="json">{
|
|
475
|
+
"type": "object",
|
|
476
|
+
"required": ["user_id", "amount", "payment_method"],
|
|
477
|
+
"properties": {
|
|
478
|
+
"user_id": { "type": "integer" },
|
|
479
|
+
"amount": {
|
|
480
|
+
"type": "integer",
|
|
481
|
+
"minimum": 1
|
|
482
|
+
},
|
|
483
|
+
"payment_method": {
|
|
484
|
+
"type": "string",
|
|
485
|
+
"enum": ["credit_card", "paypal", "bank_transfer"]
|
|
486
|
+
},
|
|
487
|
+
"currency": {
|
|
488
|
+
"type": "string",
|
|
489
|
+
"default": "USD"
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
"additionalProperties": false
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
</code></pre>
|
|
496
|
+
|
|
497
|
+
<p>Example <code>result.json</code>:</p>
|
|
498
|
+
|
|
499
|
+
<pre class="code json"><code class="json">{
|
|
500
|
+
"type": "object",
|
|
501
|
+
"required": ["transaction_id", "status"],
|
|
502
|
+
"properties": {
|
|
503
|
+
"transaction_id": { "type": "string" },
|
|
504
|
+
"status": {
|
|
505
|
+
"type": "string",
|
|
506
|
+
"enum": ["approved", "pending", "declined"]
|
|
507
|
+
},
|
|
508
|
+
"receipt_url": { "type": "string" }
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
</code></pre>
|
|
513
|
+
|
|
514
|
+
<h3 id="2-inline-schema-validation">2. Inline Schema Validation</h3>
|
|
515
|
+
|
|
516
|
+
<p>Schemas can be declared directly within the service class using the <code>schema</code> DSL method:</p>
|
|
517
|
+
|
|
518
|
+
<pre class="code ruby"><code class="ruby"><span class='kw'>class</span> <span class='const'>Services</span><span class='op'>::</span><span class='const'>ProcessPayment</span><span class='op'>::</span><span class='const'>Service</span> <span class='op'><</span> <span class='const'><span class='object_link'><a href="Servus.html" title="Servus (module)">Servus</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Servus/Base.html" title="Servus::Base (class)">Base</a></span></span>
|
|
519
|
+
<span class='id identifier rubyid_schema'>schema</span><span class='lparen'>(</span>
|
|
520
|
+
<span class='label'>arguments:</span> <span class='lbrace'>{</span>
|
|
521
|
+
<span class='label'>type:</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>object</span><span class='tstring_end'>"</span></span><span class='comma'>,</span>
|
|
522
|
+
<span class='label'>required:</span> <span class='lbracket'>[</span><span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>user_id</span><span class='tstring_end'>"</span></span><span class='comma'>,</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>amount</span><span class='tstring_end'>"</span></span><span class='comma'>,</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>payment_method</span><span class='tstring_end'>"</span></span><span class='rbracket'>]</span><span class='comma'>,</span>
|
|
523
|
+
<span class='label'>properties:</span> <span class='lbrace'>{</span>
|
|
524
|
+
<span class='label'>user_id:</span> <span class='lbrace'>{</span> <span class='label'>type:</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>integer</span><span class='tstring_end'>"</span></span> <span class='rbrace'>}</span><span class='comma'>,</span>
|
|
525
|
+
<span class='label'>amount:</span> <span class='lbrace'>{</span>
|
|
526
|
+
<span class='label'>type:</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>integer</span><span class='tstring_end'>"</span></span><span class='comma'>,</span>
|
|
527
|
+
<span class='label'>minimum:</span> <span class='int'>1</span>
|
|
528
|
+
<span class='rbrace'>}</span><span class='comma'>,</span>
|
|
529
|
+
<span class='label'>payment_method:</span> <span class='lbrace'>{</span>
|
|
530
|
+
<span class='label'>type:</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>string</span><span class='tstring_end'>"</span></span><span class='comma'>,</span>
|
|
531
|
+
<span class='label'>enum:</span> <span class='lbracket'>[</span><span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>credit_card</span><span class='tstring_end'>"</span></span><span class='comma'>,</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>paypal</span><span class='tstring_end'>"</span></span><span class='comma'>,</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>bank_transfer</span><span class='tstring_end'>"</span></span><span class='rbracket'>]</span>
|
|
532
|
+
<span class='rbrace'>}</span><span class='comma'>,</span>
|
|
533
|
+
<span class='label'>currency:</span> <span class='lbrace'>{</span>
|
|
534
|
+
<span class='label'>type:</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>string</span><span class='tstring_end'>"</span></span><span class='comma'>,</span>
|
|
535
|
+
<span class='label'>default:</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>USD</span><span class='tstring_end'>"</span></span>
|
|
536
|
+
<span class='rbrace'>}</span>
|
|
537
|
+
<span class='rbrace'>}</span><span class='comma'>,</span>
|
|
538
|
+
<span class='label'>additionalProperties:</span> <span class='kw'>false</span>
|
|
539
|
+
<span class='rbrace'>}</span><span class='comma'>,</span>
|
|
540
|
+
<span class='label'>result:</span> <span class='lbrace'>{</span>
|
|
541
|
+
<span class='label'>type:</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>object</span><span class='tstring_end'>"</span></span><span class='comma'>,</span>
|
|
542
|
+
<span class='label'>required:</span> <span class='lbracket'>[</span><span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>transaction_id</span><span class='tstring_end'>"</span></span><span class='comma'>,</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>status</span><span class='tstring_end'>"</span></span><span class='rbracket'>]</span><span class='comma'>,</span>
|
|
543
|
+
<span class='label'>properties:</span> <span class='lbrace'>{</span>
|
|
544
|
+
<span class='label'>transaction_id:</span> <span class='lbrace'>{</span> <span class='label'>type:</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>string</span><span class='tstring_end'>"</span></span> <span class='rbrace'>}</span><span class='comma'>,</span>
|
|
545
|
+
<span class='label'>status:</span> <span class='lbrace'>{</span>
|
|
546
|
+
<span class='label'>type:</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>string</span><span class='tstring_end'>"</span></span><span class='comma'>,</span>
|
|
547
|
+
<span class='label'>enum:</span> <span class='lbracket'>[</span><span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>approved</span><span class='tstring_end'>"</span></span><span class='comma'>,</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>pending</span><span class='tstring_end'>"</span></span><span class='comma'>,</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>declined</span><span class='tstring_end'>"</span></span><span class='rbracket'>]</span>
|
|
548
|
+
<span class='rbrace'>}</span><span class='comma'>,</span>
|
|
549
|
+
<span class='label'>receipt_url:</span> <span class='lbrace'>{</span> <span class='label'>type:</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>string</span><span class='tstring_end'>"</span></span> <span class='rbrace'>}</span>
|
|
550
|
+
<span class='rbrace'>}</span>
|
|
551
|
+
<span class='rbrace'>}</span>
|
|
552
|
+
<span class='rparen'>)</span>
|
|
553
|
+
|
|
554
|
+
<span class='kw'>def</span> <span class='id identifier rubyid_initialize'>initialize</span><span class='lparen'>(</span><span class='label'>user_id:</span><span class='comma'>,</span> <span class='label'>amount:</span><span class='comma'>,</span> <span class='label'>payment_method:</span><span class='comma'>,</span> <span class='label'>currency:</span> <span class='tstring'><span class='tstring_beg'>'</span><span class='tstring_content'>USD</span><span class='tstring_end'>'</span></span><span class='rparen'>)</span>
|
|
555
|
+
<span class='ivar'>@user_id</span> <span class='op'>=</span> <span class='id identifier rubyid_user_id'>user_id</span>
|
|
556
|
+
<span class='ivar'>@amount</span> <span class='op'>=</span> <span class='id identifier rubyid_amount'>amount</span>
|
|
557
|
+
<span class='ivar'>@payment_method</span> <span class='op'>=</span> <span class='id identifier rubyid_payment_method'>payment_method</span>
|
|
558
|
+
<span class='ivar'>@currency</span> <span class='op'>=</span> <span class='id identifier rubyid_currency'>currency</span>
|
|
559
|
+
<span class='kw'>end</span>
|
|
560
|
+
|
|
561
|
+
<span class='kw'>def</span> <span class='id identifier rubyid_call'>call</span>
|
|
562
|
+
<span class='comment'># Service logic...
|
|
563
|
+
</span> <span class='id identifier rubyid_success'>success</span><span class='lparen'>(</span><span class='lbrace'>{</span>
|
|
564
|
+
<span class='label'>transaction_id:</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>txn_1</span><span class='tstring_end'>"</span></span><span class='comma'>,</span>
|
|
565
|
+
<span class='label'>status:</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>approved</span><span class='tstring_end'>"</span></span>
|
|
566
|
+
<span class='rbrace'>}</span><span class='rparen'>)</span>
|
|
567
|
+
<span class='kw'>end</span>
|
|
568
|
+
<span class='kw'>end</span>
|
|
569
|
+
</code></pre>
|
|
570
|
+
|
|
571
|
+
<hr>
|
|
572
|
+
|
|
573
|
+
<p>These schemas use JSON Schema format to enforce type safety and input/output contracts. For detailed information on authoring JSON Schema files, refer to the official specification at: <a href="https://json-schema.org/specification.html">https://json-schema.org/specification.html</a></p>
|
|
574
|
+
|
|
575
|
+
<h3 id="schema-resolution">Schema Resolution</h3>
|
|
576
|
+
|
|
577
|
+
<p>The validation system follows this precedence:</p>
|
|
578
|
+
|
|
579
|
+
<ol>
|
|
580
|
+
<li>Schemas defined via <code>schema</code> DSL method (recommended)</li>
|
|
581
|
+
<li>Inline schema constants (<code>ARGUMENTS_SCHEMA</code> or <code>RESULT_SCHEMA</code>) - legacy support</li>
|
|
582
|
+
<li>JSON files in schema_root directory - legacy support</li>
|
|
583
|
+
<li>Returns nil if no schema is found (validation is opt-in)</li>
|
|
584
|
+
</ol>
|
|
585
|
+
|
|
586
|
+
<h3 id="schema-caching">Schema Caching</h3>
|
|
587
|
+
|
|
588
|
+
<p>Both file-based and inline schemas are automatically cached:</p>
|
|
589
|
+
|
|
590
|
+
<ul>
|
|
591
|
+
<li>First validation request loads and caches the schema</li>
|
|
592
|
+
<li>Subsequent validations use the cached version</li>
|
|
593
|
+
<li>Cache can be cleared using <code>Servus::Support::Validator.clear_cache!</code></li>
|
|
594
|
+
</ul>
|
|
595
|
+
|
|
596
|
+
<h2 id="logging"><strong>Logging</strong></h2>
|
|
597
|
+
|
|
598
|
+
<p>Servus automatically logs service execution details, making it easy to track and debug service calls.</p>
|
|
599
|
+
|
|
600
|
+
<h3 id="automatic-logging">Automatic Logging</h3>
|
|
601
|
+
|
|
602
|
+
<p>Every service call automatically logs:</p>
|
|
603
|
+
|
|
604
|
+
<ul>
|
|
605
|
+
<li><strong>Service invocation</strong> with input arguments</li>
|
|
606
|
+
<li><strong>Success results</strong> with execution duration</li>
|
|
607
|
+
<li><strong>Failure results</strong> with error details and duration</li>
|
|
608
|
+
<li><strong>Validation errors</strong> for schema violations</li>
|
|
609
|
+
<li><strong>Uncaught exceptions</strong> with error messages</li>
|
|
610
|
+
</ul>
|
|
611
|
+
|
|
612
|
+
<h3 id="logger-configuration">Logger Configuration</h3>
|
|
613
|
+
|
|
614
|
+
<p>The logger automatically adapts to your environment:</p>
|
|
615
|
+
|
|
616
|
+
<ul>
|
|
617
|
+
<li><strong>Rails applications</strong>: Uses <code>Rails.logger</code></li>
|
|
618
|
+
<li><strong>Non-Rails applications</strong>: Uses stdout logger</li>
|
|
619
|
+
</ul>
|
|
620
|
+
|
|
621
|
+
<h3 id="log-output-examples">Log Output Examples</h3>
|
|
622
|
+
|
|
623
|
+
<pre class="code ruby"><code class="ruby"># Success
|
|
624
|
+
INFO -- : Calling Services::ProcessPayment::Service with args: {:user_id=>123, :amount=>50}
|
|
625
|
+
INFO -- : Services::ProcessPayment::Service succeeded in 0.245s
|
|
626
|
+
|
|
627
|
+
# Failure
|
|
628
|
+
INFO -- : Calling Services::ProcessPayment::Service with args: {:user_id=>123, :amount=>50}
|
|
629
|
+
WARN -- : Services::ProcessPayment::Service failed in 0.156s with error: Insufficient funds
|
|
630
|
+
|
|
631
|
+
# Validation Error
|
|
632
|
+
ERROR -- : Services::ProcessPayment::Service validation error: The property '#/amount' value -10 was less than minimum value 1
|
|
633
|
+
|
|
634
|
+
# Exception
|
|
635
|
+
ERROR -- : Services::ProcessPayment::Service uncaught exception: NoMethodError - undefined method 'charge' for nil:NilClass
|
|
636
|
+
</code></pre>
|
|
637
|
+
|
|
638
|
+
<p>All logging happens transparently when using the class-level <code>.call</code> method. This is one of the reasons why direct instantiation (bypassing <code>.call</code>) is discouraged.</p>
|
|
639
|
+
|
|
640
|
+
<h2 id="configuration"><strong>Configuration</strong></h2>
|
|
641
|
+
|
|
642
|
+
<p>Servus can be configured to customize behavior for your application needs.</p>
|
|
643
|
+
|
|
644
|
+
<h3 id="schema-root-directory">Schema Root Directory</h3>
|
|
645
|
+
|
|
646
|
+
<p>By default, Servus looks for schema files in <code>app/schemas/services/</code>. You can customize this location:</p>
|
|
647
|
+
|
|
648
|
+
<pre class="code ruby"><code class="ruby"><span class='comment'># config/initializers/servus.rb
|
|
649
|
+
</span><span class='const'><span class='object_link'><a href="Servus.html" title="Servus (module)">Servus</a></span></span><span class='period'>.</span><span class='id identifier rubyid_configure'><span class='object_link'><a href="Servus.html#configure-class_method" title="Servus.configure (method)">configure</a></span></span> <span class='kw'>do</span> <span class='op'>|</span><span class='id identifier rubyid_config'>config</span><span class='op'>|</span>
|
|
650
|
+
<span class='id identifier rubyid_config'>config</span><span class='period'>.</span><span class='id identifier rubyid_schema_root'>schema_root</span> <span class='op'>=</span> <span class='const'>Rails</span><span class='period'>.</span><span class='id identifier rubyid_root'>root</span><span class='period'>.</span><span class='id identifier rubyid_join'>join</span><span class='lparen'>(</span><span class='tstring'><span class='tstring_beg'>'</span><span class='tstring_content'>lib/schemas</span><span class='tstring_end'>'</span></span><span class='rparen'>)</span>
|
|
651
|
+
<span class='kw'>end</span>
|
|
652
|
+
</code></pre>
|
|
653
|
+
|
|
654
|
+
<h3 id="default-behavior">Default Behavior</h3>
|
|
655
|
+
|
|
656
|
+
<p>Without explicit configuration:</p>
|
|
657
|
+
|
|
658
|
+
<ul>
|
|
659
|
+
<li><strong>Rails applications</strong>: Schema root defaults to <code>Rails.root/app/schemas/services</code></li>
|
|
660
|
+
<li><strong>Non-Rails applications</strong>: Schema root defaults to <code>./app/schemas/services</code> relative to the gem installation</li>
|
|
661
|
+
</ul>
|
|
662
|
+
|
|
663
|
+
<p>The configuration is accessed through the singleton <code>Servus.config</code> instance and can be modified using <code>Servus.configure</code>.</p>
|
|
664
|
+
</div></div>
|
|
665
|
+
|
|
666
|
+
<div id="footer">
|
|
667
|
+
Generated on Fri Nov 21 00:33:23 2025 by
|
|
668
|
+
<a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
|
669
|
+
0.9.37 (ruby-3.4.4).
|
|
670
|
+
</div>
|
|
671
|
+
|
|
672
|
+
</div>
|
|
673
|
+
</body>
|
|
674
|
+
</html>
|