servus 0.1.2 → 0.1.4

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.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -0
  3. data/CHANGELOG.md +8 -1
  4. data/IDEAS.md +5 -0
  5. data/READme.md +147 -42
  6. data/Rakefile +33 -0
  7. data/builds/servus-0.1.2.gem +0 -0
  8. data/builds/servus-0.1.3.gem +0 -0
  9. data/builds/servus-0.1.4.gem +0 -0
  10. data/docs/core/1_overview.md +77 -0
  11. data/docs/core/2_architecture.md +92 -0
  12. data/docs/core/3_service_objects.md +121 -0
  13. data/docs/features/1_schema_validation.md +119 -0
  14. data/docs/features/2_error_handling.md +121 -0
  15. data/docs/features/3_async_execution.md +81 -0
  16. data/docs/features/4_logging.md +64 -0
  17. data/docs/guides/1_common_patterns.md +90 -0
  18. data/docs/guides/2_migration_guide.md +175 -0
  19. data/docs/integration/1_configuration.md +51 -0
  20. data/docs/integration/2_testing.md +164 -0
  21. data/docs/integration/3_rails_integration.md +99 -0
  22. data/docs/yard/Servus/Base.html +1645 -0
  23. data/docs/yard/Servus/Config.html +582 -0
  24. data/docs/yard/Servus/Extensions/Async/Call.html +400 -0
  25. data/docs/yard/Servus/Extensions/Async/Errors/AsyncError.html +140 -0
  26. data/docs/yard/Servus/Extensions/Async/Errors/JobEnqueueError.html +154 -0
  27. data/docs/yard/Servus/Extensions/Async/Errors/ServiceNotFoundError.html +154 -0
  28. data/docs/yard/Servus/Extensions/Async/Errors.html +128 -0
  29. data/docs/yard/Servus/Extensions/Async/Ext.html +119 -0
  30. data/docs/yard/Servus/Extensions/Async/Job.html +310 -0
  31. data/docs/yard/Servus/Extensions/Async.html +141 -0
  32. data/docs/yard/Servus/Extensions.html +117 -0
  33. data/docs/yard/Servus/Generators/ServiceGenerator.html +261 -0
  34. data/docs/yard/Servus/Generators.html +115 -0
  35. data/docs/yard/Servus/Helpers/ControllerHelpers.html +457 -0
  36. data/docs/yard/Servus/Helpers.html +115 -0
  37. data/docs/yard/Servus/Railtie.html +134 -0
  38. data/docs/yard/Servus/Support/Errors/AuthenticationError.html +287 -0
  39. data/docs/yard/Servus/Support/Errors/BadRequestError.html +283 -0
  40. data/docs/yard/Servus/Support/Errors/ForbiddenError.html +284 -0
  41. data/docs/yard/Servus/Support/Errors/InternalServerError.html +283 -0
  42. data/docs/yard/Servus/Support/Errors/NotFoundError.html +284 -0
  43. data/docs/yard/Servus/Support/Errors/ServiceError.html +489 -0
  44. data/docs/yard/Servus/Support/Errors/ServiceUnavailableError.html +290 -0
  45. data/docs/yard/Servus/Support/Errors/UnauthorizedError.html +200 -0
  46. data/docs/yard/Servus/Support/Errors/UnprocessableEntityError.html +288 -0
  47. data/docs/yard/Servus/Support/Errors/ValidationError.html +200 -0
  48. data/docs/yard/Servus/Support/Errors.html +140 -0
  49. data/docs/yard/Servus/Support/Logger.html +856 -0
  50. data/docs/yard/Servus/Support/Rescuer/BlockContext.html +585 -0
  51. data/docs/yard/Servus/Support/Rescuer/CallOverride.html +257 -0
  52. data/docs/yard/Servus/Support/Rescuer/ClassMethods.html +343 -0
  53. data/docs/yard/Servus/Support/Rescuer.html +267 -0
  54. data/docs/yard/Servus/Support/Response.html +574 -0
  55. data/docs/yard/Servus/Support/Validator.html +1150 -0
  56. data/docs/yard/Servus/Support.html +119 -0
  57. data/docs/yard/Servus/Testing/ExampleBuilders.html +523 -0
  58. data/docs/yard/Servus/Testing/ExampleExtractor.html +578 -0
  59. data/docs/yard/Servus/Testing.html +142 -0
  60. data/docs/yard/Servus.html +343 -0
  61. data/docs/yard/_index.html +535 -0
  62. data/docs/yard/class_list.html +54 -0
  63. data/docs/yard/css/common.css +1 -0
  64. data/docs/yard/css/full_list.css +58 -0
  65. data/docs/yard/css/style.css +503 -0
  66. data/docs/yard/file.1_common_patterns.html +154 -0
  67. data/docs/yard/file.1_configuration.html +115 -0
  68. data/docs/yard/file.1_overview.html +142 -0
  69. data/docs/yard/file.1_schema_validation.html +188 -0
  70. data/docs/yard/file.2_architecture.html +157 -0
  71. data/docs/yard/file.2_error_handling.html +190 -0
  72. data/docs/yard/file.2_migration_guide.html +242 -0
  73. data/docs/yard/file.2_testing.html +227 -0
  74. data/docs/yard/file.3_async_execution.html +145 -0
  75. data/docs/yard/file.3_rails_integration.html +160 -0
  76. data/docs/yard/file.3_service_objects.html +191 -0
  77. data/docs/yard/file.4_logging.html +135 -0
  78. data/docs/yard/file.ErrorHandling.html +190 -0
  79. data/docs/yard/file.READme.html +674 -0
  80. data/docs/yard/file.architecture.html +157 -0
  81. data/docs/yard/file.async_execution.html +145 -0
  82. data/docs/yard/file.common_patterns.html +154 -0
  83. data/docs/yard/file.configuration.html +115 -0
  84. data/docs/yard/file.error_handling.html +190 -0
  85. data/docs/yard/file.logging.html +135 -0
  86. data/docs/yard/file.migration_guide.html +242 -0
  87. data/docs/yard/file.overview.html +142 -0
  88. data/docs/yard/file.rails_integration.html +160 -0
  89. data/docs/yard/file.schema_validation.html +188 -0
  90. data/docs/yard/file.service_objects.html +191 -0
  91. data/docs/yard/file.testing.html +227 -0
  92. data/docs/yard/file_list.html +119 -0
  93. data/docs/yard/frames.html +22 -0
  94. data/docs/yard/index.html +674 -0
  95. data/docs/yard/js/app.js +344 -0
  96. data/docs/yard/js/full_list.js +242 -0
  97. data/docs/yard/js/jquery.js +4 -0
  98. data/docs/yard/method_list.html +542 -0
  99. data/docs/yard/top-level-namespace.html +110 -0
  100. data/lib/generators/servus/service/service_generator.rb +64 -1
  101. data/lib/generators/servus/service/templates/service.rb.erb +1 -1
  102. data/lib/servus/base.rb +258 -57
  103. data/lib/servus/config.rb +58 -12
  104. data/lib/servus/extensions/async/call.rb +50 -18
  105. data/lib/servus/extensions/async/errors.rb +23 -3
  106. data/lib/servus/extensions/async/ext.rb +10 -2
  107. data/lib/servus/extensions/async/job.rb +32 -11
  108. data/lib/servus/helpers/controller_helpers.rb +73 -37
  109. data/lib/servus/support/errors.rb +135 -45
  110. data/lib/servus/support/rescuer.rb +189 -36
  111. data/lib/servus/support/response.rb +49 -7
  112. data/lib/servus/support/validator.rb +120 -19
  113. data/lib/servus/testing/example_builders.rb +133 -0
  114. data/lib/servus/testing/example_extractor.rb +309 -0
  115. data/lib/servus/testing.rb +17 -0
  116. data/lib/servus/version.rb +1 -1
  117. metadata +118 -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
