respondo 2.1.2 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +17 -14
- data/lib/generators/respondo/install/install_generator.rb +351 -351
- data/lib/respondo/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eac53474dd61508e20d695e46cc364e3bf9ebdb0b89a6e1b4ae93eda15ebc7b7
|
|
4
|
+
data.tar.gz: 8f1035182c1795269fe3266473bdf4b67bcfa56fcdaf10d6593c5e56739a955c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b9540985dd09e11ee39c87cccccc13b9056f07cb762a8b6523a5e1a1666cdb4e0e8ca1ab6dfd9305706c5d48386c73bd5dfbabc070b05c7a1d92d08877fd5ba4
|
|
7
|
+
data.tar.gz: 53bdf553e918de4f9b4e0a3df8fc3fb894fcbe0efaae7a2daeaa97990a18974b8b80d18b094d2fa2f8f10c7098fae0c01067394c3b6be6fb45766e5f68dd04b3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.1.3] — Docs & Dependency Updates
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- README updated (add a one-line summary of what you changed)
|
|
7
|
+
- Development dependencies updated to patched versions, fixing CVEs in
|
|
8
|
+
`concurrent-ruby`, `erb` and `rails-html-sanitizer`. These are test-only
|
|
9
|
+
dependencies, so there is no change to the gem's runtime behavior.
|
|
10
|
+
|
|
3
11
|
## [2.1.2] — Code Quality & Test Coverage
|
|
4
12
|
|
|
5
13
|
### Fixed
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Respondo 🎯
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Consistent JSON API responses for Rails, in one line.
|
|
6
6
|
|
|
7
7
|
[](https://rubygems.org/gems/respondo)
|
|
8
8
|
[](https://rubygems.org/gems/respondo)
|
|
@@ -28,9 +28,9 @@ You're building an API consumed by a React frontend and a Flutter app. Three dev
|
|
|
28
28
|
{ "users": [...] }
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Your frontend devs
|
|
31
|
+
Your frontend devs end up writing `if (res.data || res.result || res.users)` just to survive. The Front End team files bugs about the mismatch. Code review turns into an argument about whose format is "right."
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Respondo fixes this: one response shape, enforced automatically across every controller. Every developer. Every time.
|
|
34
34
|
|
|
35
35
|
```json
|
|
36
36
|
{
|
|
@@ -58,7 +58,7 @@ bundle install
|
|
|
58
58
|
rails generate respondo:install
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
That's it. No `include` in `ApplicationController
|
|
61
|
+
That's it. No `include` in `ApplicationController`, no boilerplate — Respondo hooks in automatically via a Railtie.
|
|
62
62
|
|
|
63
63
|
---
|
|
64
64
|
|
|
@@ -87,11 +87,11 @@ class UsersController < ApplicationController
|
|
|
87
87
|
end
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
Now your frontend can count on getting the same shape back every time.
|
|
91
91
|
|
|
92
92
|
---
|
|
93
93
|
|
|
94
|
-
##
|
|
94
|
+
## What changes
|
|
95
95
|
|
|
96
96
|
| Without Respondo | With Respondo |
|
|
97
97
|
|---|---|
|
|
@@ -100,13 +100,13 @@ Your frontend now gets a **guaranteed** structure — forever.
|
|
|
100
100
|
| Pagination shape differs per endpoint | Pagination always in `meta.pagination` |
|
|
101
101
|
| Validation errors in different keys | Always in `errors`, always a hash |
|
|
102
102
|
| `render json:` boilerplate in every action | One expressive method call |
|
|
103
|
-
| camelCase conversion scattered across code | `config.camelize_keys = true
|
|
103
|
+
| camelCase conversion scattered across code | `config.camelize_keys = true`, done |
|
|
104
104
|
|
|
105
105
|
---
|
|
106
106
|
|
|
107
107
|
## What every response looks like
|
|
108
108
|
|
|
109
|
-
Every response
|
|
109
|
+
Every response, success or error, has the same four keys:
|
|
110
110
|
|
|
111
111
|
| Key | Type | Description |
|
|
112
112
|
|---|---|---|
|
|
@@ -189,12 +189,12 @@ class PostsController < ApplicationController
|
|
|
189
189
|
end
|
|
190
190
|
```
|
|
191
191
|
|
|
192
|
-
## Auto-
|
|
192
|
+
## Auto-serialization
|
|
193
193
|
|
|
194
194
|
Respondo automatically handles:
|
|
195
195
|
|
|
196
196
|
| Input type | Output |
|
|
197
|
-
|
|
197
|
+
|----------------------------------|--------------------------------------|
|
|
198
198
|
| `ActiveRecord::Base` instance | `record.as_json` |
|
|
199
199
|
| `ActiveRecord::Relation` | Array of `as_json` records |
|
|
200
200
|
| `ActiveModel::Errors` | `{ field: ["message", ...] }` |
|
|
@@ -252,7 +252,7 @@ config.serializer = ->(obj) { MySerializer.new(obj).as_json }
|
|
|
252
252
|
|
|
253
253
|
## Global error handling (recommended pattern)
|
|
254
254
|
|
|
255
|
-
Add this to `ApplicationController` to handle exceptions app-wide without try/rescue in every action:
|
|
255
|
+
Add this to `ApplicationController` to handle exceptions app-wide without a try/rescue in every action:
|
|
256
256
|
|
|
257
257
|
```ruby
|
|
258
258
|
class ApplicationController < ActionController::API
|
|
@@ -275,7 +275,7 @@ end
|
|
|
275
275
|
|
|
276
276
|
## Complete HTTP helper reference
|
|
277
277
|
|
|
278
|
-
|
|
278
|
+
There's a helper for basically every HTTP status code. Here are the ones you'll actually reach for:
|
|
279
279
|
|
|
280
280
|
### 2xx — Success
|
|
281
281
|
|
|
@@ -286,7 +286,7 @@ Respondo covers every HTTP status code. Here are the helpers you'll use every da
|
|
|
286
286
|
| `render_accepted` | 202 | Async jobs — request queued |
|
|
287
287
|
| `render_no_content` | 200* | After DELETE — no body |
|
|
288
288
|
|
|
289
|
-
> *Rails renders 200 with a JSON body for `render_no_content` to
|
|
289
|
+
> *Rails renders 200 with a JSON body for `render_no_content` to keep the response shape consistent.
|
|
290
290
|
|
|
291
291
|
```ruby
|
|
292
292
|
render_ok(data: @user, message: "Profile fetched")
|
|
@@ -422,5 +422,8 @@ Released under the [MIT License](LICENSE).
|
|
|
422
422
|
---
|
|
423
423
|
|
|
424
424
|
<div align="center">
|
|
425
|
-
<sub>If Respondo saved
|
|
425
|
+
<sub>If Respondo saved your an hour, give it a Star⭐.</sub>
|
|
426
|
+
</div>
|
|
427
|
+
<div align="center">
|
|
428
|
+
<sub>it helps other developers find it.</sub>
|
|
426
429
|
</div>
|
|
@@ -1,352 +1,352 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "rails/generators"
|
|
4
|
-
|
|
5
|
-
module Respondo
|
|
6
|
-
module Generators
|
|
7
|
-
class InstallGenerator < Rails::Generators::Base
|
|
8
|
-
desc "Interactive setup — creates config/initializers/respondo.rb with your preferences."
|
|
9
|
-
|
|
10
|
-
# We bypass Thor's `say` entirely for all display output and use
|
|
11
|
-
# $stdout.puts / print directly. This prevents Thor from re-echoing
|
|
12
|
-
# buffered output and causing duplicate lines in the terminal.
|
|
13
|
-
|
|
14
|
-
def run_interactive_setup
|
|
15
|
-
# out LOGO
|
|
16
|
-
out logo_with_version
|
|
17
|
-
out divider
|
|
18
|
-
out line(" This wizard will generate config/initializers/respondo.rb")
|
|
19
|
-
out line(" tailored to your project — no need to read the full README.")
|
|
20
|
-
out blank
|
|
21
|
-
out line(yellow(" All settings can be changed later by editing the initializer."))
|
|
22
|
-
out divider
|
|
23
|
-
out blank
|
|
24
|
-
|
|
25
|
-
unless confirm(" Ready to configure Respondo? (y/n) ")
|
|
26
|
-
out blank
|
|
27
|
-
out line(yellow(" Skipped. Run this again any time:"))
|
|
28
|
-
out line(" rails generate respondo:install")
|
|
29
|
-
out blank
|
|
30
|
-
return
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
@cfg = {}
|
|
34
|
-
|
|
35
|
-
step_project_info
|
|
36
|
-
step_messages
|
|
37
|
-
step_request_id
|
|
38
|
-
step_camelize
|
|
39
|
-
step_default_meta
|
|
40
|
-
step_serializer
|
|
41
|
-
|
|
42
|
-
print_summary
|
|
43
|
-
write_initializer
|
|
44
|
-
print_done
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
# =========================================================================
|
|
50
|
-
# Steps
|
|
51
|
-
# =========================================================================
|
|
52
|
-
|
|
53
|
-
def step_project_info
|
|
54
|
-
out section("Project Info")
|
|
55
|
-
out line(" Project / app name")
|
|
56
|
-
out line(yellow(" (Used as a comment header in the initializer)"))
|
|
57
|
-
@cfg[:project_name] = prompt_default(Rails.application.class.module_parent_name)
|
|
58
|
-
|
|
59
|
-
out blank
|
|
60
|
-
out line(" API version")
|
|
61
|
-
out line(yellow(" (e.g. v1 — added to every response meta block)"))
|
|
62
|
-
@cfg[:api_version] = prompt_default("v1")
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def step_messages
|
|
66
|
-
out section("Response Messages")
|
|
67
|
-
out line(" Fallback messages used when you don't pass message: explicitly.")
|
|
68
|
-
out blank
|
|
69
|
-
|
|
70
|
-
out line(" Default success message")
|
|
71
|
-
@cfg[:default_success_message] = prompt_default("Success")
|
|
72
|
-
|
|
73
|
-
out blank
|
|
74
|
-
out line(" Default error message")
|
|
75
|
-
@cfg[:default_error_message] = prompt_default("An error occurred")
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def step_request_id
|
|
79
|
-
out section("Request ID")
|
|
80
|
-
out line(" When enabled, Rails request.request_id is included in every")
|
|
81
|
-
out line(" response meta block — useful for log correlation and debugging.")
|
|
82
|
-
out blank
|
|
83
|
-
@cfg[:include_request_id] = confirm(" Include request_id in every response? (y/n) ")
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def step_camelize
|
|
87
|
-
out section("Key Format")
|
|
88
|
-
out line(" When enabled, all JSON keys are camelCased:")
|
|
89
|
-
out line(yellow(' { "createdAt": "...", "userId": 1 }'))
|
|
90
|
-
out line(" Recommended for Flutter, React, and JavaScript clients.")
|
|
91
|
-
out blank
|
|
92
|
-
@cfg[:camelize_keys] = confirm(" Camelize all response keys? (y/n) ")
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def step_default_meta
|
|
96
|
-
out section("Global Meta Fields")
|
|
97
|
-
out line(" Static key=value pairs merged into the meta block of EVERY response.")
|
|
98
|
-
out line(" Example: platform=mobile environment=production")
|
|
99
|
-
out blank
|
|
100
|
-
out line(yellow(" Note: api_version from above is already included automatically."))
|
|
101
|
-
out blank
|
|
102
|
-
|
|
103
|
-
@cfg[:default_meta] = {}
|
|
104
|
-
return unless confirm(" Add extra global meta fields? (y/n) ")
|
|
105
|
-
|
|
106
|
-
out blank
|
|
107
|
-
out line(" Enter key=value one at a time. Blank line to finish.")
|
|
108
|
-
out blank
|
|
109
|
-
|
|
110
|
-
loop do
|
|
111
|
-
$stdout.print cyan(" key=value › ")
|
|
112
|
-
raw = $stdin.gets.to_s.strip
|
|
113
|
-
break if raw.empty?
|
|
114
|
-
|
|
115
|
-
unless raw.include?("=")
|
|
116
|
-
out line(yellow(" Use key=value format (e.g. platform=mobile). Skipping."))
|
|
117
|
-
next
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
key, value = raw.split("=", 2)
|
|
121
|
-
|
|
122
|
-
next out line(yellow(" Use key=value format.")) if key.nil? || key.strip.empty?
|
|
123
|
-
|
|
124
|
-
k = key.strip
|
|
125
|
-
v = (value || "").strip
|
|
126
|
-
@cfg[:default_meta][k] = v
|
|
127
|
-
out line(green(" ✓ #{k}: #{v.inspect}"))
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def step_serializer
|
|
132
|
-
out section("Custom Serializer")
|
|
133
|
-
out line(" By default Respondo serializes ActiveRecord models, collections,")
|
|
134
|
-
out line(" hashes, and arrays automatically.")
|
|
135
|
-
out blank
|
|
136
|
-
out line(" You can override with any callable: ->(obj) { MySerializer.new(obj).as_json }")
|
|
137
|
-
out blank
|
|
138
|
-
@cfg[:custom_serializer] = confirm(" Add a custom serializer stub? (y/n) ")
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# =========================================================================
|
|
142
|
-
# Summary
|
|
143
|
-
# =========================================================================
|
|
144
|
-
|
|
145
|
-
def print_summary
|
|
146
|
-
out blank
|
|
147
|
-
out divider
|
|
148
|
-
out line(cyan(" Configuration Summary"))
|
|
149
|
-
out divider
|
|
150
|
-
out blank
|
|
151
|
-
summary_row "Project", @cfg[:project_name]
|
|
152
|
-
summary_row "API version", @cfg[:api_version]
|
|
153
|
-
summary_row "Success message", @cfg[:default_success_message]
|
|
154
|
-
summary_row "Error message", @cfg[:default_error_message]
|
|
155
|
-
summary_row "Include request_id", @cfg[:include_request_id]
|
|
156
|
-
summary_row "Camelize keys", @cfg[:camelize_keys]
|
|
157
|
-
summary_row "Custom serializer", @cfg[:custom_serializer]
|
|
158
|
-
|
|
159
|
-
unless @cfg[:default_meta].empty?
|
|
160
|
-
out blank
|
|
161
|
-
out line(" Global meta:")
|
|
162
|
-
@cfg[:default_meta].each { |k, v| out line(" #{k}: #{v.inspect}") }
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
out blank
|
|
166
|
-
out divider
|
|
167
|
-
out blank
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def summary_row(label, value)
|
|
171
|
-
bool_true = value == true
|
|
172
|
-
val_str = bool_true ? green(value.inspect) : yellow(value.inspect)
|
|
173
|
-
out " #{("#{label}:").ljust(24)}#{val_str}\n"
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# =========================================================================
|
|
177
|
-
# Write file
|
|
178
|
-
# =========================================================================
|
|
179
|
-
|
|
180
|
-
def write_initializer
|
|
181
|
-
dir = File.join(destination_root, "config", "initializers")
|
|
182
|
-
path = File.join(dir, "respondo.rb")
|
|
183
|
-
FileUtils.mkdir_p(dir)
|
|
184
|
-
File.write(path, build_content)
|
|
185
|
-
out line(green(" ✅ Created config/initializers/respondo.rb"))
|
|
186
|
-
out blank
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def build_content
|
|
190
|
-
# meta = { "api_version" => @cfg[:api_version] }.merge(@cfg[:default_meta])
|
|
191
|
-
meta = @cfg[:default_meta].dup
|
|
192
|
-
meta["api_version"] = @cfg[:api_version] unless @cfg[:api_version].to_s.empty?
|
|
193
|
-
b = Lines.new
|
|
194
|
-
|
|
195
|
-
b << "# frozen_string_literal: true"
|
|
196
|
-
b << ""
|
|
197
|
-
b << "# Respondo initializer — #{@cfg[:project_name]}"
|
|
198
|
-
b << "# Generated by: rails generate respondo:install"
|
|
199
|
-
b << "# Respondo version: #{Respondo::VERSION}"
|
|
200
|
-
b << "# Docs: https://github.com/
|
|
201
|
-
b << ""
|
|
202
|
-
b << "Respondo.configure do |config|"
|
|
203
|
-
b << ""
|
|
204
|
-
b << " # ── Messages ─────────────────────────────────────────────────────────"
|
|
205
|
-
b << " # Fallback when render_success / render_error is called without message:"
|
|
206
|
-
b << " config.default_success_message = #{@cfg[:default_success_message].inspect}"
|
|
207
|
-
b << " config.default_error_message = #{@cfg[:default_error_message].inspect}"
|
|
208
|
-
b << ""
|
|
209
|
-
b << " # ── Request ID ───────────────────────────────────────────────────────"
|
|
210
|
-
b << " # Includes Rails request.request_id in every response meta block."
|
|
211
|
-
b << " config.include_request_id = #{@cfg[:include_request_id]}"
|
|
212
|
-
b << ""
|
|
213
|
-
b << " # ── Key Format ───────────────────────────────────────────────────────"
|
|
214
|
-
b << " # CamelCase all JSON keys — recommended for Flutter / JS clients."
|
|
215
|
-
b << " config.camelize_keys = #{@cfg[:camelize_keys]}"
|
|
216
|
-
b << ""
|
|
217
|
-
b << " # ── Global Meta ──────────────────────────────────────────────────────"
|
|
218
|
-
b << " # Static fields merged into the meta block of every response."
|
|
219
|
-
|
|
220
|
-
if meta.empty?
|
|
221
|
-
b << " config.default_meta = {}"
|
|
222
|
-
else
|
|
223
|
-
b << " config.default_meta = {"
|
|
224
|
-
meta.each_with_index do |(k, v), i|
|
|
225
|
-
comma = i < meta.size - 1 ? "," : ""
|
|
226
|
-
b << " #{k}: #{v.inspect}#{comma}"
|
|
227
|
-
end
|
|
228
|
-
b << " }"
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
if @cfg[:custom_serializer]
|
|
232
|
-
b << ""
|
|
233
|
-
b << " # ── Custom Serializer ────────────────────────────────────────────────"
|
|
234
|
-
b << " # Replace the lambda body with your own serialization logic."
|
|
235
|
-
b << " # Examples:"
|
|
236
|
-
b << " # ActiveModelSerializers: ->(obj) { SomeSerializer.new(obj).as_json }"
|
|
237
|
-
b << " # Blueprinter: ->(obj) { UserBlueprint.render_as_hash(obj) }"
|
|
238
|
-
b << " #"
|
|
239
|
-
b << " # config.serializer = ->(obj) { MySerializer.new(obj).as_json }"
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
b << ""
|
|
243
|
-
b << "end"
|
|
244
|
-
b << ""
|
|
245
|
-
b.to_s
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
# =========================================================================
|
|
249
|
-
# Done
|
|
250
|
-
# =========================================================================
|
|
251
|
-
|
|
252
|
-
def print_done
|
|
253
|
-
out divider
|
|
254
|
-
out blank
|
|
255
|
-
out line(cyan(" 🎉 Respondo is ready!"))
|
|
256
|
-
out blank
|
|
257
|
-
out line(" Next steps:")
|
|
258
|
-
out line(" 1. Review config/initializers/respondo.rb")
|
|
259
|
-
out line(" 2. Use render_success / render_error in your controllers")
|
|
260
|
-
out line(" 3. Re-run rails generate respondo:install to regenerate")
|
|
261
|
-
out blank
|
|
262
|
-
out divider
|
|
263
|
-
out blank
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
# =========================================================================
|
|
267
|
-
# Output primitives — all output goes through $stdout, never through Thor
|
|
268
|
-
# =========================================================================
|
|
269
|
-
|
|
270
|
-
def out(str)
|
|
271
|
-
$stdout.print str
|
|
272
|
-
$stdout.flush
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def line(str) = "#{str}\n"
|
|
276
|
-
def blank = "\n"
|
|
277
|
-
|
|
278
|
-
def divider
|
|
279
|
-
" #{cyan("─" * 68)}\n"
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
def section(title)
|
|
283
|
-
dashes = "─" * [0, 54 - title.length].max
|
|
284
|
-
"\n #{cyan("┌─ #{title} #{dashes}┐")}\n\n"
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
def prompt_default(default)
|
|
288
|
-
$stdout.print " #{cyan("›")} #{yellow("[#{default}]")}: "
|
|
289
|
-
$stdout.flush
|
|
290
|
-
result = $stdin.gets.to_s.strip
|
|
291
|
-
result.empty? ? default.to_s : result
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
def confirm(question)
|
|
295
|
-
$stdout.print question
|
|
296
|
-
$stdout.flush
|
|
297
|
-
$stdin.gets.to_s.strip.downcase.start_with?("y")
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
# =========================================================================
|
|
301
|
-
# ANSI colors
|
|
302
|
-
# =========================================================================
|
|
303
|
-
|
|
304
|
-
def cyan(t) = "\e[36m#{t}\e[0m"
|
|
305
|
-
def green(t) = "\e[32m#{t}\e[0m"
|
|
306
|
-
def yellow(t) = "\e[33m#{t}\e[0m"
|
|
307
|
-
|
|
308
|
-
# =========================================================================
|
|
309
|
-
# ASCII logo
|
|
310
|
-
# =========================================================================
|
|
311
|
-
|
|
312
|
-
LOGOS = <<~'LOGO'
|
|
313
|
-
|
|
314
|
-
██████╗ ███████╗███████╗██████╗ ██████╗ ███╗ ██╗██████╗ ██████╗
|
|
315
|
-
██╔══██╗██╔════╝██╔════╝██╔══██╗██╔═══██╗████╗ ██║██╔══██╗██╔═══██╗
|
|
316
|
-
██████╔╝█████╗ ███████╗██████╔╝██║ ██║██╔██╗ ██║██║ ██║██║ ██║
|
|
317
|
-
██╔══██╗██╔══╝ ╚════██║██╔═══╝ ██║ ██║██║╚██╗██║██║ ██║██║ ██║
|
|
318
|
-
██║ ██║███████╗███████║██║ ╚██████╔╝██║ ╚████║██████╔╝╚██████╔╝
|
|
319
|
-
╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═════╝ ╚═════╝
|
|
320
|
-
|
|
321
|
-
Smart JSON API Response Formatter for Rails
|
|
322
|
-
─── v#{Respondo::VERSION} ───
|
|
323
|
-
|
|
324
|
-
LOGO
|
|
325
|
-
|
|
326
|
-
def logo_with_version
|
|
327
|
-
green(<<~LOGO)
|
|
328
|
-
|
|
329
|
-
██████╗ ███████╗███████╗██████╗ ██████╗ ███╗ ██╗██████╗ ██████╗
|
|
330
|
-
██╔══██╗██╔════╝██╔════╝██╔══██╗██╔═══██╗████╗ ██║██╔══██╗██╔═══██╗
|
|
331
|
-
██████╔╝█████╗ ███████╗██████╔╝██║ ██║██╔██╗ ██║██║ ██║██║ ██║
|
|
332
|
-
██╔══██╗██╔══╝ ╚════██║██╔═══╝ ██║ ██║██║╚██╗██║██║ ██║██║ ██║
|
|
333
|
-
██║ ██║███████╗███████║██║ ╚██████╔╝██║ ╚████║██████╔╝╚██████╔╝
|
|
334
|
-
╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═════╝ ╚═════╝
|
|
335
|
-
|
|
336
|
-
Smart JSON API Response Formatter for Rails
|
|
337
|
-
─── v#{Respondo::VERSION} ───
|
|
338
|
-
|
|
339
|
-
LOGO
|
|
340
|
-
end
|
|
341
|
-
# =========================================================================
|
|
342
|
-
# Simple line buffer for building file content
|
|
343
|
-
# =========================================================================
|
|
344
|
-
|
|
345
|
-
class Lines
|
|
346
|
-
def initialize = (@buf = [])
|
|
347
|
-
def <<(str) = @buf << str
|
|
348
|
-
def to_s = @buf.join("\n") + "\n"
|
|
349
|
-
end
|
|
350
|
-
end
|
|
351
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module Respondo
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
desc "Interactive setup — creates config/initializers/respondo.rb with your preferences."
|
|
9
|
+
|
|
10
|
+
# We bypass Thor's `say` entirely for all display output and use
|
|
11
|
+
# $stdout.puts / print directly. This prevents Thor from re-echoing
|
|
12
|
+
# buffered output and causing duplicate lines in the terminal.
|
|
13
|
+
|
|
14
|
+
def run_interactive_setup
|
|
15
|
+
# out LOGO
|
|
16
|
+
out logo_with_version
|
|
17
|
+
out divider
|
|
18
|
+
out line(" This wizard will generate config/initializers/respondo.rb")
|
|
19
|
+
out line(" tailored to your project — no need to read the full README.")
|
|
20
|
+
out blank
|
|
21
|
+
out line(yellow(" All settings can be changed later by editing the initializer."))
|
|
22
|
+
out divider
|
|
23
|
+
out blank
|
|
24
|
+
|
|
25
|
+
unless confirm(" Ready to configure Respondo? (y/n) ")
|
|
26
|
+
out blank
|
|
27
|
+
out line(yellow(" Skipped. Run this again any time:"))
|
|
28
|
+
out line(" rails generate respondo:install")
|
|
29
|
+
out blank
|
|
30
|
+
return
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
@cfg = {}
|
|
34
|
+
|
|
35
|
+
step_project_info
|
|
36
|
+
step_messages
|
|
37
|
+
step_request_id
|
|
38
|
+
step_camelize
|
|
39
|
+
step_default_meta
|
|
40
|
+
step_serializer
|
|
41
|
+
|
|
42
|
+
print_summary
|
|
43
|
+
write_initializer
|
|
44
|
+
print_done
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# =========================================================================
|
|
50
|
+
# Steps
|
|
51
|
+
# =========================================================================
|
|
52
|
+
|
|
53
|
+
def step_project_info
|
|
54
|
+
out section("Project Info")
|
|
55
|
+
out line(" Project / app name")
|
|
56
|
+
out line(yellow(" (Used as a comment header in the initializer)"))
|
|
57
|
+
@cfg[:project_name] = prompt_default(Rails.application.class.module_parent_name)
|
|
58
|
+
|
|
59
|
+
out blank
|
|
60
|
+
out line(" API version")
|
|
61
|
+
out line(yellow(" (e.g. v1 — added to every response meta block)"))
|
|
62
|
+
@cfg[:api_version] = prompt_default("v1")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def step_messages
|
|
66
|
+
out section("Response Messages")
|
|
67
|
+
out line(" Fallback messages used when you don't pass message: explicitly.")
|
|
68
|
+
out blank
|
|
69
|
+
|
|
70
|
+
out line(" Default success message")
|
|
71
|
+
@cfg[:default_success_message] = prompt_default("Success")
|
|
72
|
+
|
|
73
|
+
out blank
|
|
74
|
+
out line(" Default error message")
|
|
75
|
+
@cfg[:default_error_message] = prompt_default("An error occurred")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def step_request_id
|
|
79
|
+
out section("Request ID")
|
|
80
|
+
out line(" When enabled, Rails request.request_id is included in every")
|
|
81
|
+
out line(" response meta block — useful for log correlation and debugging.")
|
|
82
|
+
out blank
|
|
83
|
+
@cfg[:include_request_id] = confirm(" Include request_id in every response? (y/n) ")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def step_camelize
|
|
87
|
+
out section("Key Format")
|
|
88
|
+
out line(" When enabled, all JSON keys are camelCased:")
|
|
89
|
+
out line(yellow(' { "createdAt": "...", "userId": 1 }'))
|
|
90
|
+
out line(" Recommended for Flutter, React, and JavaScript clients.")
|
|
91
|
+
out blank
|
|
92
|
+
@cfg[:camelize_keys] = confirm(" Camelize all response keys? (y/n) ")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def step_default_meta
|
|
96
|
+
out section("Global Meta Fields")
|
|
97
|
+
out line(" Static key=value pairs merged into the meta block of EVERY response.")
|
|
98
|
+
out line(" Example: platform=mobile environment=production")
|
|
99
|
+
out blank
|
|
100
|
+
out line(yellow(" Note: api_version from above is already included automatically."))
|
|
101
|
+
out blank
|
|
102
|
+
|
|
103
|
+
@cfg[:default_meta] = {}
|
|
104
|
+
return unless confirm(" Add extra global meta fields? (y/n) ")
|
|
105
|
+
|
|
106
|
+
out blank
|
|
107
|
+
out line(" Enter key=value one at a time. Blank line to finish.")
|
|
108
|
+
out blank
|
|
109
|
+
|
|
110
|
+
loop do
|
|
111
|
+
$stdout.print cyan(" key=value › ")
|
|
112
|
+
raw = $stdin.gets.to_s.strip
|
|
113
|
+
break if raw.empty?
|
|
114
|
+
|
|
115
|
+
unless raw.include?("=")
|
|
116
|
+
out line(yellow(" Use key=value format (e.g. platform=mobile). Skipping."))
|
|
117
|
+
next
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
key, value = raw.split("=", 2)
|
|
121
|
+
|
|
122
|
+
next out line(yellow(" Use key=value format.")) if key.nil? || key.strip.empty?
|
|
123
|
+
|
|
124
|
+
k = key.strip
|
|
125
|
+
v = (value || "").strip
|
|
126
|
+
@cfg[:default_meta][k] = v
|
|
127
|
+
out line(green(" ✓ #{k}: #{v.inspect}"))
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def step_serializer
|
|
132
|
+
out section("Custom Serializer")
|
|
133
|
+
out line(" By default Respondo serializes ActiveRecord models, collections,")
|
|
134
|
+
out line(" hashes, and arrays automatically.")
|
|
135
|
+
out blank
|
|
136
|
+
out line(" You can override with any callable: ->(obj) { MySerializer.new(obj).as_json }")
|
|
137
|
+
out blank
|
|
138
|
+
@cfg[:custom_serializer] = confirm(" Add a custom serializer stub? (y/n) ")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# =========================================================================
|
|
142
|
+
# Summary
|
|
143
|
+
# =========================================================================
|
|
144
|
+
|
|
145
|
+
def print_summary
|
|
146
|
+
out blank
|
|
147
|
+
out divider
|
|
148
|
+
out line(cyan(" Configuration Summary"))
|
|
149
|
+
out divider
|
|
150
|
+
out blank
|
|
151
|
+
summary_row "Project", @cfg[:project_name]
|
|
152
|
+
summary_row "API version", @cfg[:api_version]
|
|
153
|
+
summary_row "Success message", @cfg[:default_success_message]
|
|
154
|
+
summary_row "Error message", @cfg[:default_error_message]
|
|
155
|
+
summary_row "Include request_id", @cfg[:include_request_id]
|
|
156
|
+
summary_row "Camelize keys", @cfg[:camelize_keys]
|
|
157
|
+
summary_row "Custom serializer", @cfg[:custom_serializer]
|
|
158
|
+
|
|
159
|
+
unless @cfg[:default_meta].empty?
|
|
160
|
+
out blank
|
|
161
|
+
out line(" Global meta:")
|
|
162
|
+
@cfg[:default_meta].each { |k, v| out line(" #{k}: #{v.inspect}") }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
out blank
|
|
166
|
+
out divider
|
|
167
|
+
out blank
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def summary_row(label, value)
|
|
171
|
+
bool_true = value == true
|
|
172
|
+
val_str = bool_true ? green(value.inspect) : yellow(value.inspect)
|
|
173
|
+
out " #{("#{label}:").ljust(24)}#{val_str}\n"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# =========================================================================
|
|
177
|
+
# Write file
|
|
178
|
+
# =========================================================================
|
|
179
|
+
|
|
180
|
+
def write_initializer
|
|
181
|
+
dir = File.join(destination_root, "config", "initializers")
|
|
182
|
+
path = File.join(dir, "respondo.rb")
|
|
183
|
+
FileUtils.mkdir_p(dir)
|
|
184
|
+
File.write(path, build_content)
|
|
185
|
+
out line(green(" ✅ Created config/initializers/respondo.rb"))
|
|
186
|
+
out blank
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def build_content
|
|
190
|
+
# meta = { "api_version" => @cfg[:api_version] }.merge(@cfg[:default_meta])
|
|
191
|
+
meta = @cfg[:default_meta].dup
|
|
192
|
+
meta["api_version"] = @cfg[:api_version] unless @cfg[:api_version].to_s.empty?
|
|
193
|
+
b = Lines.new
|
|
194
|
+
|
|
195
|
+
b << "# frozen_string_literal: true"
|
|
196
|
+
b << ""
|
|
197
|
+
b << "# Respondo initializer — #{@cfg[:project_name]}"
|
|
198
|
+
b << "# Generated by: rails generate respondo:install"
|
|
199
|
+
b << "# Respondo version: #{Respondo::VERSION}"
|
|
200
|
+
b << "# Docs: https://github.com/spatelpatidar/respondo"
|
|
201
|
+
b << ""
|
|
202
|
+
b << "Respondo.configure do |config|"
|
|
203
|
+
b << ""
|
|
204
|
+
b << " # ── Messages ─────────────────────────────────────────────────────────"
|
|
205
|
+
b << " # Fallback when render_success / render_error is called without message:"
|
|
206
|
+
b << " config.default_success_message = #{@cfg[:default_success_message].inspect}"
|
|
207
|
+
b << " config.default_error_message = #{@cfg[:default_error_message].inspect}"
|
|
208
|
+
b << ""
|
|
209
|
+
b << " # ── Request ID ───────────────────────────────────────────────────────"
|
|
210
|
+
b << " # Includes Rails request.request_id in every response meta block."
|
|
211
|
+
b << " config.include_request_id = #{@cfg[:include_request_id]}"
|
|
212
|
+
b << ""
|
|
213
|
+
b << " # ── Key Format ───────────────────────────────────────────────────────"
|
|
214
|
+
b << " # CamelCase all JSON keys — recommended for Flutter / JS clients."
|
|
215
|
+
b << " config.camelize_keys = #{@cfg[:camelize_keys]}"
|
|
216
|
+
b << ""
|
|
217
|
+
b << " # ── Global Meta ──────────────────────────────────────────────────────"
|
|
218
|
+
b << " # Static fields merged into the meta block of every response."
|
|
219
|
+
|
|
220
|
+
if meta.empty?
|
|
221
|
+
b << " config.default_meta = {}"
|
|
222
|
+
else
|
|
223
|
+
b << " config.default_meta = {"
|
|
224
|
+
meta.each_with_index do |(k, v), i|
|
|
225
|
+
comma = i < meta.size - 1 ? "," : ""
|
|
226
|
+
b << " #{k}: #{v.inspect}#{comma}"
|
|
227
|
+
end
|
|
228
|
+
b << " }"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
if @cfg[:custom_serializer]
|
|
232
|
+
b << ""
|
|
233
|
+
b << " # ── Custom Serializer ────────────────────────────────────────────────"
|
|
234
|
+
b << " # Replace the lambda body with your own serialization logic."
|
|
235
|
+
b << " # Examples:"
|
|
236
|
+
b << " # ActiveModelSerializers: ->(obj) { SomeSerializer.new(obj).as_json }"
|
|
237
|
+
b << " # Blueprinter: ->(obj) { UserBlueprint.render_as_hash(obj) }"
|
|
238
|
+
b << " #"
|
|
239
|
+
b << " # config.serializer = ->(obj) { MySerializer.new(obj).as_json }"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
b << ""
|
|
243
|
+
b << "end"
|
|
244
|
+
b << ""
|
|
245
|
+
b.to_s
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# =========================================================================
|
|
249
|
+
# Done
|
|
250
|
+
# =========================================================================
|
|
251
|
+
|
|
252
|
+
def print_done
|
|
253
|
+
out divider
|
|
254
|
+
out blank
|
|
255
|
+
out line(cyan(" 🎉 Respondo is ready!"))
|
|
256
|
+
out blank
|
|
257
|
+
out line(" Next steps:")
|
|
258
|
+
out line(" 1. Review config/initializers/respondo.rb")
|
|
259
|
+
out line(" 2. Use render_success / render_error in your controllers")
|
|
260
|
+
out line(" 3. Re-run rails generate respondo:install to regenerate")
|
|
261
|
+
out blank
|
|
262
|
+
out divider
|
|
263
|
+
out blank
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# =========================================================================
|
|
267
|
+
# Output primitives — all output goes through $stdout, never through Thor
|
|
268
|
+
# =========================================================================
|
|
269
|
+
|
|
270
|
+
def out(str)
|
|
271
|
+
$stdout.print str
|
|
272
|
+
$stdout.flush
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def line(str) = "#{str}\n"
|
|
276
|
+
def blank = "\n"
|
|
277
|
+
|
|
278
|
+
def divider
|
|
279
|
+
" #{cyan("─" * 68)}\n"
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def section(title)
|
|
283
|
+
dashes = "─" * [0, 54 - title.length].max
|
|
284
|
+
"\n #{cyan("┌─ #{title} #{dashes}┐")}\n\n"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def prompt_default(default)
|
|
288
|
+
$stdout.print " #{cyan("›")} #{yellow("[#{default}]")}: "
|
|
289
|
+
$stdout.flush
|
|
290
|
+
result = $stdin.gets.to_s.strip
|
|
291
|
+
result.empty? ? default.to_s : result
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def confirm(question)
|
|
295
|
+
$stdout.print question
|
|
296
|
+
$stdout.flush
|
|
297
|
+
$stdin.gets.to_s.strip.downcase.start_with?("y")
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# =========================================================================
|
|
301
|
+
# ANSI colors
|
|
302
|
+
# =========================================================================
|
|
303
|
+
|
|
304
|
+
def cyan(t) = "\e[36m#{t}\e[0m"
|
|
305
|
+
def green(t) = "\e[32m#{t}\e[0m"
|
|
306
|
+
def yellow(t) = "\e[33m#{t}\e[0m"
|
|
307
|
+
|
|
308
|
+
# =========================================================================
|
|
309
|
+
# ASCII logo
|
|
310
|
+
# =========================================================================
|
|
311
|
+
|
|
312
|
+
LOGOS = <<~'LOGO'
|
|
313
|
+
|
|
314
|
+
██████╗ ███████╗███████╗██████╗ ██████╗ ███╗ ██╗██████╗ ██████╗
|
|
315
|
+
██╔══██╗██╔════╝██╔════╝██╔══██╗██╔═══██╗████╗ ██║██╔══██╗██╔═══██╗
|
|
316
|
+
██████╔╝█████╗ ███████╗██████╔╝██║ ██║██╔██╗ ██║██║ ██║██║ ██║
|
|
317
|
+
██╔══██╗██╔══╝ ╚════██║██╔═══╝ ██║ ██║██║╚██╗██║██║ ██║██║ ██║
|
|
318
|
+
██║ ██║███████╗███████║██║ ╚██████╔╝██║ ╚████║██████╔╝╚██████╔╝
|
|
319
|
+
╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═════╝ ╚═════╝
|
|
320
|
+
|
|
321
|
+
Smart JSON API Response Formatter for Rails
|
|
322
|
+
─── v#{Respondo::VERSION} ───
|
|
323
|
+
|
|
324
|
+
LOGO
|
|
325
|
+
|
|
326
|
+
def logo_with_version
|
|
327
|
+
green(<<~LOGO)
|
|
328
|
+
|
|
329
|
+
██████╗ ███████╗███████╗██████╗ ██████╗ ███╗ ██╗██████╗ ██████╗
|
|
330
|
+
██╔══██╗██╔════╝██╔════╝██╔══██╗██╔═══██╗████╗ ██║██╔══██╗██╔═══██╗
|
|
331
|
+
██████╔╝█████╗ ███████╗██████╔╝██║ ██║██╔██╗ ██║██║ ██║██║ ██║
|
|
332
|
+
██╔══██╗██╔══╝ ╚════██║██╔═══╝ ██║ ██║██║╚██╗██║██║ ██║██║ ██║
|
|
333
|
+
██║ ██║███████╗███████║██║ ╚██████╔╝██║ ╚████║██████╔╝╚██████╔╝
|
|
334
|
+
╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═════╝ ╚═════╝
|
|
335
|
+
|
|
336
|
+
Smart JSON API Response Formatter for Rails
|
|
337
|
+
─── v#{Respondo::VERSION} ───
|
|
338
|
+
|
|
339
|
+
LOGO
|
|
340
|
+
end
|
|
341
|
+
# =========================================================================
|
|
342
|
+
# Simple line buffer for building file content
|
|
343
|
+
# =========================================================================
|
|
344
|
+
|
|
345
|
+
class Lines
|
|
346
|
+
def initialize = (@buf = [])
|
|
347
|
+
def <<(str) = @buf << str
|
|
348
|
+
def to_s = @buf.join("\n") + "\n"
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
352
|
end
|
data/lib/respondo/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: respondo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.1.
|
|
4
|
+
version: 2.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- shailendra Kumar
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|