rails_api_kit 1.0.0 → 1.0.1
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/LICENSE.txt +1 -1
- data/README.md +1 -3
- data/lib/api_kit/fetching.rb +1 -3
- data/lib/api_kit/rails_app.rb +71 -11
- data/lib/api_kit/version.rb +1 -1
- data/spec/dummy.rb +57 -0
- data/spec/sparse_fields_type_key_spec.rb +87 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3c6f2c6b595530d2b1c63ad4a4e582c83e2555e04bc26c4d5e5c0e31e879fd34
|
|
4
|
+
data.tar.gz: 4c1700476844577653e7311cf315c1eaeca1c7eb82b1a33cd0dedd646e330311
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c998192b4f161fb2cb4cd7f87253f94feb9a83e05b1e30e0f70a9e533130e2f0864da68205f9babbac0c8c8b26319606ef3567008ca25499e00b51f40566c0f
|
|
7
|
+
data.tar.gz: 05c8ffa12577ce6980e98750ff4e6050276c6b4cd5d4a7aaef2e2a24c693f470e39263106b0c96b28541fdd6dfe8959a15454553f61dd5a54a6088ff427efb70
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
#
|
|
1
|
+
# RailsApiKit
|
|
2
2
|
|
|
3
3
|
A lightweight Rails toolkit for building standardized, structured API responses with serialization, error handling, filtering, sorting, and pagination.
|
|
4
4
|
|
|
5
|
-
> Building clean, consistent APIs shouldn't be rocket science. ApiKit provides simple, powerful modules to get you up and running quickly.
|
|
6
|
-
|
|
7
5
|
## Features
|
|
8
6
|
|
|
9
7
|
ApiKit offers a collection of lightweight modules that integrate seamlessly with your Rails controllers:
|
data/lib/api_kit/fetching.rb
CHANGED
|
@@ -12,9 +12,7 @@ module ApiKit
|
|
|
12
12
|
|
|
13
13
|
result = []
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
model_name ||= serializer_class.name.demodulize.delete_suffix("Serializer").underscore
|
|
17
|
-
end
|
|
15
|
+
model_name ||= ApiKit::RailsApp.serializer_name(serializer_class)
|
|
18
16
|
|
|
19
17
|
keys = []
|
|
20
18
|
params[:fields].each do |k, v|
|
data/lib/api_kit/rails_app.rb
CHANGED
|
@@ -75,6 +75,7 @@ module ApiKit
|
|
|
75
75
|
options[:fields] = api_fields(serializer_class, ApiKit::RailsApp.fetch_name(many, resource))
|
|
76
76
|
options[:adapter] = :attributes
|
|
77
77
|
options[:each_serializer] = serializer_class
|
|
78
|
+
ApiKit::RailsApp.assign_collection_root!(options, resource, serializer_class) if many
|
|
78
79
|
data = ActiveModelSerializers::SerializableResource.new(resource, options).as_json
|
|
79
80
|
result[:data] = data
|
|
80
81
|
result.to_json
|
|
@@ -107,6 +108,7 @@ module ApiKit
|
|
|
107
108
|
options[:adapter] = :attributes
|
|
108
109
|
options[:each_serializer] = serializer_class
|
|
109
110
|
if many
|
|
111
|
+
ApiKit::RailsApp.assign_collection_root!(options, resource, serializer_class)
|
|
110
112
|
data = ActiveModelSerializers::SerializableResource.new(resource, options).as_json
|
|
111
113
|
else
|
|
112
114
|
data = ActiveModelSerializers::SerializableResource.new([ resource ], options).as_json[0]
|
|
@@ -137,21 +139,79 @@ module ApiKit
|
|
|
137
139
|
"#{klass.name}Serializer".constantize
|
|
138
140
|
end
|
|
139
141
|
|
|
140
|
-
# Resolves the
|
|
142
|
+
# Resolves the model name used as the sparse-fieldset type key
|
|
143
|
+
#
|
|
144
|
+
# Mirrors `ActiveModel::Serializer#json_key`, which AMS uses to look a
|
|
145
|
+
# type up in the fieldset: `object.class.model_name.to_s.underscore`.
|
|
146
|
+
# NOT `model_name.singular` — that tr()s the namespace separator to an
|
|
147
|
+
# underscore (`manufacturing_work_order`), so for a namespaced model the
|
|
148
|
+
# key never matched, the primary type went unconstrained, and AMS fell
|
|
149
|
+
# through to the pluralised collection key whose value is an empty list:
|
|
150
|
+
# every attribute of the primary resource was silently dropped.
|
|
141
151
|
#
|
|
142
152
|
# @param many [Boolean] indicates whether the resource is a collection
|
|
143
153
|
# @param resource [Object] serialized resource or collection
|
|
144
|
-
# @return [String, nil]
|
|
154
|
+
# @return [String, nil] model name when available
|
|
145
155
|
def self.fetch_name(many, resource)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
record = many ? collection_model(resource) : resource
|
|
157
|
+
model_name = record&.model_name
|
|
158
|
+
model_name && model_name.to_s.underscore
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Root key for a collection AMS cannot infer one for
|
|
162
|
+
#
|
|
163
|
+
# `AMS::CollectionSerializer#json_key` reads the root from its first
|
|
164
|
+
# element, or from a named collection (`ActiveRecord::Relation` answers
|
|
165
|
+
# `#name` through its klass). An EMPTY plain Array offers neither, so it
|
|
166
|
+
# raises `CannotInferRootKeyError` the moment sparse fieldsets are
|
|
167
|
+
# requested. Aggregation endpoints that render POROs hit exactly that.
|
|
168
|
+
#
|
|
169
|
+
# Returns nil whenever AMS can infer the key itself, so a collection that
|
|
170
|
+
# already works keeps its own root and item serializers keep their
|
|
171
|
+
# `json_key`.
|
|
172
|
+
#
|
|
173
|
+
# @param resource [Object] the collection being serialized
|
|
174
|
+
# @param serializer_class [Class, NilClass] serializer for its members
|
|
175
|
+
# @return [String, nil] pluralised root key, or nil to leave it to AMS
|
|
176
|
+
def self.collection_root(resource, serializer_class)
|
|
177
|
+
return nil unless resource.respond_to?(:empty?) && resource.empty?
|
|
178
|
+
return nil if resource.respond_to?(:name)
|
|
179
|
+
|
|
180
|
+
name = serializer_name(serializer_class)
|
|
181
|
+
name && name.pluralize
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Sets the collection root only when AMS could not work one out
|
|
185
|
+
#
|
|
186
|
+
# @param options [Hash] render options, mutated in place
|
|
187
|
+
# @param resource [Object] the collection being serialized
|
|
188
|
+
# @param serializer_class [Class, NilClass] serializer for its members
|
|
189
|
+
# @return [NilClass]
|
|
190
|
+
def self.assign_collection_root!(options, resource, serializer_class)
|
|
191
|
+
return if options[:root]
|
|
192
|
+
|
|
193
|
+
root = collection_root(resource, serializer_class)
|
|
194
|
+
options[:root] = root if root
|
|
195
|
+
nil
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Resolves the type name a serializer class stands for
|
|
199
|
+
#
|
|
200
|
+
# @param serializer_class [Class, NilClass] e.g. `V1::UserSerializer`
|
|
201
|
+
# @return [String, nil] e.g. `"user"`
|
|
202
|
+
def self.serializer_name(serializer_class)
|
|
203
|
+
name = serializer_class && serializer_class.name
|
|
204
|
+
name && name.demodulize.delete_suffix("Serializer").underscore
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Resolves the record a collection's model name should come from
|
|
208
|
+
#
|
|
209
|
+
# @param resource [Object] the collection
|
|
210
|
+
# @return [Object, NilClass] a record responding to `model_name`
|
|
211
|
+
def self.collection_model(resource)
|
|
212
|
+
return resource if resource.is_a?(ActiveRecord::Relation)
|
|
213
|
+
|
|
214
|
+
resource.respond_to?(:first) ? resource.first : nil
|
|
155
215
|
end
|
|
156
216
|
end
|
|
157
217
|
end
|
data/lib/api_kit/version.rb
CHANGED
data/spec/dummy.rb
CHANGED
|
@@ -28,6 +28,12 @@ ActiveRecord::Schema.define do
|
|
|
28
28
|
t.integer :quantity
|
|
29
29
|
t.timestamps
|
|
30
30
|
end
|
|
31
|
+
|
|
32
|
+
create_table :inventory_items, force: true do |t|
|
|
33
|
+
t.string :name
|
|
34
|
+
t.integer :quantity
|
|
35
|
+
t.timestamps
|
|
36
|
+
end
|
|
31
37
|
end
|
|
32
38
|
|
|
33
39
|
|
|
@@ -94,6 +100,32 @@ class UserSerializer < ActiveModel::Serializer
|
|
|
94
100
|
end
|
|
95
101
|
end
|
|
96
102
|
|
|
103
|
+
# Namespaced model — `model_name.to_s.underscore` is slash-separated
|
|
104
|
+
# (`inventory/item`) while `model_name.singular` is not
|
|
105
|
+
# (`inventory_item`). The sparse-fieldset type key is the former.
|
|
106
|
+
module Inventory
|
|
107
|
+
class Item < ApplicationRecord
|
|
108
|
+
self.table_name = 'inventory_items'
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
class ItemSerializer < ActiveModel::Serializer
|
|
112
|
+
attributes :id, :name, :quantity
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# A row of an aggregation endpoint: a PORO, not an AR record, rendered as
|
|
117
|
+
# a plain Array. An empty one gives AMS nothing to infer a root key from.
|
|
118
|
+
class ReportRow
|
|
119
|
+
include ActiveModel::Model
|
|
120
|
+
include ActiveModel::Serialization
|
|
121
|
+
|
|
122
|
+
attr_accessor :label, :total
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
class ReportRowSerializer < ActiveModel::Serializer
|
|
126
|
+
attributes :label, :total
|
|
127
|
+
end
|
|
128
|
+
|
|
97
129
|
class MyUserSerializer < UserSerializer
|
|
98
130
|
attribute :full_name
|
|
99
131
|
|
|
@@ -110,6 +142,8 @@ class Dummy < Rails::Application
|
|
|
110
142
|
scope defaults: { format: :api } do
|
|
111
143
|
resources :users, only: [ :index, :show ]
|
|
112
144
|
resources :notes, only: [ :update ]
|
|
145
|
+
resources :inventory_items, only: [ :index ]
|
|
146
|
+
resources :reports, only: [ :index ]
|
|
113
147
|
end
|
|
114
148
|
end
|
|
115
149
|
end
|
|
@@ -170,6 +204,29 @@ class UsersController < BaseApplicationController
|
|
|
170
204
|
end
|
|
171
205
|
end
|
|
172
206
|
|
|
207
|
+
class InventoryItemsController < BaseApplicationController
|
|
208
|
+
include ApiKit::Fetching
|
|
209
|
+
|
|
210
|
+
def index
|
|
211
|
+
render api: Inventory::Item.all, serializer_class: Inventory::ItemSerializer
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
class ReportsController < BaseApplicationController
|
|
216
|
+
include ApiKit::Fetching
|
|
217
|
+
|
|
218
|
+
def index
|
|
219
|
+
rows =
|
|
220
|
+
if params[:empty]
|
|
221
|
+
[]
|
|
222
|
+
else
|
|
223
|
+
[ ReportRow.new(label: 'first', total: 1) ]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
render api: rows, serializer_class: ReportRowSerializer
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
173
230
|
class NotesController < ActionController::Base
|
|
174
231
|
include ApiKit::Fetching
|
|
175
232
|
include ApiKit::Errors
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# The sparse-fieldset type key AMS looks a serializer up by is
|
|
4
|
+
# `model_name.to_s.underscore`. api_kit has to derive the same string when it
|
|
5
|
+
# injects the fallback "everything for the primary type" entry, or AMS falls
|
|
6
|
+
# through to the pluralised collection key — whose value is an empty list.
|
|
7
|
+
RSpec.describe 'sparse fieldsets', type: :request do
|
|
8
|
+
describe 'GET /inventory_items' do
|
|
9
|
+
let!(:item) { Inventory::Item.create!(name: 'Bolt', quantity: 7) }
|
|
10
|
+
|
|
11
|
+
before { get(inventory_items_path, params: params, headers: api_headers) }
|
|
12
|
+
|
|
13
|
+
context 'when fields are sent for the primary type' do
|
|
14
|
+
let(:params) { { fields: { 'inventory/item' => 'id,name' } } }
|
|
15
|
+
|
|
16
|
+
it 'trims the primary type to the requested attributes' do
|
|
17
|
+
expect(response).to have_http_status(:ok)
|
|
18
|
+
expect(response_json['data'].first.keys).to contain_exactly('id', 'name')
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
context 'when fields are sent only for another type' do
|
|
23
|
+
let(:params) { { fields: { user: 'id' } } }
|
|
24
|
+
|
|
25
|
+
it 'leaves the namespaced primary type fully populated' do
|
|
26
|
+
expect(response).to have_http_status(:ok)
|
|
27
|
+
expect(response_json['data'].first.keys)
|
|
28
|
+
.to contain_exactly('id', 'name', 'quantity')
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
context 'without any fields param' do
|
|
33
|
+
let(:params) { {} }
|
|
34
|
+
|
|
35
|
+
it 'returns every attribute' do
|
|
36
|
+
expect(response).to have_http_status(:ok)
|
|
37
|
+
expect(response_json['data'].first.keys)
|
|
38
|
+
.to contain_exactly('id', 'name', 'quantity')
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# `AMS::CollectionSerializer#json_key` infers the root from the first element,
|
|
44
|
+
# or from a collection that answers `#name` (an `ActiveRecord::Relation` does,
|
|
45
|
+
# through its klass). An empty plain Array answers neither, and raises
|
|
46
|
+
# `CannotInferRootKeyError` as soon as a fieldset has to be built.
|
|
47
|
+
describe 'GET /reports' do
|
|
48
|
+
before { get(reports_path, params: params, headers: api_headers) }
|
|
49
|
+
|
|
50
|
+
context 'with rows and a fields param' do
|
|
51
|
+
let(:params) { { fields: { report_row: 'label' } } }
|
|
52
|
+
|
|
53
|
+
it 'trims the PORO row to the requested attributes' do
|
|
54
|
+
expect(response).to have_http_status(:ok)
|
|
55
|
+
expect(response_json['data'].first.keys).to contain_exactly('label')
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
context 'with rows and no fields param' do
|
|
60
|
+
let(:params) { {} }
|
|
61
|
+
|
|
62
|
+
it 'returns every attribute' do
|
|
63
|
+
expect(response).to have_http_status(:ok)
|
|
64
|
+
expect(response_json['data'].first.keys)
|
|
65
|
+
.to contain_exactly('label', 'total')
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
context 'with no rows and a fields param' do
|
|
70
|
+
let(:params) { { empty: true, fields: { report_row: 'label' } } }
|
|
71
|
+
|
|
72
|
+
it 'renders an empty collection instead of failing to infer a root' do
|
|
73
|
+
expect(response).to have_http_status(:ok)
|
|
74
|
+
expect(response_json['data']).to eq([])
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
context 'with no rows and no fields param' do
|
|
79
|
+
let(:params) { { empty: true } }
|
|
80
|
+
|
|
81
|
+
it 'renders an empty collection' do
|
|
82
|
+
expect(response).to have_http_status(:ok)
|
|
83
|
+
expect(response_json['data']).to eq([])
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_api_kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ismail Akbudak
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ransack
|
|
@@ -244,6 +244,7 @@ files:
|
|
|
244
244
|
- spec/fetching_spec.rb
|
|
245
245
|
- spec/filtering_spec.rb
|
|
246
246
|
- spec/pagination_spec.rb
|
|
247
|
+
- spec/sparse_fields_type_key_spec.rb
|
|
247
248
|
- spec/spec_helper.rb
|
|
248
249
|
- spec/support/api_kit_rspec.rb
|
|
249
250
|
homepage: https://github.com/iakbudak/api_kit
|