respondo 0.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,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Respondo
4
+ # Mixed into Rails controllers to provide render_success and render_error.
5
+ #
6
+ # Success helpers (2xx):
7
+ # render_success, render_created, render_accepted, render_no_content,
8
+ # render_partial_content, render_multi_status
9
+ #
10
+ # Client error helpers (4xx):
11
+ # render_bad_request, render_unauthorized, render_payment_required,
12
+ # render_forbidden, render_not_found, render_method_not_allowed,
13
+ # render_not_acceptable, render_conflict, render_gone,
14
+ # render_unprocessable, render_too_many_requests, render_locked,
15
+ # render_precondition_failed, render_unsupported_media_type,
16
+ # render_request_timeout
17
+ #
18
+ # Server error helpers (5xx):
19
+ # render_server_error, render_not_implemented, render_bad_gateway,
20
+ # render_service_unavailable, render_gateway_timeout
21
+ module ControllerHelpers
22
+
23
+ # =========================================================================
24
+ # Core DSL — all helpers delegate to these two
25
+ # =========================================================================
26
+
27
+ # Render a successful JSON response.
28
+ #
29
+ # @param data [Object] payload — AR model, collection, Hash, Array, nil
30
+ # @param message [String] human-readable description
31
+ # @param meta [Hash] extra meta fields merged into the meta block
32
+ # @param pagy [Pagy] optional Pagy object (pass when using Pagy backend)
33
+ # @param pagination [Boolean] true = include pagination meta when available (default)
34
+ # false = always suppress pagination meta
35
+ # @param status [Symbol, Integer] HTTP status (default: :ok / 200)
36
+ def render_success(data: nil, message: nil, meta: {}, code: nil, pagy: nil, pagination: true, status: :ok)
37
+ merged_meta = code ? meta.merge(code: code, status: status) : meta
38
+
39
+ payload = ResponseBuilder.new(
40
+ success: true,
41
+ data: data,
42
+ message: message,
43
+ meta: merged_meta,
44
+ pagy: pagy,
45
+ pagination: pagination,
46
+ request: try(:request)
47
+ ).build
48
+
49
+ render json: payload, status: status
50
+ end
51
+
52
+ # Render an error JSON response.
53
+ #
54
+ # @param message [String] human-readable error description
55
+ # @param errors [Hash, ActiveModel::Errors] field-level validation errors
56
+ # @param code [String, nil] machine-readable error code e.g. "AUTH_EXPIRED"
57
+ # @param meta [Hash] extra meta fields
58
+ # @param status [Symbol, Integer] HTTP status (default: :unprocessable_entity / 422)
59
+
60
+ def render_error(message: nil, errors: nil, code: nil, meta: {}, status: :unprocessable_entity)
61
+ extracted_errors = extract_errors(errors)
62
+ merged_meta = code ? meta.merge(code: code, status: status) : meta
63
+
64
+ payload = ResponseBuilder.new(
65
+ success: false,
66
+ data: nil,
67
+ message: message,
68
+ meta: merged_meta,
69
+ errors: extracted_errors,
70
+ pagination: false,
71
+ request: try(:request)
72
+ ).build
73
+
74
+ render json: payload, status: status
75
+ end
76
+
77
+ # =========================================================================
78
+ # 2xx Success helpers
79
+ # =========================================================================
80
+
81
+ # 200 OK — alias for render_success with no pagination (single record)
82
+ def render_ok(data: nil, message: nil, meta: {}, pagination: true)
83
+ render_success(data: data, message: message, meta: meta, pagination: pagination, code:200, status: :ok)
84
+ end
85
+
86
+ # 201 Created
87
+ def render_created(data: nil, message: "Created successfully", pagination: false)
88
+ render_success(data: data, message: message, pagination: pagination, code:201, status: :created)
89
+ end
90
+
91
+ # 202 Accepted — async jobs, background processing
92
+ def render_accepted(data: nil, message: "Request accepted and is being processed")
93
+ render_success(data: data, message: message, pagination: false, code:202, status: :accepted)
94
+ end
95
+
96
+ # 204 No Content — deletions, actions with no response body
97
+ # Note: we still return our standard JSON structure for consistency
98
+ def render_no_content(message: "Deleted successfully")
99
+ render_success(data: nil, message: message, pagination: false, code:204, status: :ok)
100
+ end
101
+
102
+ # 206 Partial Content — chunked / range responses
103
+ def render_partial_content(data: nil, message: "Partial content returned", meta: {})
104
+ render_success(data: data, message: message, meta: meta, pagination: false, code:206, status: :partial_content)
105
+ end
106
+
107
+ # 207 Multi-Status — batch operations with mixed results
108
+ def render_multi_status(data: nil, message: "Multi-status response", meta: {})
109
+ render_success(data: data, message: message, meta: meta, pagination: false, code:207, status: :multi_status)
110
+ end
111
+
112
+ # =========================================================================
113
+ # 4xx Client error helpers
114
+ # =========================================================================
115
+
116
+ # 400 Bad Request — malformed request, invalid params
117
+ def render_bad_request(message: "Bad request", errors: nil, code: "BAD_REQUEST")
118
+ render_error(message: message, errors: errors, code: code, status: :bad_request)
119
+ end
120
+
121
+ # 401 Unauthorized — not authenticated
122
+ def render_unauthorized(message: "Unauthorized", errors: nil, code: "UNAUTHORIZED")
123
+ render_error(message: message, errors: errors, code: code, status: :unauthorized)
124
+ end
125
+
126
+ # 402 Payment Required — paywalled features
127
+ def render_payment_required(message: "Payment required to access this resource", errors: nil, code: "PAYMENT_REQUIRED")
128
+ render_error(message: message, errors: errors, code: code, status: :payment_required)
129
+ end
130
+
131
+ # 403 Forbidden — authenticated but not authorized
132
+ def render_forbidden(message: "You do not have permission to perform this action", errors: nil, code: "FORBIDDEN")
133
+ render_error(message: message, errors: errors, code: code, status: :forbidden)
134
+ end
135
+
136
+ # 404 Not Found
137
+ def render_not_found(message: "Resource not found", errors: nil, code: "NOT_FOUND")
138
+ render_error(message: message, errors: errors, code: code, status: :not_found)
139
+ end
140
+
141
+ # 405 Method Not Allowed
142
+ def render_method_not_allowed(message: "HTTP method not allowed", errors: nil, code: "METHOD_NOT_ALLOWED")
143
+ render_error(message: message, errors: errors, code: code, status: :method_not_allowed)
144
+ end
145
+
146
+ # 406 Not Acceptable — client Accept header can't be satisfied
147
+ def render_not_acceptable(message: "Requested format not acceptable", errors: nil, code: "NOT_ACCEPTABLE")
148
+ render_error(message: message, errors: errors, code: code, status: :not_acceptable)
149
+ end
150
+
151
+ # 408 Request Timeout
152
+ def render_request_timeout(message: "Request timed out", errors: nil, code: "REQUEST_TIMEOUT")
153
+ render_error(message: message, errors: errors, code: code, status: :request_timeout)
154
+ end
155
+
156
+ # 409 Conflict — duplicate record, state conflict
157
+ def render_conflict(message: "Resource conflict", errors: nil, code: "CONFLICT")
158
+ render_error(message: message, errors: errors, code: code, status: :conflict)
159
+ end
160
+
161
+ # 410 Gone — resource permanently deleted
162
+ def render_gone(message: "Resource no longer available", errors: nil, code: "GONE")
163
+ render_error(message: message, errors: errors, code: code, status: :gone)
164
+ end
165
+
166
+ # 412 Precondition Failed — conditional request failed
167
+ def render_precondition_failed(message: "Precondition failed", errors: nil, code: "PRECONDITION_FAILED")
168
+ render_error(message: message, errors: errors, code: code, status: :precondition_failed)
169
+ end
170
+
171
+ # 415 Unsupported Media Type — wrong Content-Type header
172
+ def render_unsupported_media_type(message: "Unsupported media type", errors: nil, code: "UNSUPPORTED_MEDIA_TYPE")
173
+ render_error(message: message, errors: errors, code: code, status: :unsupported_media_type)
174
+ end
175
+
176
+ # 422 Unprocessable Entity — validation errors (most common for APIs)
177
+ def render_unprocessable(message: "Validation failed", errors: nil, code: "UNPROCESSABLE_ENTITY")
178
+ render_error(message: message, errors: errors, code: code, status: :unprocessable_entity)
179
+ end
180
+
181
+ # 423 Locked — resource is locked
182
+ def render_locked(message: "Resource is locked", errors: nil, code: "LOCKED")
183
+ render_error(message: message, errors: errors, code: code, status: :locked)
184
+ end
185
+
186
+ # 429 Too Many Requests — rate limiting
187
+ def render_too_many_requests(message: "Too many requests. Please slow down.", errors: nil, code: "RATE_LIMITED")
188
+ render_error(message: message, errors: errors, code: code, status: :too_many_requests)
189
+ end
190
+
191
+ # =========================================================================
192
+ # 5xx Server error helpers
193
+ # =========================================================================
194
+
195
+ # 500 Internal Server Error
196
+ def render_server_error(message: "An unexpected error occurred", errors: nil, code: "SERVER_ERROR")
197
+ render_error(message: message, errors: errors, code: code, status: :internal_server_error)
198
+ end
199
+
200
+ # 501 Not Implemented — feature not built yet
201
+ def render_not_implemented(message: "This feature is not yet implemented", errors: nil, code: "NOT_IMPLEMENTED")
202
+ render_error(message: message, errors: errors, code: code, status: :not_implemented)
203
+ end
204
+
205
+ # 502 Bad Gateway — upstream service failed
206
+ def render_bad_gateway(message: "Bad gateway — upstream service error", errors: nil, code: "BAD_GATEWAY")
207
+ render_error(message: message, errors: errors, code: code, status: :bad_gateway)
208
+ end
209
+
210
+ # 503 Service Unavailable — maintenance, overloaded
211
+ def render_service_unavailable(message: "Service temporarily unavailable", errors: nil, code: "SERVICE_UNAVAILABLE")
212
+ render_error(message: message, errors: errors, code: code, status: :service_unavailable)
213
+ end
214
+
215
+ # 504 Gateway Timeout — upstream service timed out
216
+ def render_gateway_timeout(message: "Gateway timeout — upstream service did not respond", errors: nil, code: "GATEWAY_TIMEOUT")
217
+ render_error(message: message, errors: errors, code: code, status: :gateway_timeout)
218
+ end
219
+
220
+ private
221
+
222
+ # Normalize errors into a plain Hash regardless of source type.
223
+ def extract_errors(errors)
224
+ return nil if errors.nil?
225
+
226
+ if defined?(ActiveModel::Errors) && errors.is_a?(ActiveModel::Errors)
227
+ return errors.to_hash
228
+ end
229
+
230
+ return errors if errors.is_a?(Hash)
231
+
232
+ if errors.is_a?(Array)
233
+ return { base: errors }
234
+ end
235
+
236
+ if errors.is_a?(String)
237
+ return { base: [errors] }
238
+ end
239
+
240
+ nil
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Respondo
4
+ # Extracts pagination metadata from Kaminari or Pagy collection objects.
5
+ #
6
+ # Returned hash shape (always the same regardless of pagination library):
7
+ # {
8
+ # current_page: Integer,
9
+ # per_page: Integer,
10
+ # total_pages: Integer,
11
+ # total_count: Integer,
12
+ # next_page: Integer | nil,
13
+ # prev_page: Integer | nil
14
+ # }
15
+ module Pagination
16
+ module_function
17
+
18
+ # @param collection [Object] any object — returns nil if not a paginated collection
19
+ # @return [Hash, nil]
20
+ def extract(collection)
21
+ return nil if collection.nil?
22
+
23
+ if pagy?(collection)
24
+ from_pagy(collection)
25
+ elsif kaminari?(collection)
26
+ from_kaminari(collection)
27
+ elsif will_paginate?(collection)
28
+ from_will_paginate(collection)
29
+ else
30
+ nil
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ module_function
37
+
38
+ # ----- Pagy ---------------------------------------------------------------
39
+ # Pagy stores metadata on a separate Pagy object, not the collection itself.
40
+ # We support both: passing the Pagy object directly, or a collection that
41
+ # has been decorated with pagy metadata via pagy_metadata.
42
+ def pagy?(object)
43
+ defined?(Pagy) && object.is_a?(Pagy)
44
+ end
45
+
46
+ def from_pagy(pagy)
47
+ {
48
+ current_page: pagy.page,
49
+ per_page: pagy.items,
50
+ total_pages: pagy.pages,
51
+ total_count: pagy.count,
52
+ next_page: pagy.next,
53
+ prev_page: pagy.prev
54
+ }
55
+ end
56
+
57
+ # ----- Kaminari -----------------------------------------------------------
58
+ def kaminari?(object)
59
+ object.respond_to?(:current_page) &&
60
+ object.respond_to?(:total_pages) &&
61
+ object.respond_to?(:limit_value)
62
+ end
63
+
64
+ def from_kaminari(collection)
65
+ {
66
+ current_page: collection.current_page,
67
+ per_page: collection.limit_value,
68
+ total_pages: collection.total_pages,
69
+ total_count: collection.total_count,
70
+ next_page: collection.next_page,
71
+ prev_page: collection.prev_page
72
+ }
73
+ end
74
+
75
+ # ----- WillPaginate -------------------------------------------------------
76
+ def will_paginate?(object)
77
+ object.respond_to?(:current_page) &&
78
+ object.respond_to?(:total_pages) &&
79
+ object.respond_to?(:per_page) &&
80
+ !object.respond_to?(:limit_value) # distinguishes from Kaminari
81
+ end
82
+
83
+ def from_will_paginate(collection)
84
+ {
85
+ current_page: collection.current_page,
86
+ per_page: collection.per_page,
87
+ total_pages: collection.total_pages,
88
+ total_count: collection.total_entries,
89
+ next_page: collection.next_page,
90
+ prev_page: collection.previous_page
91
+ }
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Respondo
4
+ # Railtie automatically includes Respondo::ControllerHelpers into
5
+ # ActionController::Base and ActionController::API when Rails is present.
6
+ # No manual include needed in ApplicationController.
7
+ class Railtie < Rails::Railtie
8
+ initializer "respondo.include_controller_helpers" do
9
+ ActiveSupport.on_load(:action_controller_base) do
10
+ include Respondo::ControllerHelpers
11
+ end
12
+
13
+ ActiveSupport.on_load(:action_controller_api) do
14
+ include Respondo::ControllerHelpers
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Respondo
6
+ # Builds the standardized response hash.
7
+ #
8
+ # Every response always contains these top-level keys:
9
+ # {
10
+ # success: Boolean,
11
+ # message: String,
12
+ # data: Object | Array | nil,
13
+ # meta: Hash
14
+ # }
15
+ #
16
+ # The meta block always contains:
17
+ # { timestamp: ISO8601 String }
18
+ # Plus pagination keys when pagination: true and the data is a paginated collection.
19
+ # Plus request_id when config.include_request_id is true.
20
+ class ResponseBuilder
21
+ # @param success [Boolean]
22
+ # @param data [Object] anything — serialized automatically
23
+ # @param message [String]
24
+ # @param meta [Hash] caller-supplied extra meta (merged in)
25
+ # @param errors [Hash] field-level errors (for 422 responses)
26
+ # @param pagy [Pagy] optional Pagy object for pagination meta
27
+ # @param pagination [Boolean] true = include pagination meta (default),
28
+ # false = always suppress pagination meta
29
+ # @param request [Object] ActionDispatch::Request (for request_id)
30
+ def initialize(success:, data: nil, message: nil, meta: {}, errors: nil,
31
+ pagy: nil, pagination: true, request: nil)
32
+ @success = success
33
+ @raw_data = data
34
+ @message = message
35
+ @extra_meta = meta || {}
36
+ @errors = errors
37
+ @pagy = pagy
38
+ @pagination = pagination
39
+ @request = request
40
+ end
41
+
42
+ # @return [Hash] the complete response payload
43
+ def build
44
+ # We initialize the hash in the exact order we want keys to appear
45
+ payload = {
46
+ success: @success,
47
+ message: resolve_message,
48
+ data: serialize_data,
49
+ }
50
+
51
+ # Add errors before meta if they exist
52
+ payload[:errors] = @errors if @errors && !@errors.empty?
53
+
54
+ # Finally, add meta so it appears at the bottom
55
+ payload[:meta] = build_meta
56
+
57
+ apply_camelize(payload)
58
+ end
59
+
60
+ private
61
+
62
+ def resolve_message
63
+ return @message if @message && !@message.empty?
64
+ @success ? Respondo.config.default_success_message : Respondo.config.default_error_message
65
+ end
66
+
67
+ def serialize_data
68
+ Serializer.call(@raw_data)
69
+ end
70
+
71
+ def build_meta
72
+ meta = { timestamp: current_timestamp }
73
+
74
+ # Only extract pagination when caller has not explicitly disabled it
75
+ if @pagination
76
+ pagination = if @pagy
77
+ Pagination.extract(@pagy)
78
+ else
79
+ Pagination.extract(@raw_data)
80
+ end
81
+ meta[:pagination] = pagination if pagination
82
+ end
83
+
84
+ # Request ID (Rails only, opt-in via config)
85
+ if Respondo.config.include_request_id && @request&.respond_to?(:request_id)
86
+ meta[:request_id] = @request.request_id
87
+ end
88
+
89
+ # Merge any caller-supplied meta last (allows overriding)
90
+ meta.merge(@extra_meta)
91
+ end
92
+
93
+ def current_timestamp
94
+ if defined?(Time.current)
95
+ Time.current.iso8601
96
+ else
97
+ Time.now.utc.iso8601
98
+ end
99
+ end
100
+
101
+ def apply_camelize(hash)
102
+ return hash unless Respondo.config.camelize_keys
103
+ camelize_hash(hash)
104
+ end
105
+
106
+ def camelize_hash(obj)
107
+ case obj
108
+ when Hash
109
+ obj.each_with_object({}) do |(k, v), memo|
110
+ memo[camelize_key(k)] = camelize_hash(v)
111
+ end
112
+ when Array
113
+ obj.map { |item| camelize_hash(item) }
114
+ else
115
+ obj
116
+ end
117
+ end
118
+
119
+ def camelize_key(key)
120
+ key.to_s.gsub(/_([a-z])/) { $1.upcase }.to_sym
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Respondo
4
+ # Responsible for converting any Ruby object into a JSON-safe Hash or Array.
5
+ #
6
+ # Priority order:
7
+ # 1. Custom serializer from Respondo.config (if set)
8
+ # 2. ActiveRecord::Base / ActiveRecord::Relation
9
+ # 3. ActiveModel::Errors / objects responding to #errors
10
+ # 4. Objects responding to #as_json or #to_h
11
+ # 5. Arrays — each element is serialized recursively
12
+ # 6. Primitives — returned as-is
13
+ module Serializer
14
+ module_function
15
+
16
+ # @param object [Object] anything — AR model, collection, error, hash, array, primitive
17
+ # @return [Hash, Array, String, Numeric, nil]
18
+ def call(object)
19
+ return nil if object.nil?
20
+
21
+ # 1. Custom serializer wins
22
+ if Respondo.config.serializer
23
+ return Respondo.config.serializer.call(object)
24
+ end
25
+
26
+ # 2. ActiveRecord::Relation or any object with #map (lazy collections)
27
+ if ar_relation?(object)
28
+ return object.map { |record| serialize_record(record) }
29
+ end
30
+
31
+ # 3. Array — recurse
32
+ if object.is_a?(Array)
33
+ return object.map { |item| call(item) }
34
+ end
35
+
36
+ # 4. ActiveRecord model instance
37
+ if ar_model?(object)
38
+ return serialize_record(object)
39
+ end
40
+
41
+ # 5. ActiveModel::Errors object
42
+ if active_model_errors?(object)
43
+ return serialize_errors(object)
44
+ end
45
+
46
+ # 6. Exception / StandardError
47
+ if object.is_a?(Exception)
48
+ return { message: object.message }
49
+ end
50
+
51
+ # 7. Hash — recursively serialize values
52
+ if object.is_a?(Hash)
53
+ return object.transform_values { |v| call(v) }
54
+ end
55
+
56
+ # 8. Responds to #as_json (ActiveSupport)
57
+ if object.respond_to?(:as_json)
58
+ return object.as_json
59
+ end
60
+
61
+ # 9. Responds to #to_h
62
+ if object.respond_to?(:to_h)
63
+ return object.to_h
64
+ end
65
+
66
+ # 10. Primitive — return as-is
67
+ object
68
+ end
69
+
70
+ private
71
+
72
+ module_function
73
+
74
+ def ar_relation?(object)
75
+ defined?(ActiveRecord::Relation) &&
76
+ object.is_a?(ActiveRecord::Relation)
77
+ end
78
+
79
+ def ar_model?(object)
80
+ defined?(ActiveRecord::Base) &&
81
+ object.is_a?(ActiveRecord::Base)
82
+ end
83
+
84
+ def active_model_errors?(object)
85
+ defined?(ActiveModel::Errors) &&
86
+ object.is_a?(ActiveModel::Errors)
87
+ end
88
+
89
+ def serialize_record(record)
90
+ if record.respond_to?(:as_json)
91
+ record.as_json
92
+ elsif record.respond_to?(:to_h)
93
+ record.to_h
94
+ else
95
+ record
96
+ end
97
+ end
98
+
99
+ def serialize_errors(errors)
100
+ # ActiveModel::Errors — return { field: ["msg", ...] }
101
+ if errors.respond_to?(:to_hash)
102
+ errors.to_hash
103
+ elsif errors.respond_to?(:messages)
104
+ errors.messages
105
+ else
106
+ {}
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Respondo
4
+ VERSION = "0.1.0"
5
+ end
data/lib/respondo.rb ADDED
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "respondo/version"
4
+ require_relative "respondo/configuration"
5
+ require_relative "respondo/serializer"
6
+ require_relative "respondo/pagination"
7
+ require_relative "respondo/response_builder"
8
+ require_relative "respondo/controller_helpers"
9
+
10
+ # Respondo — Smart JSON API response formatter for Rails.
11
+ #
12
+ # @example Configure once in an initializer
13
+ # # config/initializers/respondo.rb
14
+ # Respondo.configure do |config|
15
+ # config.default_success_message = "OK"
16
+ # config.default_error_message = "Something went wrong"
17
+ # config.include_request_id = true
18
+ # config.camelize_keys = true # for Flutter/JS clients
19
+ # end
20
+ #
21
+ # @example Include manually (without Railtie / outside Rails)
22
+ # class MyController
23
+ # include Respondo::ControllerHelpers
24
+ # end
25
+ module Respondo
26
+ class << self
27
+ # @return [Respondo::Configuration]
28
+ def config
29
+ @config ||= Configuration.new
30
+ end
31
+
32
+ # @yield [Respondo::Configuration]
33
+ def configure
34
+ yield config
35
+ end
36
+
37
+ # Reset config (useful in tests)
38
+ def reset!
39
+ @config = Configuration.new
40
+ end
41
+ end
42
+ end
43
+
44
+ # Auto-integrate with Rails if present
45
+ require_relative "respondo/railtie" if defined?(Rails::Railtie)
data/respondo.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/respondo/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "respondo"
7
+ spec.version = Respondo::VERSION
8
+ spec.authors = ["shailendra Kumar"]
9
+ spec.email = ["shailendrapatidar00@gmail.com"]
10
+
11
+ spec.summary = "Smart JSON API response formatter for Rails — consistent structure every time."
12
+ spec.description = <<~DESC
13
+ Respondo standardizes JSON API responses across Rails applications.
14
+ Every response gets success, data, message, and meta fields.
15
+ Automatic pagination meta for Kaminari and Pagy collections.
16
+ ActiveRecord serialization, error extraction, and flexible HTTP codes built in.
17
+ DESC
18
+ spec.homepage = "https://github.com/spatelpatidar/respondo"
19
+ spec.license = "MIT"
20
+ spec.required_ruby_version = ">= 2.7.0"
21
+
22
+ spec.files = Dir["lib/**/*.rb", "README.md", "LICENSE.txt", "CHANGELOG.md", "respondo.gemspec"]
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_development_dependency "rspec", "~> 3.12"
26
+ spec.add_development_dependency "rake", "~> 13.0"
27
+ spec.add_development_dependency "activesupport","~> 7.0"
28
+ spec.add_development_dependency 'simplecov', '~> 0.22'
29
+ end