+ &mdash; 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> &raquo;
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
+ =&gt; 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
+ =&gt; 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'>&lt;</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'>&lt;</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'>&lt;</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'>&quot;</span><span class='tstring_content'>No data available for the selected date range</span><span class='tstring_end'>&quot;</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'>&#39;</span><span class='tstring_content'>notifications</span><span class='tstring_end'>&#39;</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&#39;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'>&lt;</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'>&lt;</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'>&quot;</span><span class='tstring_content'>credit_card</span><span class='tstring_end'>&quot;</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'>&quot;</span><span class='tstring_content'>credit_card</span><span class='tstring_end'>&quot;</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'>&quot;</span><span class='tstring_content'>Order is not in a pending state</span><span class='tstring_end'>&quot;</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'>&quot;</span><span class='tstring_content'>processed</span><span class='tstring_end'>&quot;</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&#39;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'>&lt;</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'>&quot;</span><span class='tstring_content'>That didn&#39;t work for some reason</span><span class='tstring_end'>&quot;</span></span><span class='rparen'>)</span>
308
+ <span class='comment'>#=&gt; Response(false, nil, Servus::Support::Errors::ServiceError(&quot;That didn&#39;t work for some reason&quot;))
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'>&quot;</span><span class='tstring_content'>Custom message</span><span class='tstring_end'>&quot;</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'>#=&gt; Response(false, nil, Servus::Support::Errors::NotFoundError(&quot;Custom message&quot;))
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'>#=&gt; Response(false, nil, Servus::Support::Errors::NotFoundError(&quot;Not found&quot;))
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'>#=&gt; Response(false, nil, Servus::Support::Errors::ServiceError(&quot;An error occurred&quot;))
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'>&lt;</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 &lt; Servus::Base
351
+ class SomethingBroke &lt; StandardError; end
352
+ class SomethingGlitched &lt; 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 =&gt; e
369
+ raise SomethingGlitched, &quot;Whoaaaa, something went wrong! #{e.message}&quot;
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
+ =&gt; Servus::Support::Errors::ServiceUnavailableError
379
+ result.error.message
380
+ =&gt; &quot;[SomeServiceObject::Service::SomethingGlitched]: Whoaaaa, something went wrong! Net::HTTPError (503)&quot;
381
+ result.error.api_error
382
+ =&gt; { code: :service_unavailable, message: &quot;[SomeServiceObject::Service::SomethingGlitched]: Whoaaaa, something went wrong! Net::HTTPError (503)&quot; }
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'>&lt;</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'>&quot;</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'>&quot;</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&#39;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'>&lt;</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'># =&gt; { message: &quot;Error message&quot;, 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'>&lt;</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
+ &quot;type&quot;: &quot;object&quot;,
476
+ &quot;required&quot;: [&quot;user_id&quot;, &quot;amount&quot;, &quot;payment_method&quot;],
477
+ &quot;properties&quot;: {
478
+ &quot;user_id&quot;: { &quot;type&quot;: &quot;integer&quot; },
479
+ &quot;amount&quot;: {
480
+ &quot;type&quot;: &quot;integer&quot;,
481
+ &quot;minimum&quot;: 1
482
+ },
483
+ &quot;payment_method&quot;: {
484
+ &quot;type&quot;: &quot;string&quot;,
485
+ &quot;enum&quot;: [&quot;credit_card&quot;, &quot;paypal&quot;, &quot;bank_transfer&quot;]
486
+ },
487
+ &quot;currency&quot;: {
488
+ &quot;type&quot;: &quot;string&quot;,
489
+ &quot;default&quot;: &quot;USD&quot;
490
+ }
491
+ },
492
+ &quot;additionalProperties&quot;: 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
+ &quot;type&quot;: &quot;object&quot;,
501
+ &quot;required&quot;: [&quot;transaction_id&quot;, &quot;status&quot;],
502
+ &quot;properties&quot;: {
503
+ &quot;transaction_id&quot;: { &quot;type&quot;: &quot;string&quot; },
504
+ &quot;status&quot;: {
505
+ &quot;type&quot;: &quot;string&quot;,
506
+ &quot;enum&quot;: [&quot;approved&quot;, &quot;pending&quot;, &quot;declined&quot;]
507
+ },
508
+ &quot;receipt_url&quot;: { &quot;type&quot;: &quot;string&quot; }
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'>&lt;</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'>&quot;</span><span class='tstring_content'>object</span><span class='tstring_end'>&quot;</span></span><span class='comma'>,</span>
522
+ <span class='label'>required:</span> <span class='lbracket'>[</span><span class='tstring'><span class='tstring_beg'>&quot;</span><span class='tstring_content'>user_id</span><span class='tstring_end'>&quot;</span></span><span class='comma'>,</span> <span class='tstring'><span class='tstring_beg'>&quot;</span><span class='tstring_content'>amount</span><span class='tstring_end'>&quot;</span></span><span class='comma'>,</span> <span class='tstring'><span class='tstring_beg'>&quot;</span><span class='tstring_content'>payment_method</span><span class='tstring_end'>&quot;</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'>&quot;</span><span class='tstring_content'>integer</span><span class='tstring_end'>&quot;</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'>&quot;</span><span class='tstring_content'>integer</span><span class='tstring_end'>&quot;</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'>&quot;</span><span class='tstring_content'>string</span><span class='tstring_end'>&quot;</span></span><span class='comma'>,</span>
531
+ <span class='label'>enum:</span> <span class='lbracket'>[</span><span class='tstring'><span class='tstring_beg'>&quot;</span><span class='tstring_content'>credit_card</span><span class='tstring_end'>&quot;</span></span><span class='comma'>,</span> <span class='tstring'><span class='tstring_beg'>&quot;</span><span class='tstring_content'>paypal</span><span class='tstring_end'>&quot;</span></span><span class='comma'>,</span> <span class='tstring'><span class='tstring_beg'>&quot;</span><span class='tstring_content'>bank_transfer</span><span class='tstring_end'>&quot;</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'>&quot;</span><span class='tstring_content'>string</span><span class='tstring_end'>&quot;</span></span><span class='comma'>,</span>
535
+ <span class='label'>default:</span> <span class='tstring'><span class='tstring_beg'>&quot;</span><span class='tstring_content'>USD</span><span class='tstring_end'>&quot;</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'>&quot;</span><span class='tstring_content'>object</span><span class='tstring_end'>&quot;</span></span><span class='comma'>,</span>
542
+ <span class='label'>required:</span> <span class='lbracket'>[</span><span class='tstring'><span class='tstring_beg'>&quot;</span><span class='tstring_content'>transaction_id</span><span class='tstring_end'>&quot;</span></span><span class='comma'>,</span> <span class='tstring'><span class='tstring_beg'>&quot;</span><span class='tstring_content'>status</span><span class='tstring_end'>&quot;</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'>&quot;</span><span class='tstring_content'>string</span><span class='tstring_end'>&quot;</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'>&quot;</span><span class='tstring_content'>string</span><span class='tstring_end'>&quot;</span></span><span class='comma'>,</span>
547
+ <span class='label'>enum:</span> <span class='lbracket'>[</span><span class='tstring'><span class='tstring_beg'>&quot;</span><span class='tstring_content'>approved</span><span class='tstring_end'>&quot;</span></span><span class='comma'>,</span> <span class='tstring'><span class='tstring_beg'>&quot;</span><span class='tstring_content'>pending</span><span class='tstring_end'>&quot;</span></span><span class='comma'>,</span> <span class='tstring'><span class='tstring_beg'>&quot;</span><span class='tstring_content'>declined</span><span class='tstring_end'>&quot;</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'>&quot;</span><span class='tstring_content'>string</span><span class='tstring_end'>&quot;</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'>&#39;</span><span class='tstring_content'>USD</span><span class='tstring_end'>&#39;</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'>&quot;</span><span class='tstring_content'>txn_1</span><span class='tstring_end'>&quot;</span></span><span class='comma'>,</span>
565
+ <span class='label'>status:</span> <span class='tstring'><span class='tstring_beg'>&quot;</span><span class='tstring_content'>approved</span><span class='tstring_end'>&quot;</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=&gt;123, :amount=&gt;50}
625
+ INFO -- : Services::ProcessPayment::Service succeeded in 0.245s
626
+
627
+ # Failure
628
+ INFO -- : Calling Services::ProcessPayment::Service with args: {:user_id=&gt;123, :amount=&gt;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 &#39;#/amount&#39; value -10 was less than minimum value 1
633
+
634
+ # Exception
635
+ ERROR -- : Services::ProcessPayment::Service uncaught exception: NoMethodError - undefined method &#39;charge&#39; 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'>&#39;</span><span class='tstring_content'>lib/schemas</span><span class='tstring_end'>&#39;</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>