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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c72fa46b09dc6370fd885748d78c0a4cc878738fb994d43609edab1af1bee637
4
- data.tar.gz: 47a068b005b488305b4bf30a096a38446015643a6650a5c71e7042fe5d49927d
3
+ metadata.gz: eac53474dd61508e20d695e46cc364e3bf9ebdb0b89a6e1b4ae93eda15ebc7b7
4
+ data.tar.gz: 8f1035182c1795269fe3266473bdf4b67bcfa56fcdaf10d6593c5e56739a955c
5
5
  SHA512:
6
- metadata.gz: ed33945bff899f32f5f599bfeac0ed90906acafe2ddaa0ef8f19f8a1c24699839b9df78bcb033174e79bea720161bd3aa1a5841353193463ab69bbf329b2a9f0
7
- data.tar.gz: ff5b519754b9ec2145feaf1611cd7ad29cfc2291f369781e131e175029025d507ad62fef92b4a6057ea2abf92cf77f3bacf6ae778b0f460f94fc6a810af76ce6
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
- **Consistent JSON API responses for Rails — in one line.**
5
+ Consistent JSON API responses for Rails, in one line.
6
6
 
7
7
  [![Gem Version](https://badge.fury.io/rb/respondo.svg)](https://rubygems.org/gems/respondo)
8
8
  [![Downloads](https://img.shields.io/gem/dt/respondo.svg)](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 are writing `if (res.data || res.result || res.users)`. Your Flutter devs are filing bugs. Your code reviews are arguments.
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
- **Respondo fixes this permanently.** One response shape. Every controller. Every developer. Every time.
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`. No boilerplate. Respondo auto-injects via Railtie.
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
- Your frontend now gets a **guaranteed** structure — forever.
90
+ Now your frontend can count on getting the same shape back every time.
91
91
 
92
92
  ---
93
93
 
94
- ## Why teams switch to Respondo
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` — done |
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 — success or error — has the same four keys:
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-Serialization
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
- Respondo covers every HTTP status code. Here are the helpers you'll use every day:
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 preserve consistent structure.
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 you an hour, give it a ⭐ — it helps other developers find it.</sub>
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/your-org/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
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Respondo
4
- VERSION = "2.1.2"
4
+ VERSION = "2.1.3"
5
5
  end
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.2
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-05-13 00:00:00.000000000 Z
11
+ date: 2026-10-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties