Module: ApiKit::RailsApp

Defined in:
lib/api_kit/rails_app.rb

Constant Summary collapse

API_PAGINATE_METHODS_MAPPING =
{
  meta: :api_meta,
  links: :api_pagination,
  fields: :api_fields,
  include: :api_include,
  params: :api_serializer_params
}
API_METHODS_MAPPING =
{
  meta: :api_meta,
  fields: :api_fields,
  include: :api_include,
  params: :api_serializer_params
}

Class Method Summary collapse

Class Method Details

.add_errors_renderer! ⇒ NilClass

Adds the error renderer

Returns:

  • (NilClass)


38
39
40
41
42
43
44
45
46
47
# File 'lib/api_kit/rails_app.rb', line 38

def self.add_errors_renderer!
  ActionController::Renderers.add(:api_errors) do |resource, options|
    self.content_type ||= Mime[:json]

    many = ApiKit::RailsApp.is_collection?(resource, options[:is_collection])
    resource = [ resource ] unless many

    ApiKit::ErrorSerializer.new(resource, options).to_json
  end
end

.add_renderer! ⇒ NilClass

Adds the default renderer

Returns:

  • (NilClass)


52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/api_kit/rails_app.rb', line 52

def self.add_renderer!
  ActionController::Renderers.add(:api_paginate) do |resource, options|
    self.content_type ||= Mime[:json]

    result = {}
    API_PAGINATE_METHODS_MAPPING.to_a[0..1].each do |opt, method_name|
      next unless respond_to?(method_name, true)
      result[opt] ||= send(method_name, resource)
    end

    # If it's an empty collection, return it directly.
    many = ApiKit::RailsApp.is_collection?(resource, options[:is_collection])

    API_PAGINATE_METHODS_MAPPING.to_a[2..-1].each do |opt, method_name|
      options[opt] ||= send(method_name) if respond_to?(method_name, true)
    end

    if options[:serializer_class]
      serializer_class = options[:serializer_class]
    else
      serializer_class = ApiKit::RailsApp.serializer_class(resource, many)
    end

    options[:fields] = api_fields(serializer_class, ApiKit::RailsApp.fetch_name(many, resource))
    options[:adapter] = :attributes
    options[:each_serializer] = serializer_class
    ApiKit::RailsApp.assign_collection_root!(options, resource, serializer_class) if many
    data = ActiveModelSerializers::SerializableResource.new(resource, options).as_json
    result[:data] = data
    result.to_json
  end

  ActionController::Renderers.add(:api) do |resource, options|
    self.content_type ||= Mime[:json]

    result = {}
    API_METHODS_MAPPING.to_a[0..0].each do |opt, method_name|
      next unless respond_to?(method_name, true)
      result[opt] ||= send(method_name, resource)
    end

    # If it's an empty collection, return it directly.
    many = ApiKit::RailsApp.is_collection?(resource, options[:is_collection])

    API_METHODS_MAPPING.to_a[1..-1].each do |opt, method_name|
      options[opt] ||= send(method_name) if respond_to?(method_name, true)
    end

    if options[:serializer_class]
      serializer_class = options[:serializer_class]
    else
      serializer_class = ApiKit::RailsApp.serializer_class(resource, many)
    end

    # Use Active Model Serializers properly with fallback
    options[:fields] = api_fields(serializer_class, ApiKit::RailsApp.fetch_name(many, resource))
    options[:adapter] = :attributes
    options[:each_serializer] = serializer_class
    if many
      ApiKit::RailsApp.assign_collection_root!(options, resource, serializer_class)
      data = ActiveModelSerializers::SerializableResource.new(resource, options).as_json
    else
      data = ActiveModelSerializers::SerializableResource.new([ resource ], options).as_json[0]
    end
    result[:data] = data
    result.to_json
  end
end

.assign_collection_root!(options, resource, serializer_class) ⇒ NilClass

Sets the collection root only when AMS could not work one out

Parameters:

  • options (Hash) —

    render options, mutated in place

  • resource (Object) —

    the collection being serialized

  • serializer_class (Class, NilClass) —

    serializer for its members

Returns:

  • (NilClass)


190
191
192
193
194
195
196
# File 'lib/api_kit/rails_app.rb', line 190

def self.assign_collection_root!(options, resource, serializer_class)
  return if options[:root]

  root = collection_root(resource, serializer_class)
  options[:root] = root if root
  nil
