respondo 1.0.0 → 2.1.0

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.
@@ -0,0 +1,350 @@
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
+ b = Lines.new
192
+
193
+ b << "# frozen_string_literal: true"
194
+ b << ""
195
+ b << "# Respondo initializer — #{@cfg[:project_name]}"
196
+ b << "# Generated by: rails generate respondo:install"
197
+ b << "# Respondo version: #{Respondo::VERSION}"
198
+ b << "# Docs: https://github.com/your-org/respondo"
199
+ b << ""
200
+ b << "Respondo.configure do |config|"
201
+ b << ""
202
+ b << " # ── Messages ─────────────────────────────────────────────────────────"
203
+ b << " # Fallback when render_success / render_error is called without message:"
204
+ b << " config.default_success_message = #{@cfg[:default_success_message].inspect}"
205
+ b << " config.default_error_message = #{@cfg[:default_error_message].inspect}"
206
+ b << ""
207
+ b << " # ── Request ID ───────────────────────────────────────────────────────"
208
+ b << " # Includes Rails request.request_id in every response meta block."
209
+ b << " config.include_request_id = #{@cfg[:include_request_id]}"
210
+ b << ""
211
+ b << " # ── Key Format ───────────────────────────────────────────────────────"
212
+ b << " # CamelCase all JSON keys — recommended for Flutter / JS clients."
213
+ b << " config.camelize_keys = #{@cfg[:camelize_keys]}"
214
+ b << ""
215
+ b << " # ── Global Meta ──────────────────────────────────────────────────────"
216
+ b << " # Static fields merged into the meta block of every response."
217
+
218
+ if meta.empty?
219
+ b << " config.default_meta = {}"
220
+ else
221
+ b << " config.default_meta = {"
222
+ meta.each_with_index do |(k, v), i|
223
+ comma = i < meta.size - 1 ? "," : ""
224
+ b << " #{k}: #{v.inspect}#{comma}"
225
+ end
226
+ b << " }"
227
+ end
228
+
229
+ if @cfg[:custom_serializer]
230
+ b << ""
231
+ b << " # ── Custom Serializer ────────────────────────────────────────────────"
232
+ b << " # Replace the lambda body with your own serialization logic."
233
+ b << " # Examples:"
234
+ b << " # ActiveModelSerializers: ->(obj) { SomeSerializer.new(obj).as_json }"
235
+ b << " # Blueprinter: ->(obj) { UserBlueprint.render_as_hash(obj) }"
236
+ b << " #"
237
+ b << " # config.serializer = ->(obj) { MySerializer.new(obj).as_json }"
238
+ end
239
+
240
+ b << ""
241
+ b << "end"
242
+ b << ""
243
+ b.to_s
244
+ end
245
+
246
+ # =========================================================================
247
+ # Done
248
+ # =========================================================================
249
+
250
+ def print_done
251
+ out divider
252
+ out blank
253
+ out line(cyan(" 🎉 Respondo is ready!"))
254
+ out blank
255
+ out line(" Next steps:")
256
+ out line(" 1. Review config/initializers/respondo.rb")
257
+ out line(" 2. Use render_success / render_error in your controllers")
258
+ out line(" 3. Re-run rails generate respondo:install to regenerate")
259
+ out blank
260
+ out divider
261
+ out blank
262
+ end
263
+
264
+ # =========================================================================
265
+ # Output primitives — all output goes through $stdout, never through Thor
266
+ # =========================================================================
267
+
268
+ def out(str)
269
+ $stdout.print str
270
+ $stdout.flush
271
+ end
272
+
273
+ def line(str) = "#{str}\n"
274
+ def blank = "\n"
275
+
276
+ def divider
277
+ " #{cyan("─" * 68)}\n"
278
+ end
279
+
280
+ def section(title)
281
+ dashes = "─" * [0, 54 - title.length].max
282
+ "\n #{cyan("┌─ #{title} #{dashes}┐")}\n\n"
283
+ end
284
+
285
+ def prompt_default(default)
286
+ $stdout.print " #{cyan("›")} #{yellow("[#{default}]")}: "
287
+ $stdout.flush
288
+ result = $stdin.gets.to_s.strip
289
+ result.empty? ? default.to_s : result
290
+ end
291
+
292
+ def confirm(question)
293
+ $stdout.print question
294
+ $stdout.flush
295
+ $stdin.gets.to_s.strip.downcase.start_with?("y")
296
+ end
297
+
298
+ # =========================================================================
299
+ # ANSI colors
300
+ # =========================================================================
301
+
302
+ def cyan(t) = "\e[36m#{t}\e[0m"
303
+ def green(t) = "\e[32m#{t}\e[0m"
304
+ def yellow(t) = "\e[33m#{t}\e[0m"
305
+
306
+ # =========================================================================
307
+ # ASCII logo
308
+ # =========================================================================
309
+
310
+ LOGOS = <<~'LOGO'
311
+
312
+ ██████╗ ███████╗███████╗██████╗ ██████╗ ███╗ ██╗██████╗ ██████╗
313
+ ██╔══██╗██╔════╝██╔════╝██╔══██╗██╔═══██╗████╗ ██║██╔══██╗██╔═══██╗
314
+ ██████╔╝█████╗ ███████╗██████╔╝██║ ██║██╔██╗ ██║██║ ██║██║ ██║
315
+ ██╔══██╗██╔══╝ ╚════██║██╔═══╝ ██║ ██║██║╚██╗██║██║ ██║██║ ██║
316
+ ██║ ██║███████╗███████║██║ ╚██████╔╝██║ ╚████║██████╔╝╚██████╔╝
317
+ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═════╝ ╚═════╝
318
+
319
+ Smart JSON API Response Formatter for Rails
320
+ ─── v#{Respondo::VERSION} ───
321
+
322
+ LOGO
323
+
324
+ def logo_with_version
325
+ green(<<~LOGO)
326
+
327
+ ██████╗ ███████╗███████╗██████╗ ██████╗ ███╗ ██╗██████╗ ██████╗
328
+ ██╔══██╗██╔════╝██╔════╝██╔══██╗██╔═══██╗████╗ ██║██╔══██╗██╔═══██╗
329
+ ██████╔╝█████╗ ███████╗██████╔╝██║ ██║██╔██╗ ██║██║ ██║██║ ██║
330
+ ██╔══██╗██╔══╝ ╚════██║██╔═══╝ ██║ ██║██║╚██╗██║██║ ██║██║ ██║
331
+ ██║ ██║███████╗███████║██║ ╚██████╔╝██║ ╚████║██████╔╝╚██████╔╝
332
+ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═════╝ ╚═════╝
333
+
334
+ Smart JSON API Response Formatter for Rails
335
+ ─── v#{Respondo::VERSION} ───
336
+
337
+ LOGO
338
+ end
339
+ # =========================================================================
340
+ # Simple line buffer for building file content
341
+ # =========================================================================
342
+
343
+ class Lines
344
+ def initialize = (@buf = [])
345
+ def <<(str) = @buf << str
346
+ def to_s = @buf.join("\n") + "\n"
347
+ end
348
+ end
349
+ end
350
+ end