end

.collection_model(resource) ⇒ Object, NilClass

Resolves the record a collection's model name should come from

Parameters:

  • resource (Object) —

    the collection

Returns:

  • (Object, NilClass) —

    a record responding to model_name



211
212
213
214
215
# File 'lib/api_kit/rails_app.rb', line 211

def self.collection_model(resource)
  return resource if resource.is_a?(ActiveRecord::Relation)

  resource.respond_to?(:first) ? resource.first : nil
end

.collection_root(resource, serializer_class) ⇒ String?

Root key for a collection AMS cannot infer one for

AMS::CollectionSerializer#json_key reads the root from its first element, or from a named collection (ActiveRecord::Relation answers #name through its klass). An EMPTY plain Array offers neither, so it raises CannotInferRootKeyError the moment sparse fieldsets are requested. Aggregation endpoints that render POROs hit exactly that.

Returns nil whenever AMS can infer the key itself, so a collection that already works keeps its own root and item serializers keep their json_key.

Parameters:

  • resource (Object) —

    the collection being serialized

  • serializer_class (Class, NilClass) —

    serializer for its members

Returns:

  • (String, nil) —

    pluralised root key, or nil to leave it to AMS



176
177
178
179
180
181
182
# File 'lib/api_kit/rails_app.rb', line 176

def self.collection_root(resource, serializer_class)
  return nil unless resource.respond_to?(:empty?) && resource.empty?
  return nil if resource.respond_to?(:name)

  name = serializer_name(serializer_class)
  name && name.pluralize
end

.fetch_name(many, resource) ⇒ String?

Resolves the model name used as the sparse-fieldset type key

Mirrors ActiveModel::Serializer#json_key, which AMS uses to look a type up in the fieldset: object.class.model_name.to_s.underscore. NOT model_name.singular — that tr()s the namespace separator to an underscore (manufacturing_work_order), so for a namespaced model the key never matched, the primary type went unconstrained, and AMS fell through to the pluralised collection key whose value is an empty list: every attribute of the primary resource was silently dropped.

Parameters:

  • many (Boolean) —

    indicates whether the resource is a collection

  • resource (Object) —

    serialized resource or collection

Returns:

  • (String, nil) —

    model name when available



155
156
157
158
159
# File 'lib/api_kit/rails_app.rb', line 155

def self.fetch_name(many, resource)
  record = many ? collection_model(resource) : resource
  model_name = record&.model_name
  model_name && model_name.to_s.underscore
end

.install! ⇒ NilClass

Updates the mime types and registers the renderers

Returns:

  • (NilClass)


24
25
26
27
28
29
30
31
32
# File 'lib/api_kit/rails_app.rb', line 24

def self.install!
  return unless defined?(::Rails)

  parser = ActionDispatch::Request.parameter_parsers[:json]
  ActionDispatch::Request.parameter_parsers[:api] = parser

  self.add_renderer!
  self.add_errors_renderer!
end

.is_collection?(resource, force_is_collection = nil) ⇒ TrueClass

Checks if an object is a collection

Parameters:

  • resource (Object) —

    to check

  • force_is_collection (NilClass) (defaults to: nil) —

    flag to overwrite

Returns:

  • (TrueClass) —

    upon success



126
127
128
129
130
# File 'lib/api_kit/rails_app.rb', line 126

def self.is_collection?(resource, force_is_collection = nil)
  return force_is_collection unless force_is_collection.nil?

  resource.respond_to?(:size) && !resource.respond_to?(:each_pair)
end

.serializer_class(resource, is_collection) ⇒ Class

Resolves resource serializer class

Returns:

  • (Class)


135
136
137
138
139
140
# File 'lib/api_kit/rails_app.rb', line 135

def self.serializer_class(resource, is_collection)
  klass = resource.class
  klass = resource.first.class if is_collection

  "#{klass.name}Serializer".constantize
end

.serializer_name(serializer_class) ⇒ String?

Resolves the type name a serializer class stands for

Parameters:

  • serializer_class (Class, NilClass) —

    e.g. V1::UserSerializer

Returns:

  • (String, nil) —

    e.g. "user"



202
203
204
205
# File 'lib/api_kit/rails_app.rb', line 202

def self.serializer_name(serializer_class)
  name = serializer_class && serializer_class.name
  name && name.demodulize.delete_suffix("Serializer").underscore
end