mirror of
https://github.com/ditkrg/active_model_serializers.git
synced 2026-01-23 06:16:50 +00:00
Clean slate
This commit is contained in:
parent
04125a06b0
commit
a791070a29
51
.rubocop.yml
51
.rubocop.yml
@ -1,5 +1,5 @@
|
|||||||
AllCops:
|
AllCops:
|
||||||
TargetRubyVersion: 2.1
|
TargetRubyVersion: 2.3
|
||||||
Exclude:
|
Exclude:
|
||||||
- !ruby/regexp /(vendor|bundle|bin|db|tmp)\/.*/
|
- !ruby/regexp /(vendor|bundle|bin|db|tmp)\/.*/
|
||||||
DisplayCopNames: true
|
DisplayCopNames: true
|
||||||
@ -12,11 +12,6 @@ AllCops:
|
|||||||
Rails:
|
Rails:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
|
||||||
Lint/NestedMethodDefinition:
|
|
||||||
Enabled: false
|
|
||||||
Exclude:
|
|
||||||
- test/action_controller/serialization_test.rb
|
|
||||||
|
|
||||||
Style/Alias:
|
Style/Alias:
|
||||||
EnforcedStyle: prefer_alias
|
EnforcedStyle: prefer_alias
|
||||||
|
|
||||||
@ -24,24 +19,24 @@ Style/StringLiterals:
|
|||||||
EnforcedStyle: single_quotes
|
EnforcedStyle: single_quotes
|
||||||
|
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Max: 35 # TODO: Lower to 15
|
Max: 15
|
||||||
|
|
||||||
Metrics/ClassLength:
|
Metrics/ClassLength:
|
||||||
Max: 261 # TODO: Lower to 100
|
Max: 100
|
||||||
Exclude:
|
Exclude:
|
||||||
- test/**/*.rb
|
- test/**/*.rb
|
||||||
|
|
||||||
Metrics/CyclomaticComplexity:
|
Metrics/CyclomaticComplexity:
|
||||||
Max: 7 # TODO: Lower to 6
|
Max: 6
|
||||||
|
|
||||||
Metrics/LineLength:
|
Metrics/LineLength:
|
||||||
Max: 251 # TODO: Lower to 80
|
Max: 100
|
||||||
|
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Max: 106 # TODO: Lower to 10
|
Max: 10
|
||||||
|
|
||||||
Metrics/PerceivedComplexity:
|
Metrics/PerceivedComplexity:
|
||||||
Max: 9 # TODO: Lower to 7
|
Max: 7
|
||||||
|
|
||||||
Style/AlignParameters:
|
Style/AlignParameters:
|
||||||
EnforcedStyle: with_fixed_indentation
|
EnforcedStyle: with_fixed_indentation
|
||||||
@ -49,9 +44,6 @@ Style/AlignParameters:
|
|||||||
Style/ClassAndModuleChildren:
|
Style/ClassAndModuleChildren:
|
||||||
EnforcedStyle: nested
|
EnforcedStyle: nested
|
||||||
|
|
||||||
Style/Documentation:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/MissingElse:
|
Style/MissingElse:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
EnforcedStyle: case
|
EnforcedStyle: case
|
||||||
@ -72,34 +64,5 @@ Style/SignalException:
|
|||||||
Style/TrailingCommaInLiteral:
|
Style/TrailingCommaInLiteral:
|
||||||
EnforcedStyleForMultiline: no_comma
|
EnforcedStyleForMultiline: no_comma
|
||||||
|
|
||||||
Style/ConditionalAssignment:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/DotPosition:
|
Style/DotPosition:
|
||||||
EnforcedStyle: leading
|
EnforcedStyle: leading
|
||||||
|
|
||||||
########## test_helper.rb sanity
|
|
||||||
Style/EndBlock:
|
|
||||||
Exclude:
|
|
||||||
- test/test_helper.rb
|
|
||||||
|
|
||||||
Style/SpecialGlobalVars:
|
|
||||||
Exclude:
|
|
||||||
- test/test_helper.rb
|
|
||||||
|
|
||||||
Style/GlobalVars:
|
|
||||||
Exclude:
|
|
||||||
- test/test_helper.rb
|
|
||||||
|
|
||||||
Style/AndOr:
|
|
||||||
Exclude:
|
|
||||||
- test/test_helper.rb
|
|
||||||
- 'lib/active_model/serializer/lint.rb'
|
|
||||||
|
|
||||||
Style/Not:
|
|
||||||
Exclude:
|
|
||||||
- test/test_helper.rb
|
|
||||||
|
|
||||||
Style/ClassCheck:
|
|
||||||
Exclude:
|
|
||||||
- test/test_helper.rb
|
|
||||||
|
|||||||
80
.simplecov
80
.simplecov
@ -3,15 +3,7 @@
|
|||||||
# vim: set ft=ruby
|
# vim: set ft=ruby
|
||||||
|
|
||||||
## DEFINE VARIABLES
|
## DEFINE VARIABLES
|
||||||
@minimum_coverage = ENV.fetch('COVERAGE_MINIMUM') {
|
@minimum_coverage = 100.0
|
||||||
case (defined?(RUBY_ENGINE) && RUBY_ENGINE) || "ruby"
|
|
||||||
when 'jruby', 'rbx'
|
|
||||||
96.0
|
|
||||||
else
|
|
||||||
98.1
|
|
||||||
end
|
|
||||||
}.to_f.round(2)
|
|
||||||
# rubocop:disable Style/DoubleNegation
|
|
||||||
ENV['FULL_BUILD'] ||= ENV['CI']
|
ENV['FULL_BUILD'] ||= ENV['CI']
|
||||||
@running_ci = !!(ENV['FULL_BUILD'] =~ /\Atrue\z/i)
|
@running_ci = !!(ENV['FULL_BUILD'] =~ /\Atrue\z/i)
|
||||||
@generate_report = @running_ci || !!(ENV['COVERAGE'] =~ /\Atrue\z/i)
|
@generate_report = @running_ci || !!(ENV['COVERAGE'] =~ /\Atrue\z/i)
|
||||||
@ -43,68 +35,18 @@ SimpleCov.profiles.define 'app' do
|
|||||||
add_filter '/.bundle/'
|
add_filter '/.bundle/'
|
||||||
end
|
end
|
||||||
|
|
||||||
## START TRACKING COVERAGE (before activating SimpleCov)
|
if @generate_report
|
||||||
require 'coverage'
|
|
||||||
Coverage.start
|
|
||||||
|
|
||||||
## ADD SOME CUSTOM REPORTING AT EXIT
|
|
||||||
SimpleCov.at_exit do
|
|
||||||
next if $! and not ($!.kind_of? SystemExit and $!.success?)
|
|
||||||
|
|
||||||
header = "#{'*' * 20} SimpleCov Results #{'*' * 20}"
|
|
||||||
results = SimpleCov.result.format!.join("\n")
|
|
||||||
exit_message = <<-EOF
|
|
||||||
|
|
||||||
#{header}
|
|
||||||
{{RESULTS}}
|
|
||||||
{{FAILURE_MESSAGE}}
|
|
||||||
|
|
||||||
#{'*' * header.size}
|
|
||||||
EOF
|
|
||||||
percent = Float(SimpleCov.result.covered_percent)
|
|
||||||
if percent < @minimum_coverage
|
|
||||||
failure_message = <<-EOF
|
|
||||||
Spec coverage was not high enough: #{percent.round(2)}% is < #{@minimum_coverage}%
|
|
||||||
EOF
|
|
||||||
exit_message.sub!('{{RESULTS}}', results).sub!('{{FAILURE_MESSAGE}}', failure_message)
|
|
||||||
@output.puts exit_message
|
|
||||||
abort(failure_message) if @generate_report
|
|
||||||
elsif @running_ci
|
|
||||||
exit_message.sub!('{{RESULTS}}', results).sub!('{{FAILURE_MESSAGE}}', <<-EOF)
|
|
||||||
Nice job! Spec coverage (#{percent.round(2)}%) is still at or above #{@minimum_coverage}%
|
|
||||||
EOF
|
|
||||||
@output.puts exit_message
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
## CAPTURE CONFIG IN CLOSURE 'AppCoverage.start'
|
|
||||||
## to defer running until test/test_helper.rb is loaded.
|
|
||||||
# rubocop:disable Style/MultilineBlockChain
|
|
||||||
AppCoverage = Class.new do
|
|
||||||
def initialize(&block)
|
|
||||||
@block = block
|
|
||||||
end
|
|
||||||
|
|
||||||
def start
|
|
||||||
@block.call
|
|
||||||
end
|
|
||||||
end.new do
|
|
||||||
SimpleCov.start 'app'
|
SimpleCov.start 'app'
|
||||||
if @generate_report
|
if @running_ci
|
||||||
if @running_ci
|
require 'codeclimate-test-reporter'
|
||||||
require 'codeclimate-test-reporter'
|
@output.puts '[COVERAGE] Running with SimpleCov Simple Formatter and CodeClimate Test Reporter'
|
||||||
@output.puts '[COVERAGE] Running with SimpleCov Simple Formatter and CodeClimate Test Reporter'
|
formatters = [
|
||||||
formatters = [
|
SimpleCov::Formatter::SimpleFormatter,
|
||||||
SimpleCov::Formatter::SimpleFormatter,
|
CodeClimate::TestReporter::Formatter
|
||||||
CodeClimate::TestReporter::Formatter
|
]
|
||||||
]
|
|
||||||
else
|
|
||||||
@output.puts '[COVERAGE] Running with SimpleCov HTML Formatter'
|
|
||||||
formatters = [SimpleCov::Formatter::HTMLFormatter]
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
formatters = []
|
@output.puts '[COVERAGE] Running with SimpleCov HTML Formatter'
|
||||||
|
formatters = [SimpleCov::Formatter::HTMLFormatter]
|
||||||
end
|
end
|
||||||
SimpleCov.formatters = formatters
|
SimpleCov.formatters = formatters
|
||||||
end
|
end
|
||||||
# rubocop:enable Style/MultilineBlockChain
|
|
||||||
|
|||||||
21
.travis.yml
21
.travis.yml
@ -6,17 +6,7 @@ rvm:
|
|||||||
- 2.1
|
- 2.1
|
||||||
- 2.2.6
|
- 2.2.6
|
||||||
- 2.3.3
|
- 2.3.3
|
||||||
- ruby-head
|
|
||||||
- jruby-9.1.5.0 # is precompiled per http://rubies.travis-ci.org/
|
|
||||||
- jruby-head
|
|
||||||
|
|
||||||
jdk:
|
|
||||||
- oraclejdk8
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- gem update --system
|
|
||||||
- rvm @global do gem uninstall bundler -a -x
|
|
||||||
- rvm @global do gem install bundler -v 1.13.7
|
|
||||||
install: bundle install --path=vendor/bundle --retry=3 --jobs=3
|
install: bundle install --path=vendor/bundle --retry=3 --jobs=3
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
@ -27,8 +17,6 @@ script:
|
|||||||
after_success:
|
after_success:
|
||||||
- codeclimate-test-reporter
|
- codeclimate-test-reporter
|
||||||
env:
|
env:
|
||||||
global:
|
|
||||||
- "JRUBY_OPTS='--dev -J-Xmx1024M --debug'"
|
|
||||||
matrix:
|
matrix:
|
||||||
- "RAILS_VERSION=4.1"
|
- "RAILS_VERSION=4.1"
|
||||||
- "RAILS_VERSION=4.2"
|
- "RAILS_VERSION=4.2"
|
||||||
@ -39,17 +27,8 @@ matrix:
|
|||||||
exclude:
|
exclude:
|
||||||
- rvm: 2.1
|
- rvm: 2.1
|
||||||
env: RAILS_VERSION=master
|
env: RAILS_VERSION=master
|
||||||
- rvm: jruby-9.1.5.0
|
|
||||||
env: RAILS_VERSION=master
|
|
||||||
- rvm: jruby-head
|
|
||||||
env: RAILS_VERSION=master
|
|
||||||
- rvm: 2.1
|
- rvm: 2.1
|
||||||
env: RAILS_VERSION=5.0
|
env: RAILS_VERSION=5.0
|
||||||
- rvm: jruby-9.1.5.0
|
|
||||||
env: RAILS_VERSION=5.0
|
|
||||||
- rvm: jruby-head
|
|
||||||
env: RAILS_VERSION=5.0
|
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- rvm: ruby-head
|
- rvm: ruby-head
|
||||||
- rvm: jruby-head
|
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
## 0.10.x
|
## 1.0
|
||||||
|
|
||||||
### [master (unreleased)](https://github.com/rails-api/active_model_serializers/compare/v0.10.5...master)
|
### master (unreleased)
|
||||||
|
|
||||||
Breaking changes:
|
Breaking changes:
|
||||||
|
|
||||||
@ -10,6 +10,10 @@ Fixes:
|
|||||||
|
|
||||||
Misc:
|
Misc:
|
||||||
|
|
||||||
|
## 0.10.x
|
||||||
|
|
||||||
|
Misc:
|
||||||
|
|
||||||
- [#2081](https://github.com/rails-api/active_model_serializers/pull/2081) Documentation for `include` option in adapters. (@charlie-wasp)
|
- [#2081](https://github.com/rails-api/active_model_serializers/pull/2081) Documentation for `include` option in adapters. (@charlie-wasp)
|
||||||
|
|
||||||
### [v0.10.5 (2017-03-07)](https://github.com/rails-api/active_model_serializers/compare/v0.10.4...v0.10.5)
|
### [v0.10.5 (2017-03-07)](https://github.com/rails-api/active_model_serializers/compare/v0.10.4...v0.10.5)
|
||||||
|
|||||||
@ -6,11 +6,7 @@ Before opening an issue, try the following:
|
|||||||
|
|
||||||
See if your issue can be resolved by information in the documentation.
|
See if your issue can be resolved by information in the documentation.
|
||||||
|
|
||||||
- [0.10 (master) Documentation](https://github.com/rails-api/active_model_serializers/tree/master/docs)
|
TBD
|
||||||
- [](http://www.rubydoc.info/github/rails-api/active_model_serializers/v0.10.0)
|
|
||||||
- [Guides](docs)
|
|
||||||
- [0.9 (0-9-stable) Documentation](https://github.com/rails-api/active_model_serializers/tree/0-9-stable)
|
|
||||||
- [0.8 (0-8-stable) Documentation](https://github.com/rails-api/active_model_serializers/tree/0-8-stable)
|
|
||||||
|
|
||||||
##### Check for an existing issue
|
##### Check for an existing issue
|
||||||
|
|
||||||
@ -44,6 +40,8 @@ We also gladly welcome pull requests. When preparing to work on pull request,
|
|||||||
please adhere to these standards:
|
please adhere to these standards:
|
||||||
|
|
||||||
- Base work on the master branch unless fixing an issue with
|
- Base work on the master branch unless fixing an issue with
|
||||||
|
[0.10-stable](https://github.com/rails-api/active_model_serializers/tree/0-10-stable)
|
||||||
|
or
|
||||||
[0.9-stable](https://github.com/rails-api/active_model_serializers/tree/0-9-stable)
|
[0.9-stable](https://github.com/rails-api/active_model_serializers/tree/0-9-stable)
|
||||||
or
|
or
|
||||||
[0.8-stable](https://github.com/rails-api/active_model_serializers/tree/0-8-stable)
|
[0.8-stable](https://github.com/rails-api/active_model_serializers/tree/0-8-stable)
|
||||||
@ -72,34 +70,3 @@ Run a single test suite
|
|||||||
Run a single test
|
Run a single test
|
||||||
|
|
||||||
`$ rake test TEST=path/to/test.rb TESTOPTS="--name=test_something"`
|
`$ rake test TEST=path/to/test.rb TESTOPTS="--name=test_something"`
|
||||||
|
|
||||||
Run tests against different Rails versions by setting the RAILS_VERSION variable
|
|
||||||
and bundling gems. (save this script somewhere executable and run from top of AMS repository)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
rcommand='puts YAML.load_file("./.travis.yml")["env"]["matrix"].join(" ").gsub("RAILS_VERSION=", "")'
|
|
||||||
versions=$(ruby -ryaml -e "$rcommand")
|
|
||||||
|
|
||||||
for version in ${versions[@]}; do
|
|
||||||
export RAILS_VERSION="$version"
|
|
||||||
rm -f Gemfile.lock
|
|
||||||
bundle check || bundle --local || bundle
|
|
||||||
bundle exec rake test
|
|
||||||
if [ "$?" -eq 0 ]; then
|
|
||||||
# green in ANSI
|
|
||||||
echo -e "\033[32m **** Tests passed against Rails ${RAILS_VERSION} **** \033[0m"
|
|
||||||
else
|
|
||||||
# red in ANSI
|
|
||||||
echo -e "\033[31m **** Tests failed against Rails ${RAILS_VERSION} **** \033[0m"
|
|
||||||
read -p '[Enter] any key to continue, [q] to quit...' prompt
|
|
||||||
if [ "$prompt" = 'q' ]; then
|
|
||||||
unset RAILS_VERSION
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
unset RAILS_VERSION
|
|
||||||
done
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|||||||
44
Gemfile
44
Gemfile
@ -1,50 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
#
|
|
||||||
# Add a Gemfile.local to locally bundle gems outside of version control
|
|
||||||
local_gemfile = File.join(File.expand_path('..', __FILE__), 'Gemfile.local')
|
|
||||||
eval_gemfile local_gemfile if File.readable?(local_gemfile)
|
|
||||||
|
|
||||||
# Specify your gem's dependencies in active_model_serializers.gemspec
|
|
||||||
gemspec
|
gemspec
|
||||||
|
|
||||||
version = ENV['RAILS_VERSION'] || '4.2'
|
|
||||||
|
|
||||||
if version == 'master'
|
|
||||||
gem 'rack', github: 'rack/rack'
|
|
||||||
gem 'arel', github: 'rails/arel'
|
|
||||||
git 'https://github.com/rails/rails.git' do
|
|
||||||
gem 'railties'
|
|
||||||
gem 'activesupport'
|
|
||||||
gem 'activemodel'
|
|
||||||
gem 'actionpack'
|
|
||||||
gem 'activerecord', group: :test
|
|
||||||
# Rails 5
|
|
||||||
gem 'actionview'
|
|
||||||
end
|
|
||||||
else
|
|
||||||
gem_version = "~> #{version}.0"
|
|
||||||
gem 'railties', gem_version
|
|
||||||
gem 'activesupport', gem_version
|
|
||||||
gem 'activemodel', gem_version
|
|
||||||
gem 'actionpack', gem_version
|
|
||||||
gem 'activerecord', gem_version, group: :test
|
|
||||||
end
|
|
||||||
|
|
||||||
# https://github.com/bundler/bundler/blob/89a8778c19269561926cea172acdcda241d26d23/lib/bundler/dependency.rb#L30-L54
|
|
||||||
@windows_platforms = [:mswin, :mingw, :x64_mingw]
|
|
||||||
|
|
||||||
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
|
||||||
gem 'tzinfo-data', platforms: (@windows_platforms + [:jruby])
|
|
||||||
|
|
||||||
group :bench do
|
|
||||||
# https://github.com/rails-api/active_model_serializers/commit/cb4459580a6f4f37f629bf3185a5224c8624ca76
|
|
||||||
gem 'benchmark-ips', '>= 2.7.2', require: false, group: :development
|
|
||||||
end
|
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem 'sqlite3', platform: (@windows_platforms + [:ruby])
|
|
||||||
gem 'activerecord-jdbcsqlite3-adapter', platform: :jruby
|
|
||||||
gem 'codeclimate-test-reporter', require: false
|
|
||||||
gem 'm', '~> 1.5'
|
gem 'm', '~> 1.5'
|
||||||
gem 'pry', '~> 0.10'
|
gem 'pry', '~> 0.10'
|
||||||
gem 'pry-byebug', '~> 3.4', platform: :ruby
|
gem 'pry-byebug', '~> 3.4', platform: :ruby
|
||||||
|
|||||||
261
README.md
261
README.md
@ -12,7 +12,6 @@
|
|||||||
<td>Code Quality</td>
|
<td>Code Quality</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://codeclimate.com/github/rails-api/active_model_serializers"><img src="https://codeclimate.com/github/rails-api/active_model_serializers/badges/gpa.svg" alt="Code Quality"></a>
|
<a href="https://codeclimate.com/github/rails-api/active_model_serializers"><img src="https://codeclimate.com/github/rails-api/active_model_serializers/badges/gpa.svg" alt="Code Quality"></a>
|
||||||
<a href="https://codebeat.co/projects/github-com-rails-api-active_model_serializers"><img src="https://codebeat.co/badges/a9ab35fa-8b5a-4680-9d4e-a81f9a55ebcd" alt="codebeat" ></a>
|
|
||||||
<a href="https://codeclimate.com/github/rails-api/active_model_serializers/coverage"><img src="https://codeclimate.com/github/rails-api/active_model_serializers/badges/coverage.svg" alt="Test Coverage"></a>
|
<a href="https://codeclimate.com/github/rails-api/active_model_serializers/coverage"><img src="https://codeclimate.com/github/rails-api/active_model_serializers/badges/coverage.svg" alt="Test Coverage"></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -28,275 +27,27 @@
|
|||||||
|
|
||||||
ActiveModelSerializers brings convention over configuration to your JSON generation.
|
ActiveModelSerializers brings convention over configuration to your JSON generation.
|
||||||
|
|
||||||
ActiveModelSerializers works through two components: **serializers** and **adapters**.
|
TBD
|
||||||
|
|
||||||
Serializers describe _which_ attributes and relationships should be serialized.
|
|
||||||
|
|
||||||
Adapters describe _how_ attributes and relationships should be serialized.
|
|
||||||
|
|
||||||
SerializableResource co-ordinates the resource, Adapter and Serializer to produce the
|
|
||||||
resource serialization. The serialization has the `#as_json`, `#to_json` and `#serializable_hash`
|
|
||||||
methods used by the Rails JSON Renderer. (SerializableResource actually delegates
|
|
||||||
these methods to the adapter.)
|
|
||||||
|
|
||||||
By default ActiveModelSerializers will use the **Attributes Adapter** (no JSON root).
|
|
||||||
But we strongly advise you to use **JsonApi Adapter**, which
|
|
||||||
follows 1.0 of the format specified in [jsonapi.org/format](http://jsonapi.org/format).
|
|
||||||
Check how to change the adapter in the sections below.
|
|
||||||
|
|
||||||
`0.10.x` is **not** backward compatible with `0.9.x` nor `0.8.x`.
|
|
||||||
|
|
||||||
`0.10.x` is based on the `0.8.0` code, but with a more flexible
|
|
||||||
architecture. We'd love your help. [Learn how you can help here.](CONTRIBUTING.md)
|
|
||||||
|
|
||||||
It is generally safe and recommended to use the master branch.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Add this line to your application's Gemfile:
|
TBD
|
||||||
|
|
||||||
```
|
|
||||||
gem 'active_model_serializers', '~> 0.10.0'
|
|
||||||
```
|
|
||||||
|
|
||||||
And then execute:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ bundle
|
|
||||||
```
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
See [Getting Started](docs/general/getting_started.md) for the nuts and bolts.
|
TBD
|
||||||
|
|
||||||
More information is available in the [Guides](docs) and
|
|
||||||
[High-level behavior](README.md#high-level-behavior).
|
|
||||||
|
|
||||||
## Getting Help
|
## Getting Help
|
||||||
|
|
||||||
If you find a bug, please report an [Issue](https://github.com/rails-api/active_model_serializers/issues/new)
|
TBD
|
||||||
and see our [contributing guide](CONTRIBUTING.md).
|
|
||||||
|
|
||||||
If you have a question, please [post to Stack Overflow](http://stackoverflow.com/questions/tagged/active-model-serializers).
|
|
||||||
|
|
||||||
If you'd like to chat, we have a [community slack](http://amserializers.herokuapp.com).
|
|
||||||
|
|
||||||
Thanks!
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
If you're reading this at https://github.com/rails-api/active_model_serializers you are
|
TBD
|
||||||
reading documentation for our `master`, which may include features that have not
|
|
||||||
been released yet. Please see below for the documentation relevant to you.
|
|
||||||
|
|
||||||
- [0.10 (master) Documentation](https://github.com/rails-api/active_model_serializers/tree/master)
|
|
||||||
- [0.10.4 (latest release) Documentation](https://github.com/rails-api/active_model_serializers/tree/v0.10.4)
|
|
||||||
- [](http://www.rubydoc.info/gems/active_model_serializers/0.10.4)
|
|
||||||
- [Guides](docs)
|
|
||||||
- [0.9 (0-9-stable) Documentation](https://github.com/rails-api/active_model_serializers/tree/0-9-stable)
|
|
||||||
- [](http://www.rubydoc.info/github/rails-api/active_model_serializers/0-9-stable)
|
|
||||||
- [0.8 (0-8-stable) Documentation](https://github.com/rails-api/active_model_serializers/tree/0-8-stable)
|
|
||||||
- [](http://www.rubydoc.info/github/rails-api/active_model_serializers/0-8-stable)
|
|
||||||
|
|
||||||
|
|
||||||
## High-level behavior
|
## High-level behavior
|
||||||
|
|
||||||
Choose an adapter from [adapters](lib/active_model_serializers/adapter):
|
TBD
|
||||||
|
|
||||||
``` ruby
|
|
||||||
ActiveModelSerializers.config.adapter = :json_api # Default: `:attributes`
|
|
||||||
```
|
|
||||||
|
|
||||||
Given a [serializable model](lib/active_model/serializer/lint.rb):
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# either
|
|
||||||
class SomeResource < ActiveRecord::Base
|
|
||||||
# columns: title, body
|
|
||||||
end
|
|
||||||
# or
|
|
||||||
class SomeResource < ActiveModelSerializers::Model
|
|
||||||
attributes :title, :body
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
And initialized as:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
resource = SomeResource.new(title: 'ActiveModelSerializers', body: 'Convention over configuration')
|
|
||||||
```
|
|
||||||
|
|
||||||
Given a serializer for the serializable model:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class SomeSerializer < ActiveModel::Serializer
|
|
||||||
attribute :title, key: :name
|
|
||||||
attributes :body
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
The model can be serialized as:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
options = {}
|
|
||||||
serialization = ActiveModelSerializers::SerializableResource.new(resource, options)
|
|
||||||
serialization.to_json
|
|
||||||
serialization.as_json
|
|
||||||
```
|
|
||||||
|
|
||||||
SerializableResource delegates to the adapter, which it builds as:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
adapter_options = {}
|
|
||||||
adapter = ActiveModelSerializers::Adapter.create(serializer, adapter_options)
|
|
||||||
adapter.to_json
|
|
||||||
adapter.as_json
|
|
||||||
adapter.serializable_hash
|
|
||||||
```
|
|
||||||
|
|
||||||
The adapter formats the serializer's attributes and associations (a.k.a. includes):
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
serializer_options = {}
|
|
||||||
serializer = SomeSerializer.new(resource, serializer_options)
|
|
||||||
serializer.attributes
|
|
||||||
serializer.associations
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
This section focuses on architecture the 0.10.x version of ActiveModelSerializers. If you are interested in the architecture of the 0.8 or 0.9 versions,
|
|
||||||
please refer to the [0.8 README](https://github.com/rails-api/active_model_serializers/blob/0-8-stable/README.md) or
|
|
||||||
[0.9 README](https://github.com/rails-api/active_model_serializers/blob/0-9-stable/README.md).
|
|
||||||
|
|
||||||
The original design is also available [here](https://github.com/rails-api/active_model_serializers/blob/d72b66d4c5355b0ff0a75a04895fcc4ea5b0c65e/README.textile).
|
|
||||||
|
|
||||||
### ActiveModel::Serializer
|
|
||||||
|
|
||||||
An **`ActiveModel::Serializer`** wraps a [serializable resource](https://github.com/rails/rails/blob/4-2-stable/activemodel/lib/active_model/serialization.rb)
|
|
||||||
and exposes an `attributes` method, among a few others.
|
|
||||||
It allows you to specify which attributes and associations should be represented in the serializatation of the resource.
|
|
||||||
It requires an adapter to transform its attributes into a JSON document; it cannot be serialized itself.
|
|
||||||
It may be useful to think of it as a
|
|
||||||
[presenter](http://blog.steveklabnik.com/posts/2011-09-09-better-ruby-presenters).
|
|
||||||
|
|
||||||
#### ActiveModel::CollectionSerializer
|
|
||||||
|
|
||||||
The **`ActiveModel::CollectionSerializer`** represents a collection of resources as serializers
|
|
||||||
and, if there is no serializer, primitives.
|
|
||||||
|
|
||||||
### ActiveModelSerializers::Adapter::Base
|
|
||||||
|
|
||||||
The **`ActiveModelSerializeres::Adapter::Base`** describes the structure of the JSON document generated from a
|
|
||||||
serializer. For example, the `Attributes` example represents each serializer as its
|
|
||||||
unmodified attributes. The `JsonApi` adapter represents the serializer as a [JSON
|
|
||||||
API](http://jsonapi.org/) document.
|
|
||||||
|
|
||||||
### ActiveModelSerializers::SerializableResource
|
|
||||||
|
|
||||||
The **`ActiveModelSerializers::SerializableResource`** acts to coordinate the serializer(s) and adapter
|
|
||||||
to an object that responds to `to_json`, and `as_json`. It is used in the controller to
|
|
||||||
encapsulate the serialization resource when rendered. However, it can also be used on its own
|
|
||||||
to serialize a resource outside of a controller, as well.
|
|
||||||
|
|
||||||
### Primitive handling
|
|
||||||
|
|
||||||
Definitions: A primitive is usually a String or Array. There is no serializer
|
|
||||||
defined for them; they will be serialized when the resource is converted to JSON (`as_json` or
|
|
||||||
`to_json`). (The below also applies for any object with no serializer.)
|
|
||||||
|
|
||||||
- ActiveModelSerializers doesn't handle primitives passed to `render json:` at all.
|
|
||||||
|
|
||||||
Internally, if no serializer can be found in the controller, the resource is not decorated by
|
|
||||||
ActiveModelSerializers.
|
|
||||||
|
|
||||||
- However, when a primitive value is an attribute or in a collection, it is not modified.
|
|
||||||
|
|
||||||
When serializing a collection and the collection serializer (CollectionSerializer) cannot
|
|
||||||
identify a serializer for a resource in its collection, it throws [`:no_serializer`](https://github.com/rails-api/active_model_serializers/issues/1191#issuecomment-142327128).
|
|
||||||
For example, when caught by `Reflection#build_association`, and the association value is set directly:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
reflection_options[:virtual_value] = association_value.try(:as_json) || association_value
|
|
||||||
```
|
|
||||||
|
|
||||||
(which is called by the adapter as `serializer.associations(*)`.)
|
|
||||||
|
|
||||||
### How options are parsed
|
|
||||||
|
|
||||||
High-level overview:
|
|
||||||
|
|
||||||
- For a **collection**
|
|
||||||
- `:serializer` specifies the collection serializer and
|
|
||||||
- `:each_serializer` specifies the serializer for each resource in the collection.
|
|
||||||
- For a **single resource**, the `:serializer` option is the resource serializer.
|
|
||||||
- Options are partitioned in serializer options and adapter options. Keys for adapter options are specified by
|
|
||||||
[`ADAPTER_OPTION_KEYS`](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model_serializers/serializable_resource.rb#L5).
|
|
||||||
The remaining options are serializer options.
|
|
||||||
|
|
||||||
Details:
|
|
||||||
|
|
||||||
1. **ActionController::Serialization**
|
|
||||||
1. `serializable_resource = ActiveModelSerializers::SerializableResource.new(resource, options)`
|
|
||||||
1. `options` are partitioned into `adapter_opts` and everything else (`serializer_opts`).
|
|
||||||
The `adapter_opts` keys are defined in [`ActiveModelSerializers::SerializableResource::ADAPTER_OPTION_KEYS`](lib/active_model_serializers/serializable_resource.rb#L5).
|
|
||||||
1. **ActiveModelSerializers::SerializableResource**
|
|
||||||
1. `if serializable_resource.serializer?` (there is a serializer for the resource, and an adapter is used.)
|
|
||||||
- Where `serializer?` is `use_adapter? && !!(serializer)`
|
|
||||||
- Where `use_adapter?`: 'True when no explicit adapter given, or explicit value is truthy (non-nil);
|
|
||||||
False when explicit adapter is falsy (nil or false)'
|
|
||||||
- Where `serializer`:
|
|
||||||
1. from explicit `:serializer` option, else
|
|
||||||
2. implicitly from resource `ActiveModel::Serializer.serializer_for(resource)`
|
|
||||||
1. A side-effect of checking `serializer` is:
|
|
||||||
- The `:serializer` option is removed from the serializer_opts hash
|
|
||||||
- If the `:each_serializer` option is present, it is removed from the serializer_opts hash and set as the `:serializer` option
|
|
||||||
1. The serializer and adapter are created as
|
|
||||||
1. `serializer_instance = serializer.new(resource, serializer_opts)`
|
|
||||||
2. `adapter_instance = ActiveModel::Serializer::Adapter.create(serializer_instance, adapter_opts)`
|
|
||||||
1. **ActiveModel::Serializer::CollectionSerializer#new**
|
|
||||||
1. If the `serializer_instance` was a `CollectionSerializer` and the `:serializer` serializer_opts
|
|
||||||
is present, then [that serializer is passed into each resource](https://github.com/rails-api/active_model_serializers/blob/a54d237e2828fe6bab1ea5dfe6360d4ecc8214cd/lib/active_model/serializer/array_serializer.rb#L14-L16).
|
|
||||||
1. **ActiveModel::Serializer#attributes** is used by the adapter to get the attributes for
|
|
||||||
resource as defined by the serializer.
|
|
||||||
|
|
||||||
(In Rails, the `options` are also passed to the `as_json(options)` or `to_json(options)`
|
|
||||||
methods on the resource serialization by the Rails JSON renderer. They are, therefore, important
|
|
||||||
to know about, but not part of ActiveModelSerializers.)
|
|
||||||
|
|
||||||
### What does a 'serializable resource' look like?
|
|
||||||
|
|
||||||
- An `ActiveRecord::Base` object.
|
|
||||||
- Any Ruby object that passes the
|
|
||||||
[Lint](http://www.rubydoc.info/github/rails-api/active_model_serializers/ActiveModel/Serializer/Lint/Tests)
|
|
||||||
[code](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model/serializer/lint.rb).
|
|
||||||
|
|
||||||
ActiveModelSerializers provides a
|
|
||||||
[`ActiveModelSerializers::Model`](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model_serializers/model.rb),
|
|
||||||
which is a simple serializable PORO (Plain-Old Ruby Object).
|
|
||||||
|
|
||||||
`ActiveModelSerializers::Model` may be used either as a reference implementation, or in production code.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class MyModel < ActiveModelSerializers::Model
|
|
||||||
attributes :id, :name, :level
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
The default serializer for `MyModel` would be `MyModelSerializer` whether MyModel is an
|
|
||||||
ActiveRecord::Base object or not.
|
|
||||||
|
|
||||||
Outside of the controller the rules are **exactly** the same as for records. For example:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
render json: MyModel.new(level: 'awesome'), adapter: :json
|
|
||||||
```
|
|
||||||
|
|
||||||
would be serialized the same as
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
ActiveModelSerializers::SerializableResource.new(MyModel.new(level: 'awesome'), adapter: :json).as_json
|
|
||||||
```
|
|
||||||
|
|
||||||
## Semantic Versioning
|
## Semantic Versioning
|
||||||
|
|
||||||
|
|||||||
140
Rakefile
140
Rakefile
@ -1,74 +1,78 @@
|
|||||||
begin
|
# frozen_string_literal: true
|
||||||
require 'bundler/setup'
|
require 'English'
|
||||||
rescue LoadError
|
require 'rake/clean'
|
||||||
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
|
||||||
|
GEMSPEC = 'ams.gemspec'
|
||||||
|
require File.join(File.dirname(__FILE__), 'lib', 'ams', 'version')
|
||||||
|
VERSION = AMS::VERSION
|
||||||
|
VERSION_TAG = "AMS_#{VERSION}"
|
||||||
|
GEMPATH = "AMS-#{VERSION}.gem"
|
||||||
|
CLEAN.include(GEMPATH)
|
||||||
|
CLOBBER << GEMPATH
|
||||||
|
|
||||||
|
SOURCE_FILES = Rake::FileList.new(GEMSPEC)
|
||||||
|
|
||||||
|
rule '.gem' => '.gemspec' do
|
||||||
|
sh "gem build -V #{GEMSPEC}"
|
||||||
end
|
end
|
||||||
begin
|
|
||||||
require 'simplecov'
|
desc 'build gem'
|
||||||
rescue LoadError # rubocop:disable Lint/HandleExceptions
|
task build: [:clobber, SOURCE_FILES.ext('.gem')]
|
||||||
|
|
||||||
|
desc 'install gem'
|
||||||
|
task install: :build do
|
||||||
|
sh "gem install #{GEMPATH}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc 'uninstall gem'
|
||||||
|
task :uninstall do
|
||||||
|
sh 'gem uninstall -aIx AMS'
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'test install message'
|
||||||
|
task :test_install do
|
||||||
|
gemspec = Gem::Specification.load(GEMSPEC)
|
||||||
|
puts "You should see post install message '#{gemspec.post_install_message}' below:"
|
||||||
|
begin
|
||||||
|
Rake::Task['install'].invoke
|
||||||
|
ensure
|
||||||
|
Rake::Task['uninstall'].invoke
|
||||||
|
end
|
||||||
|
puts "#{GEMSPEC} => #{GEMPATH}"
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'abort when repo not clean or has uncommited code'
|
||||||
|
task :assert_clean_repo do
|
||||||
|
sh 'git diff --exit-code'
|
||||||
|
abort 'Git repo not clean' unless $CHILD_STATUS.success?
|
||||||
|
sh 'git diff-index --quiet --cached HEAD'
|
||||||
|
abort 'Git repo not commited' unless $CHILD_STATUS.success?
|
||||||
|
end
|
||||||
|
|
||||||
|
task push_and_tag: [:build] do
|
||||||
|
sh "gem push #{GEMPATH}"
|
||||||
|
if $CHILD_STATUS.success?
|
||||||
|
Rake::Task['tag_globally'].invoke
|
||||||
|
else
|
||||||
|
abort 'tagging aborted; pushing gem failed.'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
task :tag_globally do
|
||||||
|
sh "git tag -a -m \"Version #{VERSION}\" #{VERSION_TAG}"
|
||||||
|
STDOUT.puts "Tagged #{VERSION_TAG}."
|
||||||
|
sh 'git push'
|
||||||
|
sh 'git push --tags'
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Release'
|
||||||
|
task release: [:assert_clean_repo, :push_and_tag]
|
||||||
|
|
||||||
import('lib/tasks/rubocop.rake')
|
import('lib/tasks/rubocop.rake')
|
||||||
|
import('lib/tasks/doc.rake')
|
||||||
|
import('lib/tasks/test.rake')
|
||||||
|
|
||||||
Bundler::GemHelper.install_tasks
|
task default: [:test, :rubocop]
|
||||||
|
|
||||||
require 'yard'
|
|
||||||
|
|
||||||
namespace :yard do
|
|
||||||
YARD::Rake::YardocTask.new(:doc) do |t|
|
|
||||||
t.stats_options = ['--list-undoc']
|
|
||||||
end
|
|
||||||
|
|
||||||
desc 'start a gem server'
|
|
||||||
task :server do
|
|
||||||
sh 'bundle exec yard server --gems'
|
|
||||||
end
|
|
||||||
|
|
||||||
desc 'use Graphviz to generate dot graph'
|
|
||||||
task :graph do
|
|
||||||
output_file = 'doc/erd.dot'
|
|
||||||
sh "bundle exec yard graph --protected --full --dependencies > #{output_file}"
|
|
||||||
puts 'open doc/erd.dot if you have graphviz installed'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
require 'rake/testtask'
|
|
||||||
|
|
||||||
Rake::TestTask.new(:test) do |t|
|
|
||||||
t.libs << 'lib'
|
|
||||||
t.libs << 'test'
|
|
||||||
t.pattern = 'test/**/*_test.rb'
|
|
||||||
t.ruby_opts = ['-r./test/test_helper.rb']
|
|
||||||
t.ruby_opts << ' -w' unless ENV['NO_WARN'] == 'true'
|
|
||||||
t.verbose = true
|
|
||||||
end
|
|
||||||
|
|
||||||
desc 'Run isolated tests'
|
|
||||||
task isolated: ['test:isolated']
|
|
||||||
namespace :test do
|
|
||||||
task :isolated do
|
|
||||||
desc 'Run isolated tests for Railtie'
|
|
||||||
require 'shellwords'
|
|
||||||
dir = File.dirname(__FILE__)
|
|
||||||
dir = Shellwords.shellescape(dir)
|
|
||||||
isolated_test_files = FileList['test/**/*_test_isolated.rb']
|
|
||||||
# https://github.com/rails/rails/blob/3d590add45/railties/lib/rails/generators/app_base.rb#L345-L363
|
|
||||||
_bundle_command = Gem.bin_path('bundler', 'bundle')
|
|
||||||
require 'bundler'
|
|
||||||
Bundler.with_clean_env do
|
|
||||||
isolated_test_files.all? do |test_file|
|
|
||||||
command = "-w -I#{dir}/lib -I#{dir}/test #{Shellwords.shellescape(test_file)}"
|
|
||||||
full_command = %("#{Gem.ruby}" #{command})
|
|
||||||
system(full_command)
|
|
||||||
end or fail 'Failures' # rubocop:disable Style/AndOr
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if ENV['RAILS_VERSION'].to_s > '4.0' && RUBY_ENGINE == 'ruby'
|
|
||||||
task default: [:isolated, :test, :rubocop]
|
|
||||||
else
|
|
||||||
task default: [:test, :rubocop]
|
|
||||||
end
|
|
||||||
|
|
||||||
desc 'CI test task'
|
desc 'CI test task'
|
||||||
task ci: [:default]
|
task ci: [:default, 'doc:stats']
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
# coding: utf-8
|
|
||||||
lib = File.expand_path('../lib', __FILE__)
|
|
||||||
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
||||||
require 'active_model/serializer/version'
|
|
||||||
|
|
||||||
Gem::Specification.new do |spec|
|
|
||||||
spec.name = 'active_model_serializers'
|
|
||||||
spec.version = ActiveModel::Serializer::VERSION
|
|
||||||
spec.platform = Gem::Platform::RUBY
|
|
||||||
spec.authors = ['Steve Klabnik']
|
|
||||||
spec.email = ['steve@steveklabnik.com']
|
|
||||||
spec.summary = 'Conventions-based JSON generation for Rails.'
|
|
||||||
spec.description = 'ActiveModel::Serializers allows you to generate your JSON in an object-oriented and convention-driven manner.'
|
|
||||||
spec.homepage = 'https://github.com/rails-api/active_model_serializers'
|
|
||||||
spec.license = 'MIT'
|
|
||||||
|
|
||||||
spec.files = `git ls-files -z`.split("\x0")
|
|
||||||
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
||||||
spec.require_paths = ['lib']
|
|
||||||
spec.executables = []
|
|
||||||
|
|
||||||
spec.required_ruby_version = '>= 2.1'
|
|
||||||
|
|
||||||
rails_versions = ['>= 4.1', '< 6']
|
|
||||||
spec.add_runtime_dependency 'activemodel', rails_versions
|
|
||||||
# 'activesupport', rails_versions
|
|
||||||
# 'builder'
|
|
||||||
|
|
||||||
spec.add_runtime_dependency 'actionpack', rails_versions
|
|
||||||
# 'activesupport', rails_versions
|
|
||||||
# 'rack'
|
|
||||||
# 'rack-test', '~> 0.6.2'
|
|
||||||
|
|
||||||
spec.add_development_dependency 'railties', rails_versions
|
|
||||||
# 'activesupport', rails_versions
|
|
||||||
# 'actionpack', rails_versions
|
|
||||||
# 'rake', '>= 0.8.7'
|
|
||||||
|
|
||||||
# 'activesupport', rails_versions
|
|
||||||
# 'i18n,
|
|
||||||
# 'tzinfo'
|
|
||||||
# 'minitest'
|
|
||||||
# 'thread_safe'
|
|
||||||
|
|
||||||
spec.add_runtime_dependency 'jsonapi-renderer', ['>= 0.1.1.beta1', '< 0.2']
|
|
||||||
spec.add_runtime_dependency 'case_transform', '>= 0.2'
|
|
||||||
|
|
||||||
spec.add_development_dependency 'activerecord', rails_versions
|
|
||||||
# arel
|
|
||||||
# activesupport
|
|
||||||
# activemodel
|
|
||||||
|
|
||||||
# Soft dependency for pagination
|
|
||||||
spec.add_development_dependency 'kaminari', ' ~> 0.16.3'
|
|
||||||
spec.add_development_dependency 'will_paginate', '~> 3.0', '>= 3.0.7'
|
|
||||||
|
|
||||||
spec.add_development_dependency 'bundler', '~> 1.6'
|
|
||||||
spec.add_development_dependency 'simplecov', '~> 0.11'
|
|
||||||
spec.add_development_dependency 'timecop', '~> 0.7'
|
|
||||||
spec.add_development_dependency 'grape', ['>= 0.13', '< 1.0']
|
|
||||||
spec.add_development_dependency 'json_schema'
|
|
||||||
spec.add_development_dependency 'rake', ['>= 10.0', '< 12.0']
|
|
||||||
end
|
|
||||||
49
ams.gemspec
Normal file
49
ams.gemspec
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
lib = File.expand_path('../lib', __FILE__)
|
||||||
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||||
|
require 'ams/version'
|
||||||
|
|
||||||
|
Gem::Specification.new do |spec|
|
||||||
|
spec.name = 'ams'
|
||||||
|
spec.version = AMS::VERSION
|
||||||
|
spec.platform = Gem::Platform::RUBY
|
||||||
|
spec.authors = ['Benjamin Fleischer']
|
||||||
|
spec.email = ['dev@benjaminfleischer.com']
|
||||||
|
spec.summary = 'Conventions-based JSON generation for Rails.'
|
||||||
|
spec.description = 'AMS allows you to generate your JSON in an object-oriented and convention-driven manner.'
|
||||||
|
spec.homepage = 'https://github.com/bf4/ams'
|
||||||
|
spec.license = 'MIT'
|
||||||
|
|
||||||
|
spec.metadata = {
|
||||||
|
'homepage_uri' => spec.homepage,
|
||||||
|
# 'wiki_uri' => nil,
|
||||||
|
# 'documentation_uri' => nil,
|
||||||
|
# 'mailing_list_uri' => nil,
|
||||||
|
'source_code_uri' => spec.homepage,
|
||||||
|
'bug_tracker_uri' => spec.homepage + '/issues'
|
||||||
|
}
|
||||||
|
|
||||||
|
spec.files = `git ls-files -z`.split("\x0")
|
||||||
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
||||||
|
spec.require_paths = ['lib']
|
||||||
|
spec.executables = []
|
||||||
|
spec.extensions = []
|
||||||
|
|
||||||
|
spec.required_ruby_version = '>= 2.1'
|
||||||
|
|
||||||
|
rails_versions = ['>= 4.1', '< 6']
|
||||||
|
spec.add_runtime_dependency 'activemodel', rails_versions
|
||||||
|
# 'activesupport', rails_versions
|
||||||
|
# 'builder'
|
||||||
|
|
||||||
|
# 'activesupport', rails_versions
|
||||||
|
# 'i18n,
|
||||||
|
# 'tzinfo'
|
||||||
|
# 'minitest'
|
||||||
|
# 'thread_safe'
|
||||||
|
|
||||||
|
spec.add_development_dependency 'bundler', '~> 1'
|
||||||
|
spec.add_development_dependency 'simplecov', '~> 0'
|
||||||
|
spec.add_development_dependency 'rake', '~> 12'
|
||||||
|
spec.add_development_dependency 'minitest', '~> 5'
|
||||||
|
end
|
||||||
30
appveyor.yml
30
appveyor.yml
@ -1,30 +0,0 @@
|
|||||||
version: 1.0.{build}-{branch}
|
|
||||||
|
|
||||||
skip_tags: true
|
|
||||||
|
|
||||||
environment:
|
|
||||||
JRUBY_OPTS: "--dev -J-Xmx1024M --debug"
|
|
||||||
matrix:
|
|
||||||
- ruby_version: "Ruby21"
|
|
||||||
- ruby_version: "Ruby21-x64"
|
|
||||||
|
|
||||||
cache:
|
|
||||||
- vendor/bundle
|
|
||||||
|
|
||||||
install:
|
|
||||||
- SET PATH=C:\%ruby_version%\bin;%PATH%
|
|
||||||
- gem update --system
|
|
||||||
- gem uninstall bundler -a -x
|
|
||||||
- gem install bundler -v 1.13.7
|
|
||||||
- bundle env
|
|
||||||
- bundle install --path=vendor/bundle --retry=3 --jobs=3
|
|
||||||
|
|
||||||
before_test:
|
|
||||||
- ruby -v
|
|
||||||
- gem -v
|
|
||||||
- bundle -v
|
|
||||||
|
|
||||||
test_script:
|
|
||||||
- bundle exec rake ci
|
|
||||||
|
|
||||||
build: off
|
|
||||||
171
bin/bench
171
bin/bench
@ -1,171 +0,0 @@
|
|||||||
#!/usr/bin/env ruby
|
|
||||||
# ActiveModelSerializers Benchmark driver
|
|
||||||
# Adapted from
|
|
||||||
# https://github.com/ruby-bench/ruby-bench-suite/blob/8ad567f7e43a044ae48c36833218423bb1e2bd9d/rails/benchmarks/driver.rb
|
|
||||||
require 'bundler'
|
|
||||||
Bundler.setup
|
|
||||||
require 'json'
|
|
||||||
require 'pathname'
|
|
||||||
require 'optparse'
|
|
||||||
require 'digest'
|
|
||||||
require 'pathname'
|
|
||||||
require 'shellwords'
|
|
||||||
require 'logger'
|
|
||||||
require 'English'
|
|
||||||
|
|
||||||
class BenchmarkDriver
|
|
||||||
ROOT = Pathname File.expand_path(File.join('..', '..'), __FILE__)
|
|
||||||
BASE = ENV.fetch('BASE') { ROOT.join('test', 'benchmark') }
|
|
||||||
ESCAPED_BASE = Shellwords.shellescape(BASE)
|
|
||||||
|
|
||||||
def self.benchmark(options)
|
|
||||||
new(options).run
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.parse_argv_and_run(argv = ARGV, options = {})
|
|
||||||
options = {
|
|
||||||
repeat_count: 1,
|
|
||||||
pattern: [],
|
|
||||||
env: 'CACHE_ON=on'
|
|
||||||
}.merge!(options)
|
|
||||||
|
|
||||||
OptionParser.new do |opts|
|
|
||||||
opts.banner = 'Usage: bin/bench [options]'
|
|
||||||
|
|
||||||
opts.on('-r', '--repeat-count [NUM]', 'Run benchmarks [NUM] times taking the best result') do |value|
|
|
||||||
options[:repeat_count] = value.to_i
|
|
||||||
end
|
|
||||||
|
|
||||||
opts.on('-p', '--pattern <PATTERN1,PATTERN2,PATTERN3>', 'Benchmark name pattern') do |value|
|
|
||||||
options[:pattern] = value.split(',')
|
|
||||||
end
|
|
||||||
|
|
||||||
opts.on('-e', '--env <var1=val1,var2=val2,var3=vale>', 'ENV variables to pass in') do |value|
|
|
||||||
options[:env] = value.split(',')
|
|
||||||
end
|
|
||||||
end.parse!(argv)
|
|
||||||
|
|
||||||
benchmark(options)
|
|
||||||
end
|
|
||||||
|
|
||||||
attr_reader :commit_hash, :base
|
|
||||||
|
|
||||||
# Based on logfmt:
|
|
||||||
# https://www.brandur.org/logfmt
|
|
||||||
# For more complete implementation see:
|
|
||||||
# see https://github.com/arachnid-cb/logfmtr/blob/master/lib/logfmtr/base.rb
|
|
||||||
# For usage see:
|
|
||||||
# https://blog.codeship.com/logfmt-a-log-format-thats-easy-to-read-and-write/
|
|
||||||
# https://engineering.heroku.com/blogs/2014-09-05-hutils-explore-your-structured-data-logs/
|
|
||||||
# For Ruby parser see:
|
|
||||||
# https://github.com/cyberdelia/logfmt-ruby
|
|
||||||
def self.summary_logger(device = 'output.txt')
|
|
||||||
require 'time'
|
|
||||||
logger = Logger.new(device)
|
|
||||||
logger.level = Logger::INFO
|
|
||||||
logger.formatter = proc { |severity, datetime, progname, msg|
|
|
||||||
msg = "'#{msg}'"
|
|
||||||
"level=#{severity} time=#{datetime.utc.iso8601(6)} pid=#{Process.pid} progname=#{progname} msg=#{msg}#{$INPUT_RECORD_SEPARATOR}"
|
|
||||||
}
|
|
||||||
logger
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.stdout_logger
|
|
||||||
logger = Logger.new(STDOUT)
|
|
||||||
logger.level = Logger::INFO
|
|
||||||
logger.formatter = proc { |_, _, _, msg| "#{msg}#{$INPUT_RECORD_SEPARATOR}" }
|
|
||||||
logger
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(options)
|
|
||||||
@writer = ENV['SUMMARIZE'] ? self.class.summary_logger : self.class.stdout_logger
|
|
||||||
@repeat_count = options[:repeat_count]
|
|
||||||
@pattern = options[:pattern]
|
|
||||||
@commit_hash = options.fetch(:commit_hash) { `git rev-parse --short HEAD`.chomp }
|
|
||||||
@base = options.fetch(:base) { ESCAPED_BASE }
|
|
||||||
@env = Array(options[:env]).join(' ')
|
|
||||||
@rubyopt = options[:rubyopt] # TODO: rename
|
|
||||||
end
|
|
||||||
|
|
||||||
def run
|
|
||||||
files.each do |path|
|
|
||||||
next if !@pattern.empty? && /#{@pattern.join('|')}/ !~ File.basename(path)
|
|
||||||
run_single(Shellwords.shellescape(path))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def files
|
|
||||||
Dir[File.join(base, 'bm_*')]
|
|
||||||
end
|
|
||||||
|
|
||||||
def run_single(path)
|
|
||||||
script = "RAILS_ENV=production #{@env} ruby #{@rubyopt} #{path}"
|
|
||||||
environment = `ruby -v`.chomp.strip[/\d+\.\d+\.\d+\w+/]
|
|
||||||
|
|
||||||
runs_output = measure(script)
|
|
||||||
if runs_output.empty?
|
|
||||||
results = { error: :no_results }
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
results = {}
|
|
||||||
results['commit_hash'] = commit_hash
|
|
||||||
results['version'] = runs_output.first['version']
|
|
||||||
results['rails_version'] = runs_output.first['rails_version']
|
|
||||||
results['benchmark_run[environment]'] = environment
|
|
||||||
results['runs'] = []
|
|
||||||
|
|
||||||
runs_output.each do |output|
|
|
||||||
results['runs'] << {
|
|
||||||
'benchmark_type[category]' => output['label'],
|
|
||||||
'benchmark_run[result][iterations_per_second]' => output['iterations_per_second'].round(3),
|
|
||||||
'benchmark_run[result][total_allocated_objects_per_iteration]' => output['total_allocated_objects_per_iteration']
|
|
||||||
}
|
|
||||||
end
|
|
||||||
ensure
|
|
||||||
results && report(results)
|
|
||||||
end
|
|
||||||
|
|
||||||
def report(results)
|
|
||||||
@writer.info { 'Benchmark results:' }
|
|
||||||
@writer.info { JSON.pretty_generate(results) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def summarize(result)
|
|
||||||
puts "#{result['label']} #{result['iterations_per_second']}/ips; #{result['total_allocated_objects_per_iteration']} objects"
|
|
||||||
end
|
|
||||||
|
|
||||||
# FIXME: ` provides the full output but it'll return failed output as well.
|
|
||||||
def measure(script)
|
|
||||||
results = Hash.new { |h, k| h[k] = [] }
|
|
||||||
|
|
||||||
@repeat_count.times do
|
|
||||||
output = sh(script)
|
|
||||||
output.each_line do |line|
|
|
||||||
next if line.nil?
|
|
||||||
begin
|
|
||||||
result = JSON.parse(line)
|
|
||||||
rescue JSON::ParserError
|
|
||||||
result = { error: line } # rubocop:disable Lint/UselessAssignment
|
|
||||||
else
|
|
||||||
summarize(result)
|
|
||||||
results[result['label']] << result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
results.map do |_, bm_runs|
|
|
||||||
bm_runs.sort_by do |run|
|
|
||||||
run['iterations_per_second']
|
|
||||||
end.last
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def sh(cmd)
|
|
||||||
`#{cmd}`
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
BenchmarkDriver.parse_argv_and_run if $PROGRAM_NAME == __FILE__
|
|
||||||
@ -1,316 +0,0 @@
|
|||||||
#!/usr/bin/env ruby
|
|
||||||
require 'fileutils'
|
|
||||||
require 'pathname'
|
|
||||||
require 'shellwords'
|
|
||||||
require 'English'
|
|
||||||
|
|
||||||
############################
|
|
||||||
# USAGE
|
|
||||||
#
|
|
||||||
# bundle exec bin/bench_regression <ref1> <ref2>
|
|
||||||
# <ref1> defaults to the current branch
|
|
||||||
# <ref2> defaults to the master branch
|
|
||||||
# bundle exec bin/bench_regression current # will run on the current branch
|
|
||||||
# bundle exec bin/bench_regression revisions 792fb8a90 master # every revision inclusive
|
|
||||||
# bundle exec bin/bench_regression 792fb8a90 master --repeat-count 2 --env CACHE_ON=off
|
|
||||||
# bundle exec bin/bench_regression vendor
|
|
||||||
###########################
|
|
||||||
|
|
||||||
class BenchRegression
|
|
||||||
ROOT = Pathname File.expand_path(File.join(*['..', '..']), __FILE__)
|
|
||||||
TMP_DIR_NAME = File.join('tmp', 'bench')
|
|
||||||
TMP_DIR = File.join(ROOT, TMP_DIR_NAME)
|
|
||||||
E_TMP_DIR = Shellwords.shellescape(TMP_DIR)
|
|
||||||
load ROOT.join('bin', 'bench')
|
|
||||||
|
|
||||||
attr_reader :source_stasher
|
|
||||||
|
|
||||||
def initialize
|
|
||||||
@source_stasher = SourceStasher.new
|
|
||||||
end
|
|
||||||
|
|
||||||
class SourceStasher
|
|
||||||
attr_reader :gem_require_paths, :gem_paths
|
|
||||||
attr_writer :vendor
|
|
||||||
|
|
||||||
def initialize
|
|
||||||
@gem_require_paths = []
|
|
||||||
@gem_paths = []
|
|
||||||
refresh_temp_dir
|
|
||||||
@vendor = false
|
|
||||||
end
|
|
||||||
|
|
||||||
def temp_dir_empty?
|
|
||||||
File.directory?(TMP_DIR) &&
|
|
||||||
Dir[File.join(TMP_DIR, '*')].none?
|
|
||||||
end
|
|
||||||
|
|
||||||
def empty_temp_dir
|
|
||||||
return if @vendor
|
|
||||||
return if temp_dir_empty?
|
|
||||||
FileUtils.mkdir_p(TMP_DIR)
|
|
||||||
Dir[File.join(TMP_DIR, '*')].each do |file|
|
|
||||||
if File.directory?(file)
|
|
||||||
FileUtils.rm_rf(file)
|
|
||||||
else
|
|
||||||
FileUtils.rm(file)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def fill_temp_dir
|
|
||||||
vendor_files(Dir[File.join(ROOT, 'test', 'benchmark', '*.{rb,ru}')])
|
|
||||||
# vendor_file(File.join('bin', 'bench'))
|
|
||||||
housekeeping { empty_temp_dir }
|
|
||||||
vendor_gem('benchmark-ips')
|
|
||||||
end
|
|
||||||
|
|
||||||
def vendor_files(files)
|
|
||||||
files.each do |file|
|
|
||||||
vendor_file(file)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def vendor_file(file)
|
|
||||||
FileUtils.cp(file, File.join(TMP_DIR, File.basename(file)))
|
|
||||||
end
|
|
||||||
|
|
||||||
def vendor_gem(gem_name)
|
|
||||||
directory_name = `bundle exec gem unpack benchmark-ips --target=#{E_TMP_DIR}`[/benchmark-ips.+\d/]
|
|
||||||
gem_paths << File.join(TMP_DIR, directory_name)
|
|
||||||
gem_require_paths << File.join(TMP_DIR_NAME, directory_name, 'lib')
|
|
||||||
housekeeping { remove_vendored_gems }
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove_vendored_gems
|
|
||||||
return if @vendor
|
|
||||||
FileUtils.rm_rf(*gem_paths)
|
|
||||||
end
|
|
||||||
|
|
||||||
def refresh_temp_dir
|
|
||||||
empty_temp_dir
|
|
||||||
fill_temp_dir
|
|
||||||
end
|
|
||||||
|
|
||||||
def housekeeping
|
|
||||||
at_exit { yield }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module RevisionMethods
|
|
||||||
module_function
|
|
||||||
def current_branch
|
|
||||||
@current_branch ||= `cat .git/HEAD | cut -d/ -f3,4,5`.chomp
|
|
||||||
end
|
|
||||||
|
|
||||||
def current_revision
|
|
||||||
`git rev-parse --short HEAD`.chomp
|
|
||||||
end
|
|
||||||
|
|
||||||
def revision_description(rev)
|
|
||||||
`git log --oneline -1 #{rev}`.chomp
|
|
||||||
end
|
|
||||||
|
|
||||||
def revisions(start_ref, end_ref)
|
|
||||||
cmd = "git rev-list --reverse #{start_ref}..#{end_ref}"
|
|
||||||
`#{cmd}`.chomp.split("\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
def checkout_ref(ref)
|
|
||||||
`git checkout #{ref}`.chomp
|
|
||||||
if $CHILD_STATUS
|
|
||||||
STDERR.puts "Checkout failed: #{ref}, #{$CHILD_STATUS.exitstatus}" unless $CHILD_STATUS.success?
|
|
||||||
$CHILD_STATUS.success?
|
|
||||||
else
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def clean_head
|
|
||||||
system('git reset --hard --quiet')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
module ShellMethods
|
|
||||||
|
|
||||||
def sh(cmd)
|
|
||||||
puts cmd
|
|
||||||
# system(cmd)
|
|
||||||
run(cmd)
|
|
||||||
# env = {}
|
|
||||||
# # out = STDOUT
|
|
||||||
# pid = spawn(env, cmd)
|
|
||||||
# Process.wait(pid)
|
|
||||||
# pid = fork do
|
|
||||||
# exec cmd
|
|
||||||
# end
|
|
||||||
# Process.waitpid2(pid)
|
|
||||||
# puts $CHILD_STATUS.exitstatus
|
|
||||||
end
|
|
||||||
|
|
||||||
require 'pty'
|
|
||||||
# should consider trapping SIGINT in here
|
|
||||||
def run(cmd)
|
|
||||||
puts cmd
|
|
||||||
child_process = ''
|
|
||||||
result = ''
|
|
||||||
# http://stackoverflow.com/a/1162850
|
|
||||||
# stream output of subprocess
|
|
||||||
begin
|
|
||||||
PTY.spawn(cmd) do |stdin, _stdout, pid|
|
|
||||||
begin
|
|
||||||
# Do stuff with the output here. Just printing to show it works
|
|
||||||
stdin.each do |line|
|
|
||||||
print line
|
|
||||||
result << line
|
|
||||||
end
|
|
||||||
child_process = PTY.check(pid)
|
|
||||||
rescue Errno::EIO
|
|
||||||
puts 'Errno:EIO error, but this probably just means ' \
|
|
||||||
'that the process has finished giving output'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
rescue PTY::ChildExited
|
|
||||||
puts 'The child process exited!'
|
|
||||||
end
|
|
||||||
unless (child_process && child_process.success?)
|
|
||||||
exitstatus = child_process.exitstatus
|
|
||||||
puts "FAILED: #{child_process.pid} exited with status #{exitstatus.inspect} due to failed command #{cmd}"
|
|
||||||
exit exitstatus || 1
|
|
||||||
end
|
|
||||||
result
|
|
||||||
end
|
|
||||||
|
|
||||||
def bundle(ref)
|
|
||||||
system("rm -f Gemfile.lock")
|
|
||||||
# This is absolutely critical for bundling to work
|
|
||||||
Bundler.with_clean_env do
|
|
||||||
system("bundle check ||
|
|
||||||
bundle install --local ||
|
|
||||||
bundle install ||
|
|
||||||
bundle update")
|
|
||||||
end
|
|
||||||
|
|
||||||
# if $CHILD_STATUS
|
|
||||||
# STDERR.puts "Bundle failed at: #{ref}, #{$CHILD_STATUS.exitstatus}" unless $CHILD_STATUS.success?
|
|
||||||
# $CHILD_STATUS.success?
|
|
||||||
# else
|
|
||||||
# false
|
|
||||||
# end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
include ShellMethods
|
|
||||||
include RevisionMethods
|
|
||||||
|
|
||||||
def benchmark_refs(ref1: nil, ref2: nil, cmd:)
|
|
||||||
checking_out = false
|
|
||||||
ref0 = current_branch
|
|
||||||
ref1 ||= current_branch
|
|
||||||
ref2 ||= 'master'
|
|
||||||
p [ref0, ref1, ref2, current_revision]
|
|
||||||
|
|
||||||
run_benchmark_at_ref(cmd, ref1)
|
|
||||||
p [ref0, ref1, ref2, current_revision]
|
|
||||||
run_benchmark_at_ref(cmd, ref2)
|
|
||||||
p [ref0, ref1, ref2, current_revision]
|
|
||||||
|
|
||||||
checking_out = true
|
|
||||||
checkout_ref(ref0)
|
|
||||||
rescue Exception # rubocop:disable Lint/RescueException
|
|
||||||
STDERR.puts "[ERROR] #{$!.message}"
|
|
||||||
checkout_ref(ref0) unless checking_out
|
|
||||||
raise
|
|
||||||
end
|
|
||||||
|
|
||||||
def benchmark_revisions(ref1: nil, ref2: nil, cmd:)
|
|
||||||
checking_out = false
|
|
||||||
ref0 = current_branch
|
|
||||||
ref1 ||= current_branch
|
|
||||||
ref2 ||= 'master'
|
|
||||||
|
|
||||||
revisions(ref1, ref2).each do |rev|
|
|
||||||
STDERR.puts "Checking out: #{revision_description(rev)}"
|
|
||||||
|
|
||||||
run_benchmark_at_ref(cmd, rev)
|
|
||||||
clean_head
|
|
||||||
end
|
|
||||||
checking_out = true
|
|
||||||
checkout_ref(ref0)
|
|
||||||
rescue Exception # rubocop:disable Lint/RescueException
|
|
||||||
STDERR.puts "[ERROR]: #{$!.message}"
|
|
||||||
checkout_ref(ref0) unless checking_out
|
|
||||||
raise
|
|
||||||
end
|
|
||||||
|
|
||||||
def run_benchmark_at_ref(cmd, ref)
|
|
||||||
checkout_ref(ref)
|
|
||||||
run_benchmark(cmd, ref)
|
|
||||||
end
|
|
||||||
|
|
||||||
def run_benchmark(cmd, ref = nil)
|
|
||||||
ref ||= current_revision
|
|
||||||
bundle(ref) &&
|
|
||||||
benchmark_tests(cmd, ref)
|
|
||||||
end
|
|
||||||
|
|
||||||
def benchmark_tests(cmd, ref)
|
|
||||||
base = E_TMP_DIR
|
|
||||||
# cmd.sub('bin/bench', 'tmp/revision_runner/bench')
|
|
||||||
# bundle = Gem.bin('bunle'
|
|
||||||
# Bundler.with_clean_env(&block)
|
|
||||||
|
|
||||||
# cmd = Shellwords.shelljoin(cmd)
|
|
||||||
# cmd = "COMMIT_HASH=#{ref} BASE=#{base} bundle exec ruby -rbenchmark/ips #{cmd}"
|
|
||||||
# Add vendoring benchmark/ips to load path
|
|
||||||
|
|
||||||
# CURRENT THINKING: IMPORTANT
|
|
||||||
# Pass into require statement as RUBYOPTS i.e. via env rather than command line argument
|
|
||||||
# otherwise, have a 'fast ams benchmarking' module that extends benchmarkings to add the 'ams'
|
|
||||||
# method but doesn't depend on benchmark-ips
|
|
||||||
options = {
|
|
||||||
commit_hash: ref,
|
|
||||||
base: base,
|
|
||||||
rubyopt: Shellwords.shellescape("-Ilib:#{source_stasher.gem_require_paths.join(':')}")
|
|
||||||
}
|
|
||||||
BenchmarkDriver.parse_argv_and_run(ARGV.dup, options)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if $PROGRAM_NAME == __FILE__
|
|
||||||
benchmarking = BenchRegression.new
|
|
||||||
|
|
||||||
case ARGV[0]
|
|
||||||
when 'current'
|
|
||||||
# Run current branch only
|
|
||||||
|
|
||||||
# super simple command line parsing
|
|
||||||
args = ARGV.dup
|
|
||||||
_ = args.shift # remove 'current' from args
|
|
||||||
cmd = args
|
|
||||||
benchmarking.run_benchmark(cmd)
|
|
||||||
when 'revisions'
|
|
||||||
# Runs on every revision
|
|
||||||
|
|
||||||
# super simple command line parsing
|
|
||||||
args = ARGV.dup
|
|
||||||
_ = args.shift
|
|
||||||
ref1 = args.shift # remove 'revisions' from args
|
|
||||||
ref2 = args.shift
|
|
||||||
cmd = args
|
|
||||||
benchmarking.benchmark_revisions(ref1: ref1, ref2: ref2, cmd: cmd)
|
|
||||||
when 'vendor'
|
|
||||||
# Just prevents vendored files from being cleaned up
|
|
||||||
# at exit. (They are vendored at initialize.)
|
|
||||||
benchmarking.source_stasher.vendor = true
|
|
||||||
else
|
|
||||||
# Default: Compare current_branch to master
|
|
||||||
# Optionally: pass in two refs as args to `bin/bench_regression`
|
|
||||||
# TODO: Consider checking across more revisions, to automatically find problems.
|
|
||||||
|
|
||||||
# super simple command line parsing
|
|
||||||
args = ARGV.dup
|
|
||||||
ref1 = args.shift
|
|
||||||
ref2 = args.shift
|
|
||||||
cmd = args
|
|
||||||
benchmarking.benchmark_refs(ref1: ref1, ref2: ref2, cmd: cmd)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
17
bin/rake
Executable file
17
bin/rake
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
#
|
||||||
|
# This file was generated by Bundler.
|
||||||
|
#
|
||||||
|
# The application 'rake' is installed as part of a gem, and
|
||||||
|
# this file is here to facilitate running it.
|
||||||
|
#
|
||||||
|
|
||||||
|
require "pathname"
|
||||||
|
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
||||||
|
Pathname.new(__FILE__).realpath)
|
||||||
|
|
||||||
|
require "rubygems"
|
||||||
|
require "bundler/setup"
|
||||||
|
|
||||||
|
load Gem.bin_path("rake", "rake")
|
||||||
@ -14,12 +14,12 @@ set -e
|
|||||||
case $1 in
|
case $1 in
|
||||||
-A)
|
-A)
|
||||||
echo "Rubocop autocorrect is ON" >&2
|
echo "Rubocop autocorrect is ON" >&2
|
||||||
bundle exec rake -f lib/tasks/rubocop.rake rubocop:auto_correct
|
bin/rake -f lib/tasks/rubocop.rake rubocop:auto_correct
|
||||||
;;
|
;;
|
||||||
|
|
||||||
-t)
|
-t)
|
||||||
echo "Rubocop is generating a new TODO" >&2
|
echo "Rubocop is generating a new TODO" >&2
|
||||||
bundle exec rake -f lib/tasks/rubocop.rake rubocop:auto_gen_config
|
bin/rake -f lib/tasks/rubocop.rake rubocop:auto_gen_config
|
||||||
;;
|
;;
|
||||||
|
|
||||||
-h|--help|help)
|
-h|--help|help)
|
||||||
@ -30,7 +30,7 @@ case $1 in
|
|||||||
# with no args, run vanilla rubocop
|
# with no args, run vanilla rubocop
|
||||||
# else assume we're passing in arbitrary arguments
|
# else assume we're passing in arbitrary arguments
|
||||||
if [ -z "$1" ]; then
|
if [ -z "$1" ]; then
|
||||||
bundle exec rake -f lib/tasks/rubocop.rake rubocop
|
bin/rake -f lib/tasks/rubocop.rake rubocop
|
||||||
else
|
else
|
||||||
bundle exec rubocop "$@"
|
bundle exec rubocop "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -1,39 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
|
|
||||||
start)
|
|
||||||
config="${CONFIG_RU:-test/benchmark/config.ru}"
|
|
||||||
bundle exec ruby -Ilib -S rackup "$config" --daemonize --pid tmp/benchmark_app.pid --warn --server webrick
|
|
||||||
until [ -f 'tmp/benchmark_app.pid' ]; do
|
|
||||||
sleep 0.1 # give it time to start.. I don't know a better way
|
|
||||||
done
|
|
||||||
cat tmp/benchmark_app.pid
|
|
||||||
true
|
|
||||||
;;
|
|
||||||
|
|
||||||
stop)
|
|
||||||
if [ -f 'tmp/benchmark_app.pid' ]; then
|
|
||||||
kill -TERM $(cat tmp/benchmark_app.pid)
|
|
||||||
else
|
|
||||||
echo 'No pidfile'
|
|
||||||
false
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
status)
|
|
||||||
if [ -f 'tmp/benchmark_app.pid' ]; then
|
|
||||||
kill -0 $(cat tmp/benchmark_app.pid)
|
|
||||||
[ "$?" -eq 0 ]
|
|
||||||
else
|
|
||||||
echo 'No pidfile'
|
|
||||||
false
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
*)
|
|
||||||
echo "Usage: $0 [start|stop|status]"
|
|
||||||
;;
|
|
||||||
|
|
||||||
esac
|
|
||||||
17
bin/yard
Executable file
17
bin/yard
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
#
|
||||||
|
# This file was generated by Bundler.
|
||||||
|
#
|
||||||
|
# The application 'yard' is installed as part of a gem, and
|
||||||
|
# this file is here to facilitate running it.
|
||||||
|
#
|
||||||
|
|
||||||
|
require "pathname"
|
||||||
|
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
||||||
|
Pathname.new(__FILE__).realpath)
|
||||||
|
|
||||||
|
require "rubygems"
|
||||||
|
require "bundler/setup"
|
||||||
|
|
||||||
|
load Gem.bin_path("yard", "yard")
|
||||||
@ -1,41 +0,0 @@
|
|||||||
# Docs - ActiveModel::Serializer 0.10.x
|
|
||||||
|
|
||||||
This is the documentation of ActiveModelSerializers, it's focused on the **0.10.x version.**
|
|
||||||
|
|
||||||
-----
|
|
||||||
|
|
||||||
## General
|
|
||||||
|
|
||||||
- [Getting Started](general/getting_started.md)
|
|
||||||
- [Configuration Options](general/configuration_options.md)
|
|
||||||
- [Serializers](general/serializers.md)
|
|
||||||
- [Adapters](general/adapters.md)
|
|
||||||
- [Rendering](general/rendering.md)
|
|
||||||
- [Caching](general/caching.md)
|
|
||||||
- [Logging](general/logging.md)
|
|
||||||
- [Deserialization](general/deserialization.md)
|
|
||||||
- [Instrumentation](general/instrumentation.md)
|
|
||||||
- JSON API
|
|
||||||
- [Schema](jsonapi/schema.md)
|
|
||||||
- [Errors](jsonapi/errors.md)
|
|
||||||
|
|
||||||
## How to
|
|
||||||
|
|
||||||
- [How to add root key](howto/add_root_key.md)
|
|
||||||
- [How to add pagination links](howto/add_pagination_links.md)
|
|
||||||
- [How to add relationship links](howto/add_relationship_links.md)
|
|
||||||
- [Using ActiveModelSerializers Outside Of Controllers](howto/outside_controller_use.md)
|
|
||||||
- [Testing ActiveModelSerializers](howto/test.md)
|
|
||||||
- [Passing Arbitrary Options](howto/passing_arbitrary_options.md)
|
|
||||||
- [How to serialize a Plain-Old Ruby Object (PORO)](howto/serialize_poro.md)
|
|
||||||
- [How to upgrade from `0.8` to `0.10` safely](howto/upgrade_from_0_8_to_0_10.md)
|
|
||||||
|
|
||||||
## Integrations
|
|
||||||
|
|
||||||
| Integration | Supported ActiveModelSerializers versions | Gem name and/or link
|
|
||||||
|----|-----|----
|
|
||||||
| Ember.js | 0.9.x | [active-model-adapter](https://github.com/ember-data/active-model-adapter)
|
|
||||||
| Ember.js | 0.10.x + | [docs/integrations/ember-and-json-api.md](integrations/ember-and-json-api.md)
|
|
||||||
| Grape | 0.10.x + | [docs/integrations/grape.md](integrations/grape.md) |
|
|
||||||
| Grape | 0.9.x | https://github.com/jrhe/grape-active_model_serializers/ |
|
|
||||||
| Sinatra | 0.9.x | https://github.com/SauloSilva/sinatra-active-model-serializers/
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
# STYLE
|
|
||||||
|
|
||||||
## Code and comments
|
|
||||||
|
|
||||||
- We are actively working to identify tasks under the label [**Good for New
|
|
||||||
Contributors**](https://github.com/rails-api/active_model_serializers/labels/Good%20for%20New%20Contributors).
|
|
||||||
- [Changelog
|
|
||||||
Missing](https://github.com/rails-api/active_model_serializers/issues?q=label%3A%22Changelog+Missing%22+is%3Aclosed) is
|
|
||||||
an easy way to help out.
|
|
||||||
|
|
||||||
- [Fix a bug](https://github.com/rails-api/active_model_serializers/labels/Ready%20for%20PR).
|
|
||||||
- Ready for PR - A well defined bug, needs someone to PR a fix.
|
|
||||||
- Bug - Anything that is broken.
|
|
||||||
- Regression - A bug that did not exist in previous versions and isn't a new feature (applied in tandem with Bug).
|
|
||||||
- Performance - A performance related issue. We could track this as a bug, but usually these would have slightly lower priority than standard bugs.
|
|
||||||
|
|
||||||
- [Develop new features](https://github.com/rails-api/active_model_serializers/labels/Feature).
|
|
||||||
|
|
||||||
- [Improve code quality](https://codeclimate.com/github/rails-api/active_model_serializers/code?sort=smell_count&sort_direction=desc).
|
|
||||||
|
|
||||||
- [Improve amount of code exercised by tests](https://codeclimate.com/github/rails-api/active_model_serializers/coverage?sort=covered_percent&sort_direction=asc).
|
|
||||||
|
|
||||||
- [Fix RuboCop (Style) TODOS](https://github.com/rails-api/active_model_serializers/blob/master/.rubocop_todo.yml).
|
|
||||||
- Delete and offsense, run `rake rubocop` (or possibly `rake rubocop:auto_correct`),
|
|
||||||
and [submit a PR](CONTRIBUTING.md#submitting-a-pull-request-pr).
|
|
||||||
|
|
||||||
- We are also encouraging comments to substantial changes (larger than bugfixes and simple features) under an
|
|
||||||
"RFC" (Request for Comments) process before we start active development.
|
|
||||||
Look for the [**RFC**](https://github.com/rails-api/active_model_serializers/labels/RFC) label.
|
|
||||||
|
|
||||||
|
|
||||||
## Pull requests
|
|
||||||
|
|
||||||
- If the tests pass and the pull request looks good, a maintainer will merge it.
|
|
||||||
- If the pull request needs to be changed,
|
|
||||||
- you can change it by updating the branch you generated the pull request from
|
|
||||||
- either by adding more commits, or
|
|
||||||
- by force pushing to it
|
|
||||||
- A maintainer can make any changes themselves and manually merge the code in.
|
|
||||||
|
|
||||||
## Commit messages
|
|
||||||
|
|
||||||
- [A Note About Git Commit Messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
|
|
||||||
- [http://stopwritingramblingcommitmessages.com/](http://stopwritingramblingcommitmessages.com/)
|
|
||||||
- [ThoughtBot style guide](https://github.com/thoughtbot/guides/tree/master/style#git)
|
|
||||||
|
|
||||||
#### About Pull Requests (PR's)
|
|
||||||
|
|
||||||
- [Using Pull Requests](https://help.github.com/articles/using-pull-requests)
|
|
||||||
- [Github pull requests made easy](http://www.element84.com/github-pull-requests-made-easy.html)
|
|
||||||
- [Exercism Git Workflow](http://help.exercism.io/git-workflow.html).
|
|
||||||
- [Level up your Git](http://rakeroutes.com/blog/deliberate-git/)
|
|
||||||
- [All Your Open Source Code Are Belong To Us](http://www.benjaminfleischer.com/2013/07/30/all-your-open-source-code-are-belong-to-us/)
|
|
||||||
|
|
||||||
## Issue Labeling
|
|
||||||
|
|
||||||
ActiveModelSerializers uses a subset of [StandardIssueLabels](https://github.com/wagenet/StandardIssueLabels) for Github Issues. You can [see our labels here](https://github.com/rails-api/active_model_serializers/labels).
|
|
||||||
|
|
||||||
@ -1,263 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# Adapters
|
|
||||||
|
|
||||||
ActiveModelSerializers offers the ability to configure which adapter
|
|
||||||
to use both globally and/or when serializing (usually when rendering).
|
|
||||||
|
|
||||||
The global adapter configuration is set on [`ActiveModelSerializers.config`](configuration_options.md).
|
|
||||||
It should be set only once, preferably at initialization.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
ActiveModelSerializers.config.adapter = ActiveModelSerializers::Adapter::JsonApi
|
|
||||||
```
|
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
ActiveModelSerializers.config.adapter = :json_api
|
|
||||||
```
|
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
ActiveModelSerializers.config.adapter = :json
|
|
||||||
```
|
|
||||||
|
|
||||||
The local adapter option is in the format `adapter: adapter`, where `adapter` is
|
|
||||||
any of the same values as set globally.
|
|
||||||
|
|
||||||
The configured adapter can be set as a symbol, class, or class name, as described in
|
|
||||||
[Advanced adapter configuration](adapters.md#advanced-adapter-configuration).
|
|
||||||
|
|
||||||
The `Attributes` adapter does not include a root key. It is just the serialized attributes.
|
|
||||||
|
|
||||||
Use either the `JSON` or `JSON API` adapters if you want the response document to have a root key.
|
|
||||||
|
|
||||||
## Built in Adapters
|
|
||||||
|
|
||||||
### Attributes - Default
|
|
||||||
|
|
||||||
It's the default adapter, it generates a json response without a root key.
|
|
||||||
Doesn't follow any specific convention.
|
|
||||||
|
|
||||||
##### Example output
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"title": "Title 1",
|
|
||||||
"body": "Body 1",
|
|
||||||
"publish_at": "2020-03-16T03:55:25.291Z",
|
|
||||||
"author": {
|
|
||||||
"first_name": "Bob",
|
|
||||||
"last_name": "Jones"
|
|
||||||
},
|
|
||||||
"comments": [
|
|
||||||
{
|
|
||||||
"body": "cool"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"body": "awesome"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### JSON
|
|
||||||
|
|
||||||
The json response is always rendered with a root key.
|
|
||||||
|
|
||||||
The root key can be overridden by:
|
|
||||||
* passing the `root` option in the render call. See details in the [Rendering Guides](rendering.md#overriding-the-root-key).
|
|
||||||
* setting the `type` of the serializer. See details in the [Serializers Guide](serializers.md#type).
|
|
||||||
|
|
||||||
Doesn't follow any specific convention.
|
|
||||||
|
|
||||||
##### Example output
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"post": {
|
|
||||||
"title": "Title 1",
|
|
||||||
"body": "Body 1",
|
|
||||||
"publish_at": "2020-03-16T03:55:25.291Z",
|
|
||||||
"author": {
|
|
||||||
"first_name": "Bob",
|
|
||||||
"last_name": "Jones"
|
|
||||||
},
|
|
||||||
"comments": [{
|
|
||||||
"body": "cool"
|
|
||||||
}, {
|
|
||||||
"body": "awesome"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### JSON API
|
|
||||||
|
|
||||||
This adapter follows **version 1.0** of the [format specified](../jsonapi/schema.md) in
|
|
||||||
[jsonapi.org/format](http://jsonapi.org/format).
|
|
||||||
|
|
||||||
##### Example output
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"id": "1337",
|
|
||||||
"type": "posts",
|
|
||||||
"attributes": {
|
|
||||||
"title": "Title 1",
|
|
||||||
"body": "Body 1",
|
|
||||||
"publish-at": "2020-03-16T03:55:25.291Z"
|
|
||||||
},
|
|
||||||
"relationships": {
|
|
||||||
"author": {
|
|
||||||
"data": {
|
|
||||||
"id": "1",
|
|
||||||
"type": "authors"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"comments": {
|
|
||||||
"data": [{
|
|
||||||
"id": "7",
|
|
||||||
"type": "comments"
|
|
||||||
}, {
|
|
||||||
"id": "12",
|
|
||||||
"type": "comments"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"post-authors": "https://example.com/post_authors"
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"rating": 5,
|
|
||||||
"favorite-count": 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Include option
|
|
||||||
|
|
||||||
Which [serializer associations](https://github.com/rails-api/active_model_serializers/blob/master/docs/general/serializers.md#associations) are rendered can be specified using the `include` option. The option usage is consistent with [the include option in the JSON API spec](http://jsonapi.org/format/#fetching-includes), and is available in all adapters.
|
|
||||||
|
|
||||||
Example of the usage:
|
|
||||||
```ruby
|
|
||||||
render json: @posts, include: ['author', 'comments', 'comments.author']
|
|
||||||
# or
|
|
||||||
render json: @posts, include: 'author,comments,comments.author'
|
|
||||||
```
|
|
||||||
|
|
||||||
The format of the `include` option can be either:
|
|
||||||
|
|
||||||
- a String composed of a comma-separated list of [relationship paths](http://jsonapi.org/format/#fetching-includes).
|
|
||||||
- an Array of Symbols and Hashes.
|
|
||||||
- a mix of both.
|
|
||||||
|
|
||||||
An empty string or an empty array will prevent rendering of any associations.
|
|
||||||
|
|
||||||
In addition, two types of wildcards may be used:
|
|
||||||
|
|
||||||
- `*` includes one level of associations.
|
|
||||||
- `**` includes all recursively.
|
|
||||||
|
|
||||||
These can be combined with other paths.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
render json: @posts, include: '**' # or '*' for a single layer
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
The following would render posts and include:
|
|
||||||
|
|
||||||
- the author
|
|
||||||
- the author's comments, and
|
|
||||||
- every resource referenced by the author's comments (recursively).
|
|
||||||
|
|
||||||
It could be combined, like above, with other paths in any combination desired.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
render json: @posts, include: 'author.comments.**'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note:** Wildcards are ActiveModelSerializers-specific, they are not part of the JSON API spec.
|
|
||||||
|
|
||||||
The default include for the JSON API adapter is no associations. The default for the JSON and Attributes adapters is all associations.
|
|
||||||
|
|
||||||
For the JSON API adapter associated resources will be gathered in the `"included"` member. For the JSON and Attributes
|
|
||||||
adapters associated resources will be rendered among the other attributes.
|
|
||||||
|
|
||||||
Only for the JSON API adapter you can specify, which attributes of associated resources will be rendered. This feature
|
|
||||||
is called [sparse fieldset](http://jsonapi.org/format/#fetching-sparse-fieldsets):
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
render json: @posts, include: 'comments', fields: { comments: ['content', 'created_at'] }
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Security Considerations
|
|
||||||
|
|
||||||
Since the included options may come from the query params (i.e. user-controller):
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
render json: @posts, include: params[:include]
|
|
||||||
```
|
|
||||||
|
|
||||||
The user could pass in `include=**`.
|
|
||||||
|
|
||||||
We recommend filtering any user-supplied includes appropriately.
|
|
||||||
|
|
||||||
## Advanced adapter configuration
|
|
||||||
|
|
||||||
### Registering an adapter
|
|
||||||
|
|
||||||
The default adapter can be configured, as above, to use any class given to it.
|
|
||||||
|
|
||||||
An adapter may also be specified, e.g. when rendering, as a class or as a symbol.
|
|
||||||
If a symbol, then the adapter must be, e.g. `:great_example`,
|
|
||||||
`ActiveModelSerializers::Adapter::GreatExample`, or registered.
|
|
||||||
|
|
||||||
There are two ways to register an adapter:
|
|
||||||
|
|
||||||
1) The simplest, is to subclass `ActiveModelSerializers::Adapter::Base`, e.g. the below will
|
|
||||||
register the `Example::UsefulAdapter` as `"example/useful_adapter"`.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
module Example
|
|
||||||
class UsefulAdapter < ActiveModelSerializers::Adapter::Base
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
You'll notice that the name it registers is the underscored namespace and class.
|
|
||||||
|
|
||||||
Under the covers, when the `ActiveModelSerializers::Adapter::Base` is subclassed, it registers
|
|
||||||
the subclass as `register("example/useful_adapter", Example::UsefulAdapter)`
|
|
||||||
|
|
||||||
2) Any class can be registered as an adapter by calling `register` directly on the
|
|
||||||
`ActiveModelSerializers::Adapter` class. e.g., the below registers `MyAdapter` as
|
|
||||||
`:special_adapter`.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class MyAdapter; end
|
|
||||||
ActiveModelSerializers::Adapter.register(:special_adapter, MyAdapter)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Looking up an adapter
|
|
||||||
|
|
||||||
| Method | Return value |
|
|
||||||
| :------------ |:---------------|
|
|
||||||
| `ActiveModelSerializers::Adapter.adapter_map` | A Hash of all known adapters `{ adapter_name => adapter_class }` |
|
|
||||||
| `ActiveModelSerializers::Adapter.adapters` | A (sorted) Array of all known `adapter_names` |
|
|
||||||
| `ActiveModelSerializers::Adapter.lookup(name_or_klass)` | The `adapter_class`, else raises an `ActiveModelSerializers::Adapter::UnknownAdapter` error |
|
|
||||||
| `ActiveModelSerializers::Adapter.adapter_class(adapter)` | Delegates to `ActiveModelSerializers::Adapter.lookup(adapter)` |
|
|
||||||
| `ActiveModelSerializers::Adapter.configured_adapter` | A convenience method for `ActiveModelSerializers::Adapter.lookup(config.adapter)` |
|
|
||||||
|
|
||||||
The registered adapter name is always a String, but may be looked up as a Symbol or String.
|
|
||||||
Helpfully, the Symbol or String is underscored, so that `get(:my_adapter)` and `get("MyAdapter")`
|
|
||||||
may both be used.
|
|
||||||
|
|
||||||
For more information, see [the Adapter class on GitHub](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model_serializers/adapter.rb)
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# Caching
|
|
||||||
|
|
||||||
## Warning
|
|
||||||
|
|
||||||
There is currently a problem with caching in AMS [Caching doesn't improve performance](https://github.com/rails-api/active_model_serializers/issues/1586). Adding caching _may_ slow down your application, rather than speeding it up. We suggest you benchmark any caching you implement before using in a production enviroment
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
To cache a serializer, call ```cache``` and pass its options.
|
|
||||||
The options are the same options of ```ActiveSupport::Cache::Store```, plus
|
|
||||||
a ```key``` option that will be the prefix of the object cache
|
|
||||||
on a pattern ```"#{key}/#{object.id}-#{object.updated_at}"```.
|
|
||||||
|
|
||||||
The cache support is optimized to use the cached object in multiple request. An object cached on a ```show``` request will be reused at the ```index```. If there is a relationship with another cached serializer it will also be created and reused automatically.
|
|
||||||
|
|
||||||
**[NOTE] Every object is individually cached.**
|
|
||||||
|
|
||||||
**[NOTE] The cache is automatically expired after an object is updated, but it's not deleted.**
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
cache(options = nil) # options: ```{key, expires_in, compress, force, race_condition_ttl}```
|
|
||||||
```
|
|
||||||
|
|
||||||
Take the example below:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class PostSerializer < ActiveModel::Serializer
|
|
||||||
cache key: 'post', expires_in: 3.hours
|
|
||||||
attributes :title, :body
|
|
||||||
|
|
||||||
has_many :comments
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
On this example every ```Post``` object will be cached with
|
|
||||||
the key ```"post/#{post.id}-#{post.updated_at}"```. You can use this key to expire it as you want,
|
|
||||||
but in this case it will be automatically expired after 3 hours.
|
|
||||||
|
|
||||||
## Fragment Caching
|
|
||||||
|
|
||||||
If there is some API endpoint that shouldn't be fully cached, you can still optimise it, using Fragment Cache on the attributes and relationships that you want to cache.
|
|
||||||
|
|
||||||
You can define the attribute by using ```only``` or ```except``` option on cache method.
|
|
||||||
|
|
||||||
**[NOTE] Cache serializers will be used at their relationships**
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class PostSerializer < ActiveModel::Serializer
|
|
||||||
cache key: 'post', expires_in: 3.hours, only: [:title]
|
|
||||||
attributes :title, :body
|
|
||||||
|
|
||||||
has_many :comments
|
|
||||||
end
|
|
||||||
```
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# Configuration Options
|
|
||||||
|
|
||||||
The following configuration options can be set on
|
|
||||||
`ActiveModelSerializers.config`, preferably inside an initializer.
|
|
||||||
|
|
||||||
## General
|
|
||||||
|
|
||||||
##### adapter
|
|
||||||
|
|
||||||
The [adapter](adapters.md) to use.
|
|
||||||
|
|
||||||
Possible values:
|
|
||||||
|
|
||||||
- `:attributes` (default)
|
|
||||||
- `:json`
|
|
||||||
- `:json_api`
|
|
||||||
|
|
||||||
##### serializer_lookup_enabled
|
|
||||||
|
|
||||||
Enable automatic serializer lookup.
|
|
||||||
|
|
||||||
Possible values:
|
|
||||||
|
|
||||||
- `true` (default)
|
|
||||||
- `false`
|
|
||||||
|
|
||||||
When `false`, serializers must be explicitly specified.
|
|
||||||
|
|
||||||
##### key_transform
|
|
||||||
|
|
||||||
The [key transform](key_transforms.md) to use.
|
|
||||||
|
|
||||||
|
|
||||||
| Option | Result |
|
|
||||||
|----|----|
|
|
||||||
| `:camel` | ExampleKey |
|
|
||||||
| `:camel_lower` | exampleKey |
|
|
||||||
| `:dash` | example-key |
|
|
||||||
| `:unaltered` | the original, unaltered key |
|
|
||||||
| `:underscore` | example_key |
|
|
||||||
| `nil` | use the adapter default |
|
|
||||||
|
|
||||||
Each adapter has a default key transform configured:
|
|
||||||
|
|
||||||
| Adapter | Default Key Transform |
|
|
||||||
|----|----|
|
|
||||||
| `Attributes` | `:unaltered` |
|
|
||||||
| `Json` | `:unaltered` |
|
|
||||||
| `JsonApi` | `:dash` |
|
|
||||||
|
|
||||||
`config.key_transform` is a global override of the adapter default. Adapters
|
|
||||||
still prefer the render option `:key_transform` over this setting.
|
|
||||||
|
|
||||||
*NOTE: Key transforms can be expensive operations. If key transforms are unnecessary for the
|
|
||||||
application, setting `config.key_transform` to `:unaltered` will provide a performance boost.*
|
|
||||||
|
|
||||||
##### default_includes
|
|
||||||
What relationships to serialize by default. Default: `'*'`, which includes one level of related
|
|
||||||
objects. See [includes](adapters.md#included) for more info.
|
|
||||||
|
|
||||||
|
|
||||||
##### serializer_lookup_chain
|
|
||||||
|
|
||||||
Configures how serializers are searched for. By default, the lookup chain is
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
ActiveModelSerializers::LookupChain::DEFAULT
|
|
||||||
```
|
|
||||||
|
|
||||||
which is shorthand for
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
[
|
|
||||||
ActiveModelSerializers::LookupChain::BY_PARENT_SERIALIZER,
|
|
||||||
ActiveModelSerializers::LookupChain::BY_NAMESPACE,
|
|
||||||
ActiveModelSerializers::LookupChain::BY_RESOURCE_NAMESPACE,
|
|
||||||
ActiveModelSerializers::LookupChain::BY_RESOURCE
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Each of the array entries represent a proc. A serializer lookup proc will be yielded 3 arguments. `resource_class`, `serializer_class`, and `namespace`.
|
|
||||||
|
|
||||||
Note that:
|
|
||||||
- `resource_class` is the class of the resource being rendered
|
|
||||||
- by default `serializer_class` is `ActiveModel::Serializer`
|
|
||||||
- for association lookup it's the "parent" serializer
|
|
||||||
- `namespace` correspond to either the controller namespace or the [optionally] specified [namespace render option](./rendering.md#namespace)
|
|
||||||
|
|
||||||
An example config could be:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
ActiveModelSerializers.config.serializer_lookup_chain = [
|
|
||||||
lambda do |resource_class, serializer_class, namespace|
|
|
||||||
"API::#{namespace}::#{resource_class}"
|
|
||||||
end
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
If you simply want to add to the existing lookup_chain. Use `unshift`.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
ActiveModelSerializers.config.serializer_lookup_chain.unshift(
|
|
||||||
lambda do |resource_class, serializer_class, namespace|
|
|
||||||
# ...
|
|
||||||
end
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
See [lookup_chain.rb](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model_serializers/lookup_chain.rb) for further explanations and examples.
|
|
||||||
|
|
||||||
## JSON API
|
|
||||||
|
|
||||||
##### jsonapi_resource_type
|
|
||||||
|
|
||||||
Sets whether the [type](http://jsonapi.org/format/#document-resource-identifier-objects)
|
|
||||||
of the resource should be `singularized` or `pluralized` when it is not
|
|
||||||
[explicitly specified by the serializer](https://github.com/rails-api/active_model_serializers/blob/master/docs/general/serializers.md#type)
|
|
||||||
|
|
||||||
Possible values:
|
|
||||||
|
|
||||||
- `:singular`
|
|
||||||
- `:plural` (default)
|
|
||||||
|
|
||||||
##### jsonapi_namespace_separator
|
|
||||||
|
|
||||||
Sets separator string for namespaced models to render `type` attribute.
|
|
||||||
|
|
||||||
|
|
||||||
| Separator | Example: Admin::User |
|
|
||||||
|----|----|
|
|
||||||
| `'-'` (default) | 'admin-users'
|
|
||||||
| `'--'` (recommended) | 'admin--users'
|
|
||||||
|
|
||||||
See [Recommendation for dasherizing (kebab-case-ing) namespaced object, such as `Admin::User`](https://github.com/json-api/json-api/issues/850)
|
|
||||||
for more discussion.
|
|
||||||
|
|
||||||
##### jsonapi_include_toplevel_object
|
|
||||||
|
|
||||||
Include a [top level jsonapi member](http://jsonapi.org/format/#document-jsonapi-object)
|
|
||||||
in the response document.
|
|
||||||
|
|
||||||
Possible values:
|
|
||||||
|
|
||||||
- `true`
|
|
||||||
- `false` (default)
|
|
||||||
|
|
||||||
##### jsonapi_version
|
|
||||||
|
|
||||||
The latest version of the spec to which the API conforms.
|
|
||||||
|
|
||||||
Default: `'1.0'`.
|
|
||||||
|
|
||||||
*Used when `jsonapi_include_toplevel_object` is `true`*
|
|
||||||
|
|
||||||
##### jsonapi_toplevel_meta
|
|
||||||
|
|
||||||
Optional top-level metadata. Not included if empty.
|
|
||||||
|
|
||||||
Default: `{}`.
|
|
||||||
|
|
||||||
*Used when `jsonapi_include_toplevel_object` is `true`*
|
|
||||||
|
|
||||||
|
|
||||||
## Hooks
|
|
||||||
|
|
||||||
To run a hook when ActiveModelSerializers is loaded, use
|
|
||||||
`ActiveSupport.on_load(:action_controller) do end`
|
|
||||||
@ -1,100 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# Deserialization
|
|
||||||
|
|
||||||
This is currently an *experimental* feature. The interface may change.
|
|
||||||
|
|
||||||
## JSON API
|
|
||||||
|
|
||||||
The `ActiveModelSerializers::Deserialization` defines two methods (namely `jsonapi_parse` and `jsonapi_parse!`), which take a `Hash` or an instance of `ActionController::Parameters` representing a JSON API payload, and return a hash that can directly be used to create/update models. The bang version throws an `InvalidDocument` exception when parsing fails, whereas the "safe" version simply returns an empty hash.
|
|
||||||
|
|
||||||
- Parameters
|
|
||||||
- document: `Hash` or `ActionController::Parameters` instance
|
|
||||||
- options:
|
|
||||||
- only: `Array` of whitelisted fields
|
|
||||||
- except: `Array` of blacklisted fields
|
|
||||||
- keys: `Hash` of fields the name of which needs to be modified (e.g. `{ :author => :user, :date => :created_at }`)
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class PostsController < ActionController::Base
|
|
||||||
def create
|
|
||||||
Post.create(create_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_params
|
|
||||||
ActiveModelSerializers::Deserialization.jsonapi_parse(params, only: [:title, :content, :author])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Given a JSON API document,
|
|
||||||
|
|
||||||
```
|
|
||||||
document = {
|
|
||||||
'data' => {
|
|
||||||
'id' => 1,
|
|
||||||
'type' => 'post',
|
|
||||||
'attributes' => {
|
|
||||||
'title' => 'Title 1',
|
|
||||||
'date' => '2015-12-20'
|
|
||||||
},
|
|
||||||
'relationships' => {
|
|
||||||
'author' => {
|
|
||||||
'data' => {
|
|
||||||
'type' => 'user',
|
|
||||||
'id' => '2'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'second_author' => {
|
|
||||||
'data' => nil
|
|
||||||
},
|
|
||||||
'comments' => {
|
|
||||||
'data' => [{
|
|
||||||
'type' => 'comment',
|
|
||||||
'id' => '3'
|
|
||||||
},{
|
|
||||||
'type' => 'comment',
|
|
||||||
'id' => '4'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The entire document can be parsed without specifying any options:
|
|
||||||
```ruby
|
|
||||||
ActiveModelSerializers::Deserialization.jsonapi_parse(document)
|
|
||||||
#=>
|
|
||||||
# {
|
|
||||||
# title: 'Title 1',
|
|
||||||
# date: '2015-12-20',
|
|
||||||
# author_id: 2,
|
|
||||||
# second_author_id: nil
|
|
||||||
# comment_ids: [3, 4]
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
|
|
||||||
and fields, relationships, and polymorphic relationships can be specified via the options:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
ActiveModelSerializers::Deserialization
|
|
||||||
.jsonapi_parse(document, only: [:title, :date, :author],
|
|
||||||
keys: { date: :published_at },
|
|
||||||
polymorphic: [:author])
|
|
||||||
#=>
|
|
||||||
# {
|
|
||||||
# title: 'Title 1',
|
|
||||||
# published_at: '2015-12-20',
|
|
||||||
# author_id: '2',
|
|
||||||
# author_type: 'user'
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Attributes/Json
|
|
||||||
|
|
||||||
There is currently no deserialization for those adapters.
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# Fields
|
|
||||||
|
|
||||||
If for any reason, you need to restrict the fields returned, you should use `fields` option.
|
|
||||||
|
|
||||||
For example, if you have a serializer like this
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class UserSerializer < ActiveModel::Serializer
|
|
||||||
attributes :access_token, :first_name, :last_name
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
and in a specific controller, you want to return `access_token` only, `fields` will help you:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class AnonymousController < ApplicationController
|
|
||||||
def create
|
|
||||||
render json: User.create(activation_state: 'anonymous'), fields: [:access_token], status: 201
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that this is only valid for the `json` and `attributes` adapter. For the `json_api` adapter, you would use
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
render json: @user, fields: { users: [:access_token] }
|
|
||||||
```
|
|
||||||
|
|
||||||
Where `users` is the JSONAPI type.
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# Getting Started
|
|
||||||
|
|
||||||
## Creating a Serializer
|
|
||||||
|
|
||||||
The easiest way to create a new serializer is to generate a new resource, which
|
|
||||||
will generate a serializer at the same time:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rails g resource post title:string body:string
|
|
||||||
```
|
|
||||||
|
|
||||||
This will generate a serializer in `app/serializers/post_serializer.rb` for
|
|
||||||
your new model. You can also generate a serializer for an existing model with
|
|
||||||
the serializer generator:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rails g serializer post
|
|
||||||
```
|
|
||||||
|
|
||||||
The generated serializer will contain basic `attributes` and
|
|
||||||
`has_many`/`has_one`/`belongs_to` declarations, based on the model. For example:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class PostSerializer < ActiveModel::Serializer
|
|
||||||
attributes :title, :body
|
|
||||||
|
|
||||||
has_many :comments
|
|
||||||
has_one :author
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
and
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class CommentSerializer < ActiveModel::Serializer
|
|
||||||
attributes :name, :body
|
|
||||||
|
|
||||||
belongs_to :post
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
The attribute names are a **whitelist** of attributes to be serialized.
|
|
||||||
|
|
||||||
The `has_many`, `has_one`, and `belongs_to` declarations describe relationships between
|
|
||||||
resources. By default, when you serialize a `Post`, you will get its `Comments`
|
|
||||||
as well.
|
|
||||||
|
|
||||||
For more information, see [Serializers](/docs/general/serializers.md).
|
|
||||||
|
|
||||||
### Namespaced Models
|
|
||||||
|
|
||||||
When serializing a model inside a namespace, such as `Api::V1::Post`, ActiveModelSerializers will expect the corresponding serializer to be inside the same namespace (namely `Api::V1::PostSerializer`).
|
|
||||||
|
|
||||||
### Model Associations and Nested Serializers
|
|
||||||
|
|
||||||
When declaring a serializer for a model with associations, such as:
|
|
||||||
```ruby
|
|
||||||
class PostSerializer < ActiveModel::Serializer
|
|
||||||
has_many :comments
|
|
||||||
end
|
|
||||||
```
|
|
||||||
ActiveModelSerializers will look for `PostSerializer::CommentSerializer` in priority, and fall back to `::CommentSerializer` in case the former does not exist. This allows for more control over the way a model gets serialized as an association of an other model.
|
|
||||||
|
|
||||||
For example, in the following situation:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class CommentSerializer < ActiveModel::Serializer
|
|
||||||
attributes :body, :date, :nb_likes
|
|
||||||
end
|
|
||||||
|
|
||||||
class PostSerializer < ActiveModel::Serializer
|
|
||||||
has_many :comments
|
|
||||||
class CommentSerializer < ActiveModel::Serializer
|
|
||||||
attributes :body_short
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
ActiveModelSerializers will use `PostSerializer::CommentSerializer` (thus including only the `:body_short` attribute) when serializing a `Comment` as part of a `Post`, but use `::CommentSerializer` when serializing a `Comment` directly (thus including `:body, :date, :nb_likes`).
|
|
||||||
|
|
||||||
### Extending a Base `ApplicationSerializer`
|
|
||||||
|
|
||||||
By default, new serializers descend from `ActiveModel::Serializer`. However, if
|
|
||||||
you wish to share behavior across your serializers, you can create an
|
|
||||||
`ApplicationSerializer` at `app/serializers/application_serializer.rb`:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class ApplicationSerializer < ActiveModel::Serializer
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
Then any newly-generated serializers will automatically descend from
|
|
||||||
`ApplicationSerializer`.
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rails g serializer post
|
|
||||||
```
|
|
||||||
|
|
||||||
Now generates:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class PostSerializer < ApplicationSerializer
|
|
||||||
attributes :id
|
|
||||||
end
|
|
||||||
````
|
|
||||||
|
|
||||||
## Rails Integration
|
|
||||||
|
|
||||||
ActiveModelSerializers will automatically integrate with your Rails app,
|
|
||||||
so you won't need to update your controller.
|
|
||||||
This is a example of how the controller will look:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class PostsController < ApplicationController
|
|
||||||
|
|
||||||
def show
|
|
||||||
@post = Post.find(params[:id])
|
|
||||||
render json: @post
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
If you wish to use Rails url helpers for link generation, e.g., `link(:resources) { resources_url }`, ensure your application sets
|
|
||||||
`Rails.application.routes.default_url_options`.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
Rails.application.routes.default_url_options = {
|
|
||||||
host: 'example.com'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# Instrumentation
|
|
||||||
|
|
||||||
ActiveModelSerializers uses the
|
|
||||||
[ActiveSupport::Notification API](http://guides.rubyonrails.org/active_support_instrumentation.html#subscribing-to-an-event),
|
|
||||||
which allows for subscribing to events, such as for logging.
|
|
||||||
|
|
||||||
## Events
|
|
||||||
|
|
||||||
Name:
|
|
||||||
|
|
||||||
`render.active_model_serializers`
|
|
||||||
|
|
||||||
Payload (example):
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
{
|
|
||||||
serializer: PostSerializer,
|
|
||||||
adapter: ActiveModelSerializers::Adapter::Attributes
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Subscribing:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
ActiveSupport::Notifications.subscribe 'render.active_model_serializers' do |name, started, finished, unique_id, data|
|
|
||||||
# whatever
|
|
||||||
end
|
|
||||||
ActiveSupport::Notifications.subscribe 'render.active_model_serializers' do |*args|
|
|
||||||
event = ActiveSupport::Notifications::Event.new(*args)
|
|
||||||
# event.payload
|
|
||||||
# whatever
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## [LogSubscriber](http://api.rubyonrails.org/classes/ActiveSupport/LogSubscriber.html)
|
|
||||||
|
|
||||||
ActiveModelSerializers includes an `ActiveModelSerializers::LogSubscriber` that attaches to
|
|
||||||
`render.active_model_serializers`.
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# Key Transforms
|
|
||||||
|
|
||||||
Key Transforms modify the casing of keys and keys referenced in values in
|
|
||||||
serialized responses.
|
|
||||||
|
|
||||||
Provided key transforms:
|
|
||||||
|
|
||||||
| Option | Result |
|
|
||||||
|----|----|
|
|
||||||
| `:camel` | ExampleKey |
|
|
||||||
| `:camel_lower` | exampleKey |
|
|
||||||
| `:dash` | example-key |
|
|
||||||
| `:unaltered` | the original, unaltered key |
|
|
||||||
| `:underscore` | example_key |
|
|
||||||
| `nil` | use the adapter default |
|
|
||||||
|
|
||||||
Key translation precedence is as follows:
|
|
||||||
|
|
||||||
##### Adapter option
|
|
||||||
|
|
||||||
`key_transform` is provided as an option via render.
|
|
||||||
|
|
||||||
```render json: posts, each_serializer: PostSerializer, key_transform: :camel_lower```
|
|
||||||
|
|
||||||
##### Configuration option
|
|
||||||
|
|
||||||
`key_transform` is set in `ActiveModelSerializers.config.key_transform`.
|
|
||||||
|
|
||||||
```ActiveModelSerializers.config.key_transform = :camel_lower```
|
|
||||||
|
|
||||||
##### Adapter default
|
|
||||||
|
|
||||||
Each adapter has a default transform configured:
|
|
||||||
|
|
||||||
| Adapter | Default Key Transform |
|
|
||||||
|----|----|
|
|
||||||
| `Json` | `:unaltered` |
|
|
||||||
| `JsonApi` | `:dash` |
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
|
|
||||||
The default logger in a Rails application will be `Rails.logger`.
|
|
||||||
|
|
||||||
When there is no `Rails.logger`, the default logger is an instance of
|
|
||||||
`ActiveSupport::TaggedLogging` logging to STDOUT.
|
|
||||||
|
|
||||||
You may customize the logger in an initializer, for example:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
ActiveModelSerializers.logger = Logger.new(STDOUT)
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also disable the logger, just put this in `config/initializers/active_model_serializers.rb`:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
require 'active_model_serializers'
|
|
||||||
ActiveSupport::Notifications.unsubscribe(ActiveModelSerializers::Logging::RENDER_EVENT)
|
|
||||||
```
|
|
||||||
@ -1,279 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# Rendering
|
|
||||||
|
|
||||||
### Implicit Serializer
|
|
||||||
|
|
||||||
In your controllers, when you use `render :json`, Rails will now first search
|
|
||||||
for a serializer for the object and use it if available.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class PostsController < ApplicationController
|
|
||||||
def show
|
|
||||||
@post = Post.find(params[:id])
|
|
||||||
|
|
||||||
render json: @post
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
In this case, Rails will look for a serializer named `PostSerializer`, and if
|
|
||||||
it exists, use it to serialize the `Post`.
|
|
||||||
|
|
||||||
### Explicit Serializer
|
|
||||||
|
|
||||||
If you wish to use a serializer other than the default, you can explicitly pass it to the renderer.
|
|
||||||
|
|
||||||
#### 1. For a resource:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
render json: @post, serializer: PostPreviewSerializer
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. For a resource collection:
|
|
||||||
|
|
||||||
Specify the serializer for each resource with `each_serializer`
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
render json: @posts, each_serializer: PostPreviewSerializer
|
|
||||||
```
|
|
||||||
|
|
||||||
The default serializer for collections is `CollectionSerializer`.
|
|
||||||
|
|
||||||
Specify the collection serializer with the `serializer` option.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
render json: @posts, serializer: CollectionSerializer, each_serializer: PostPreviewSerializer
|
|
||||||
```
|
|
||||||
|
|
||||||
## Serializing non-ActiveRecord objects
|
|
||||||
|
|
||||||
See [README](../../README.md#what-does-a-serializable-resource-look-like)
|
|
||||||
|
|
||||||
## SerializableResource options
|
|
||||||
|
|
||||||
See [README](../../README.md#activemodelserializersserializableresource)
|
|
||||||
|
|
||||||
### adapter_opts
|
|
||||||
|
|
||||||
#### fields
|
|
||||||
|
|
||||||
If you are using `json` or `attributes` adapter
|
|
||||||
```ruby
|
|
||||||
render json: @user, fields: [:access_token]
|
|
||||||
```
|
|
||||||
|
|
||||||
See [Fields](fields.md) for more information.
|
|
||||||
|
|
||||||
#### adapter
|
|
||||||
|
|
||||||
This option lets you explicitly set the adapter to be used by passing a registered adapter. Your options are `:attributes`, `:json`, and `:json_api`.
|
|
||||||
|
|
||||||
```
|
|
||||||
ActiveModel::Serializer.config.adapter = :json_api
|
|
||||||
```
|
|
||||||
|
|
||||||
#### key_transform
|
|
||||||
|
|
||||||
```render json: posts, each_serializer: PostSerializer, key_transform: :camel_lower```
|
|
||||||
|
|
||||||
See [Key Transforms](key_transforms.md) for more information.
|
|
||||||
|
|
||||||
#### meta
|
|
||||||
|
|
||||||
A `meta` member can be used to include non-standard meta-information. `meta` can
|
|
||||||
be utilized in several levels in a response.
|
|
||||||
|
|
||||||
##### Top-level
|
|
||||||
|
|
||||||
To set top-level `meta` in a response, specify it in the `render` call.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
render json: @post, meta: { total: 10 }
|
|
||||||
```
|
|
||||||
|
|
||||||
The key can be customized using `meta_key` option.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
render json: @post, meta: { total: 10 }, meta_key: "custom_meta"
|
|
||||||
```
|
|
||||||
|
|
||||||
`meta` will only be included in your response if you are using an Adapter that
|
|
||||||
supports `root`, e.g., `JsonApi` and `Json` adapters. The default adapter,
|
|
||||||
`Attributes` does not have `root`.
|
|
||||||
|
|
||||||
|
|
||||||
##### Resource-level
|
|
||||||
|
|
||||||
To set resource-level `meta` in a response, define meta in a serializer with one
|
|
||||||
of the following methods:
|
|
||||||
|
|
||||||
As a single, static string.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
meta stuff: 'value'
|
|
||||||
```
|
|
||||||
|
|
||||||
As a block containing a Hash.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
meta do
|
|
||||||
{
|
|
||||||
rating: 4,
|
|
||||||
comments_count: object.comments.count
|
|
||||||
}
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
#### links
|
|
||||||
|
|
||||||
If you wish to use Rails url helpers for link generation, e.g., `link(:resources) { resources_url }`, ensure your application sets
|
|
||||||
`Rails.application.routes.default_url_options`.
|
|
||||||
|
|
||||||
##### Top-level
|
|
||||||
|
|
||||||
JsonApi supports a [links object](http://jsonapi.org/format/#document-links) to be specified at top-level, that you can specify in the `render`:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
links_object = {
|
|
||||||
href: "http://example.com/api/posts",
|
|
||||||
meta: {
|
|
||||||
count: 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
render json: @posts, links: links_object
|
|
||||||
```
|
|
||||||
|
|
||||||
That's the result:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"type": "posts",
|
|
||||||
"id": "1",
|
|
||||||
"attributes": {
|
|
||||||
"title": "JSON API is awesome!",
|
|
||||||
"body": "You should be using JSON API",
|
|
||||||
"created": "2015-05-22T14:56:29.000Z",
|
|
||||||
"updated": "2015-05-22T14:56:28.000Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"links": {
|
|
||||||
"href": "http://example.com/api/posts",
|
|
||||||
"meta": {
|
|
||||||
"count": 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This feature is specific to JsonApi, so you have to use the use the [JsonApi Adapter](adapters.md#jsonapi)
|
|
||||||
|
|
||||||
|
|
||||||
##### Resource-level
|
|
||||||
|
|
||||||
In your serializer, define each link in one of the following methods:
|
|
||||||
|
|
||||||
As a static string
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
link :link_name, 'https://example.com/resource'
|
|
||||||
```
|
|
||||||
|
|
||||||
As a block to be evaluated. When using Rails, URL helpers are available.
|
|
||||||
Ensure your application sets `Rails.application.routes.default_url_options`.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
link :link_name_ do
|
|
||||||
"https://example.com/resource/#{object.id}"
|
|
||||||
end
|
|
||||||
|
|
||||||
link(:link_name) { "https://example.com/resource/#{object.id}" }
|
|
||||||
|
|
||||||
link(:link_name) { resource_url(object) }
|
|
||||||
|
|
||||||
link(:link_name) { url_for(controller: 'controller_name', action: 'index', only_path: false) }
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### serializer_opts
|
|
||||||
|
|
||||||
#### include
|
|
||||||
|
|
||||||
PR please :)
|
|
||||||
|
|
||||||
#### Overriding the root key
|
|
||||||
|
|
||||||
Overriding the resource root only applies when using the JSON adapter.
|
|
||||||
|
|
||||||
Normally, the resource root is derived from the class name of the resource being serialized.
|
|
||||||
e.g. `UserPostSerializer.new(UserPost.new)` will be serialized with the root `user_post` or `user_posts` according the adapter collection pluralization rules.
|
|
||||||
|
|
||||||
When using the JSON adapter in your initializer (ActiveModelSerializers.config.adapter = :json), or passing in the adapter in your render call, you can specify the root by passing it as an argument to `render`. For example:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
render json: @user_post, root: "admin_post", adapter: :json
|
|
||||||
```
|
|
||||||
|
|
||||||
This will be rendered as:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"admin_post": {
|
|
||||||
"title": "how to do open source"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Note: the `Attributes` adapter (default) does not include a resource root. You also will not be able to create a single top-level root if you are using the :json_api adapter.
|
|
||||||
|
|
||||||
#### namespace
|
|
||||||
|
|
||||||
The namespace for serializer lookup is based on the controller.
|
|
||||||
|
|
||||||
To configure the implicit namespace, in your controller, create a before filter
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
before_action do
|
|
||||||
self.namespace_for_serializer = Api::V2
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
`namespace` can also be passed in as a render option:
|
|
||||||
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
@post = Post.first
|
|
||||||
render json: @post, namespace: Api::V2
|
|
||||||
```
|
|
||||||
|
|
||||||
This tells the serializer lookup to check for the existence of `Api::V2::PostSerializer`, and if any relations are rendered with `@post`, they will also utilize the `Api::V2` namespace.
|
|
||||||
|
|
||||||
The `namespace` can be any object whose namespace can be represented by string interpolation (i.e. by calling to_s)
|
|
||||||
- Module `Api::V2`
|
|
||||||
- String `'Api::V2'`
|
|
||||||
- Symbol `:'Api::V2'`
|
|
||||||
|
|
||||||
Note that by using a string and symbol, Ruby will assume the namespace is defined at the top level.
|
|
||||||
|
|
||||||
|
|
||||||
#### serializer
|
|
||||||
|
|
||||||
PR please :)
|
|
||||||
|
|
||||||
#### scope
|
|
||||||
|
|
||||||
PR please :)
|
|
||||||
|
|
||||||
#### scope_name
|
|
||||||
|
|
||||||
PR please :)
|
|
||||||
|
|
||||||
## Using a serializer without `render`
|
|
||||||
|
|
||||||
See [Usage outside of a controller](../howto/outside_controller_use.md#serializing-before-controller-render).
|
|
||||||
|
|
||||||
## Pagination
|
|
||||||
|
|
||||||
See [How to add pagination links](https://github.com/rails-api/active_model_serializers/blob/master/docs/howto/add_pagination_links.md).
|
|
||||||
@ -1,461 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# Serializers
|
|
||||||
|
|
||||||
Given a serializer class:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class SomeSerializer < ActiveModel::Serializer
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
The following methods may be defined in it:
|
|
||||||
|
|
||||||
### Attributes
|
|
||||||
|
|
||||||
#### ::attributes
|
|
||||||
|
|
||||||
Serialization of the resource `title` and `body`
|
|
||||||
|
|
||||||
| In Serializer | #attributes |
|
|
||||||
|---------------------------- |-------------|
|
|
||||||
| `attributes :title, :body` | `{ title: 'Some Title', body: 'Some Body' }`
|
|
||||||
| `attributes :title, :body`<br>`def body "Special #{object.body}" end` | `{ title: 'Some Title', body: 'Special Some Body' }`
|
|
||||||
|
|
||||||
|
|
||||||
#### ::attribute
|
|
||||||
|
|
||||||
Serialization of the resource `title`
|
|
||||||
|
|
||||||
| In Serializer | #attributes |
|
|
||||||
|---------------------------- |-------------|
|
|
||||||
| `attribute :title` | `{ title: 'Some Title' } `
|
|
||||||
| `attribute :title, key: :name` | `{ name: 'Some Title' } `
|
|
||||||
| `attribute(:title) { 'A Different Title'}` | `{ title: 'A Different Title' } `
|
|
||||||
| `attribute :title`<br>`def title 'A Different Title' end` | `{ title: 'A Different Title' }`
|
|
||||||
|
|
||||||
An `if` or `unless` option can make an attribute conditional. It takes a symbol of a method name on the serializer, or a lambda literal.
|
|
||||||
|
|
||||||
e.g.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
attribute :private_data, if: :is_current_user?
|
|
||||||
attribute :another_private_data, if: -> { scope.admin? }
|
|
||||||
|
|
||||||
def is_current_user?
|
|
||||||
object.id == current_user.id
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### Associations
|
|
||||||
|
|
||||||
The interface for associations is, generically:
|
|
||||||
|
|
||||||
> `association_type(association_name, options, &block)`
|
|
||||||
|
|
||||||
Where:
|
|
||||||
|
|
||||||
- `association_type` may be `has_one`, `has_many`, `belongs_to`.
|
|
||||||
- `association_name` is a method name the serializer calls.
|
|
||||||
- optional: `options` may be:
|
|
||||||
- `key:` The name used for the serialized association.
|
|
||||||
- `serializer:`
|
|
||||||
- `if:`
|
|
||||||
- `unless:`
|
|
||||||
- `virtual_value:`
|
|
||||||
- `polymorphic:` defines if polymorphic relation type should be nested in serialized association.
|
|
||||||
- optional: `&block` is a context that returns the association's attributes.
|
|
||||||
- prevents `association_name` method from being called.
|
|
||||||
- return value of block is used as the association value.
|
|
||||||
- yields the `serializer` to the block.
|
|
||||||
- `include_data false` prevents the `data` key from being rendered in the JSON API relationship.
|
|
||||||
|
|
||||||
#### ::has_one
|
|
||||||
|
|
||||||
e.g.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
has_one :bio
|
|
||||||
has_one :blog, key: :site
|
|
||||||
has_one :maker, virtual_value: { id: 1 }
|
|
||||||
|
|
||||||
has_one :blog do |serializer|
|
|
||||||
serializer.cached_blog
|
|
||||||
end
|
|
||||||
|
|
||||||
def cached_blog
|
|
||||||
cache_store.fetch("cached_blog:#{object.updated_at}") do
|
|
||||||
Blog.find(object.blog_id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
has_one :blog, if: :show_blog?
|
|
||||||
# you can also use a string or lambda
|
|
||||||
# has_one :blog, if: 'scope.admin?'
|
|
||||||
# has_one :blog, if: -> (serializer) { serializer.scope.admin? }
|
|
||||||
# has_one :blog, if: -> { scope.admin? }
|
|
||||||
|
|
||||||
def show_blog?
|
|
||||||
scope.admin?
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ::has_many
|
|
||||||
|
|
||||||
e.g.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
has_many :comments
|
|
||||||
has_many :comments, key: :reviews
|
|
||||||
has_many :comments, serializer: CommentPreviewSerializer
|
|
||||||
has_many :reviews, virtual_value: [{ id: 1 }, { id: 2 }]
|
|
||||||
has_many :comments, key: :last_comments do
|
|
||||||
last(1)
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ::belongs_to
|
|
||||||
|
|
||||||
e.g.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
belongs_to :author, serializer: AuthorPreviewSerializer
|
|
||||||
belongs_to :author, key: :writer
|
|
||||||
belongs_to :post
|
|
||||||
belongs_to :blog
|
|
||||||
def blog
|
|
||||||
Blog.new(id: 999, name: 'Custom blog')
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### Polymorphic Relationships
|
|
||||||
|
|
||||||
Polymorphic relationships are serialized by specifying the relationship, like any other association. For example:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class PictureSerializer < ActiveModel::Serializer
|
|
||||||
has_one :imageable
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
You can specify the serializers by [overriding serializer_for](serializers.md#overriding-association-serializer-lookup). For more context about polymorphic relationships, see the [tests](../../test/adapter/polymorphic_test.rb) for each adapter.
|
|
||||||
|
|
||||||
### Caching
|
|
||||||
|
|
||||||
#### ::cache
|
|
||||||
|
|
||||||
e.g.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
cache key: 'post', expires_in: 0.1, skip_digest: true
|
|
||||||
cache expires_in: 1.day, skip_digest: true
|
|
||||||
cache key: 'writer', skip_digest: true
|
|
||||||
cache only: [:name], skip_digest: true
|
|
||||||
cache except: [:content], skip_digest: true
|
|
||||||
cache key: 'blog'
|
|
||||||
cache only: [:id]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### #cache_key
|
|
||||||
|
|
||||||
e.g.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# Uses a custom non-time-based cache key
|
|
||||||
def cache_key
|
|
||||||
"#{self.class.name.downcase}/#{self.id}"
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### Other
|
|
||||||
|
|
||||||
#### ::type
|
|
||||||
|
|
||||||
When using the `:json_api` adapter, the `::type` method defines the JSONAPI [type](http://jsonapi.org/format/#document-resource-object-identification) that will be rendered for this serializer.
|
|
||||||
|
|
||||||
When using the `:json` adapter, the `::type` method defines the name of the root element.
|
|
||||||
|
|
||||||
It either takes a `String` or `Symbol` as parameter.
|
|
||||||
|
|
||||||
Note: This method is useful only when using the `:json_api` or `:json` adapter.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
```ruby
|
|
||||||
class UserProfileSerializer < ActiveModel::Serializer
|
|
||||||
type 'profile'
|
|
||||||
|
|
||||||
attribute :name
|
|
||||||
end
|
|
||||||
class AuthorProfileSerializer < ActiveModel::Serializer
|
|
||||||
type :profile
|
|
||||||
|
|
||||||
attribute :name
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
With the `:json_api` adapter, the previous serializers would be rendered as:
|
|
||||||
|
|
||||||
``` json
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"id": "1",
|
|
||||||
"type": "profile",
|
|
||||||
"attributes": {
|
|
||||||
"name": "Julia"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
With the `:json` adapter, the previous serializer would be rendered as:
|
|
||||||
|
|
||||||
``` json
|
|
||||||
{
|
|
||||||
"profile": {
|
|
||||||
"name": "Julia"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ::link
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
link :self do
|
|
||||||
href "https://example.com/link_author/#{object.id}"
|
|
||||||
end
|
|
||||||
link(:author) { link_author_url(object) }
|
|
||||||
link(:link_authors) { link_authors_url }
|
|
||||||
link :other, 'https://example.com/resource'
|
|
||||||
link(:posts) { link_author_posts_url(object) }
|
|
||||||
```
|
|
||||||
|
|
||||||
#### #object
|
|
||||||
|
|
||||||
The object being serialized.
|
|
||||||
|
|
||||||
#### #root
|
|
||||||
|
|
||||||
Resource root which is included in `JSON` adapter. As you can see at [Adapters Document](adapters.md), `Attribute` adapter (default) and `JSON API` adapter does not include root at top level.
|
|
||||||
By default, the resource root comes from the `model_name` of the serialized object's class.
|
|
||||||
|
|
||||||
There are several ways to specify root:
|
|
||||||
* [Overriding the root key](rendering.md#overriding-the-root-key)
|
|
||||||
* [Setting `type`](serializers.md#type)
|
|
||||||
* Specifying the `root` option, e.g. `root: 'specific_name'`, during the serializer's initialization:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
ActiveModelSerializers::SerializableResource.new(foo, root: 'bar')
|
|
||||||
```
|
|
||||||
|
|
||||||
#### #scope
|
|
||||||
|
|
||||||
Allows you to include in the serializer access to an external method.
|
|
||||||
|
|
||||||
It's intended to provide an authorization context to the serializer, so that
|
|
||||||
you may e.g. show an admin all comments on a post, else only published comments.
|
|
||||||
|
|
||||||
- `scope` is a method on the serializer instance that comes from `options[:scope]`. It may be nil.
|
|
||||||
- `scope_name` is an option passed to the new serializer (`options[:scope_name]`). The serializer
|
|
||||||
defines a method with that name that calls the `scope`, e.g. `def current_user; scope; end`.
|
|
||||||
Note: it does not define the method if the serializer instance responds to it.
|
|
||||||
|
|
||||||
That's a lot of words, so here's some examples:
|
|
||||||
|
|
||||||
First, let's assume the serializer is instantiated in the controller, since that's the usual scenario.
|
|
||||||
We'll refer to the serialization context as `controller`.
|
|
||||||
|
|
||||||
| options | `Serializer#scope` | method definition |
|
|
||||||
|-------- | ------------------|--------------------|
|
|
||||||
| `scope: current_user, scope_name: :current_user` | `current_user` | `Serializer#current_user` calls `controller.current_user`
|
|
||||||
| `scope: view_context, scope_name: :view_context` | `view_context` | `Serializer#view_context` calls `controller.view_context`
|
|
||||||
|
|
||||||
We can take advantage of the scope to customize the objects returned based
|
|
||||||
on the current user (scope).
|
|
||||||
|
|
||||||
For example, we can limit the posts the current user sees to those they created:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class PostSerializer < ActiveModel::Serializer
|
|
||||||
attributes :id, :title, :body
|
|
||||||
|
|
||||||
# scope comments to those created_by the current user
|
|
||||||
has_many :comments do
|
|
||||||
object.comments.where(created_by: current_user)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
Whether you write the method as above or as `object.comments.where(created_by: scope)`
|
|
||||||
is a matter of preference (assuming `scope_name` has been set).
|
|
||||||
|
|
||||||
##### Controller Authorization Context
|
|
||||||
|
|
||||||
In the controller, the scope/scope_name options are equal to
|
|
||||||
the [`serialization_scope`method](https://github.com/rails-api/active_model_serializers/blob/d02cd30fe55a3ea85e1d351b6e039620903c1871/lib/action_controller/serialization.rb#L13-L20),
|
|
||||||
which is `:current_user`, by default.
|
|
||||||
|
|
||||||
Specifically, the `scope_name` is defaulted to `:current_user`, and may be set as
|
|
||||||
`serialization_scope :view_context`. The `scope` is set to `send(scope_name)` when `scope_name` is
|
|
||||||
present and the controller responds to `scope_name`.
|
|
||||||
|
|
||||||
Thus, in a serializer, the controller provides `current_user` as the
|
|
||||||
current authorization scope when you call `render :json`.
|
|
||||||
|
|
||||||
**IMPORTANT**: Since the scope is set at render, you may want to customize it so that `current_user` isn't
|
|
||||||
called on every request. This was [also a problem](https://github.com/rails-api/active_model_serializers/pull/1252#issuecomment-159810477)
|
|
||||||
in [`0.9`](https://github.com/rails-api/active_model_serializers/tree/0-9-stable#customizing-scope).
|
|
||||||
|
|
||||||
We can change the scope from `current_user` to `view_context`.
|
|
||||||
|
|
||||||
```diff
|
|
||||||
class SomeController < ActionController::Base
|
|
||||||
+ serialization_scope :view_context
|
|
||||||
|
|
||||||
def current_user
|
|
||||||
User.new(id: 2, name: 'Bob', admin: true)
|
|
||||||
end
|
|
||||||
|
|
||||||
def edit
|
|
||||||
user = User.new(id: 1, name: 'Pete')
|
|
||||||
render json: user, serializer: AdminUserSerializer, adapter: :json_api
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
We could then use the controller method `view_context` in our serializer, like so:
|
|
||||||
|
|
||||||
```diff
|
|
||||||
class AdminUserSerializer < ActiveModel::Serializer
|
|
||||||
attributes :id, :name, :can_edit
|
|
||||||
|
|
||||||
def can_edit?
|
|
||||||
+ view_context.current_user.admin?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
So that when we render the `#edit` action, we'll get
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"data":{"id":"1","type":"users","attributes":{"name":"Pete","can_edit":true}}}
|
|
||||||
```
|
|
||||||
|
|
||||||
Where `can_edit` is `view_context.current_user.admin?` (true).
|
|
||||||
|
|
||||||
You can also tell what to set as `serialization_scope` for specific actions.
|
|
||||||
|
|
||||||
For example, use `admin_user` only for `Admin::PostSerializer` and `current_user` for rest.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class PostsController < ActionController::Base
|
|
||||||
|
|
||||||
before_action only: :edit do
|
|
||||||
self.class.serialization_scope :admin_user
|
|
||||||
end
|
|
||||||
|
|
||||||
def show
|
|
||||||
render json: @post, serializer: PostSerializer
|
|
||||||
end
|
|
||||||
|
|
||||||
def edit
|
|
||||||
@post.save
|
|
||||||
render json: @post, serializer: Admin::PostSerializer
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def admin_user
|
|
||||||
User.new(id: 2, name: 'Bob', admin: true)
|
|
||||||
end
|
|
||||||
|
|
||||||
def current_user
|
|
||||||
User.new(id: 2, name: 'Bob', admin: false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
#### #read_attribute_for_serialization(key)
|
|
||||||
|
|
||||||
The serialized value for a given key. e.g. `read_attribute_for_serialization(:title) #=> 'Hello World'`
|
|
||||||
|
|
||||||
#### #links
|
|
||||||
|
|
||||||
PR please :)
|
|
||||||
|
|
||||||
#### #json_key
|
|
||||||
|
|
||||||
PR please :)
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
Given two models, a `Post(title: string, body: text)` and a
|
|
||||||
`Comment(name: string, body: text, post_id: integer)`, you will have two
|
|
||||||
serializers:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class PostSerializer < ActiveModel::Serializer
|
|
||||||
cache key: 'posts', expires_in: 3.hours
|
|
||||||
attributes :title, :body
|
|
||||||
|
|
||||||
has_many :comments
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
and
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class CommentSerializer < ActiveModel::Serializer
|
|
||||||
attributes :name, :body
|
|
||||||
|
|
||||||
belongs_to :post
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
Generally speaking, you, as a user of ActiveModelSerializers, will write (or generate) these
|
|
||||||
serializer classes.
|
|
||||||
|
|
||||||
## More Info
|
|
||||||
|
|
||||||
For more information, see [the Serializer class on GitHub](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model/serializer.rb)
|
|
||||||
|
|
||||||
## Overriding association methods
|
|
||||||
|
|
||||||
To override an association, call `has_many`, `has_one` or `belongs_to` with a block:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class PostSerializer < ActiveModel::Serializer
|
|
||||||
has_many :comments do
|
|
||||||
object.comments.active
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Overriding attribute methods
|
|
||||||
|
|
||||||
To override an attribute, call `attribute` with a block:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class PostSerializer < ActiveModel::Serializer
|
|
||||||
attribute :body do
|
|
||||||
object.body.downcase
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Overriding association serializer lookup
|
|
||||||
|
|
||||||
If you want to define a specific serializer lookup for your associations, you can override
|
|
||||||
the `ActiveModel::Serializer.serializer_for` method to return a serializer class based on defined conditions.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class MySerializer < ActiveModel::Serializer
|
|
||||||
def self.serializer_for(model, options)
|
|
||||||
return SparseAdminSerializer if model.class == 'Admin'
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
# the rest of the serializer
|
|
||||||
end
|
|
||||||
```
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 276 KiB |
@ -1,138 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# How to add pagination links
|
|
||||||
|
|
||||||
### JSON API adapter
|
|
||||||
|
|
||||||
Pagination links will be included in your response automatically as long as
|
|
||||||
the resource is paginated and if you are using the ```JsonApi``` adapter.
|
|
||||||
|
|
||||||
If you want pagination links in your response, use [Kaminari](https://github.com/amatsuda/kaminari)
|
|
||||||
or [WillPaginate](https://github.com/mislav/will_paginate).
|
|
||||||
|
|
||||||
Although the other adapters do not have this feature, it is possible to
|
|
||||||
implement pagination links to `JSON` adapter. For more information about it,
|
|
||||||
please check our docs.
|
|
||||||
|
|
||||||
###### Kaminari examples
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
#array
|
|
||||||
@posts = Kaminari.paginate_array([1, 2, 3]).page(3).per(1)
|
|
||||||
render json: @posts
|
|
||||||
|
|
||||||
#active_record
|
|
||||||
@posts = Post.page(3).per(1)
|
|
||||||
render json: @posts
|
|
||||||
```
|
|
||||||
|
|
||||||
###### WillPaginate examples
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
#array
|
|
||||||
@posts = [1,2,3].paginate(page: 3, per_page: 1)
|
|
||||||
render json: @posts
|
|
||||||
|
|
||||||
#active_record
|
|
||||||
@posts = Post.page(3).per_page(1)
|
|
||||||
render json: @posts
|
|
||||||
```
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
ActiveModelSerializers.config.adapter = :json_api
|
|
||||||
```
|
|
||||||
|
|
||||||
ex:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"type": "articles",
|
|
||||||
"id": "3",
|
|
||||||
"attributes": {
|
|
||||||
"title": "JSON API paints my bikeshed!",
|
|
||||||
"body": "The shortest article. Ever.",
|
|
||||||
"created": "2015-05-22T14:56:29.000Z",
|
|
||||||
"updated": "2015-05-22T14:56:28.000Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"links": {
|
|
||||||
"self": "http://example.com/articles?page[number]=3&page[size]=1",
|
|
||||||
"first": "http://example.com/articles?page[number]=1&page[size]=1",
|
|
||||||
"prev": "http://example.com/articles?page[number]=2&page[size]=1",
|
|
||||||
"next": "http://example.com/articles?page[number]=4&page[size]=1",
|
|
||||||
"last": "http://example.com/articles?page[number]=13&page[size]=1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
ActiveModelSerializers pagination relies on a paginated collection with the methods `current_page`, `total_pages`, and `size`, such as are supported by both [Kaminari](https://github.com/amatsuda/kaminari) or [WillPaginate](https://github.com/mislav/will_paginate).
|
|
||||||
|
|
||||||
|
|
||||||
### JSON adapter
|
|
||||||
|
|
||||||
If you are not using `JSON` adapter, pagination links will not be included automatically, but it is possible to do so using `meta` key.
|
|
||||||
|
|
||||||
Add this method to your base API controller.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
def pagination_dict(collection)
|
|
||||||
{
|
|
||||||
current_page: collection.current_page,
|
|
||||||
next_page: collection.next_page,
|
|
||||||
prev_page: collection.prev_page, # use collection.previous_page when using will_paginate
|
|
||||||
total_pages: collection.total_pages,
|
|
||||||
total_count: collection.total_count
|
|
||||||
}
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, use it on your render method.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
render json: posts, meta: pagination_dict(posts)
|
|
||||||
```
|
|
||||||
|
|
||||||
ex.
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"posts": [
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "JSON API paints my bikeshed!",
|
|
||||||
"body": "The shortest article. Ever."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"meta": {
|
|
||||||
"current_page": 3,
|
|
||||||
"next_page": 4,
|
|
||||||
"prev_page": 2,
|
|
||||||
"total_pages": 10,
|
|
||||||
"total_count": 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also achieve the same result if you have a helper method that adds the pagination info in the meta tag. For instance, in your action specify a custom serializer.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
render json: @posts, each_serializer: PostPreviewSerializer, meta: meta_attributes(@posts)
|
|
||||||
```
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
#expects pagination!
|
|
||||||
def meta_attributes(collection, extra_meta = {})
|
|
||||||
{
|
|
||||||
current_page: collection.current_page,
|
|
||||||
next_page: collection.next_page,
|
|
||||||
prev_page: collection.prev_page, # use collection.previous_page when using will_paginate
|
|
||||||
total_pages: collection.total_pages,
|
|
||||||
total_count: collection.total_count
|
|
||||||
}.merge(extra_meta)
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### Attributes adapter
|
|
||||||
|
|
||||||
This adapter does not allow us to use `meta` key, due to that it is not possible to add pagination links.
|
|
||||||
@ -1,140 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# How to add relationship links
|
|
||||||
|
|
||||||
ActiveModelSerializers offers you many ways to add links in your JSON, depending on your needs.
|
|
||||||
The most common use case for links is supporting nested resources.
|
|
||||||
|
|
||||||
The following examples are without included relationship data (`include` param is empty),
|
|
||||||
specifically the following Rails controller was used for these examples:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class Api::V1::UsersController < ApplicationController
|
|
||||||
def show
|
|
||||||
render jsonapi: User.find(params[:id]),
|
|
||||||
serializer: Api::V1::UserSerializer,
|
|
||||||
include: []
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
Bear in mind though that ActiveModelSerializers are [framework-agnostic](outside_controller_use.md), Rails is just a common example here.
|
|
||||||
|
|
||||||
### Links as an attribute of a resource
|
|
||||||
**This is applicable to JSON and Attributes adapters**
|
|
||||||
|
|
||||||
You can define an attribute in the resource, named `links`.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class Api::V1::UserSerializer < ActiveModel::Serializer
|
|
||||||
include Rails.application.routes.url_helpers
|
|
||||||
|
|
||||||
attributes :id, :name
|
|
||||||
|
|
||||||
attribute :links do
|
|
||||||
id = object.id
|
|
||||||
{
|
|
||||||
self: api_v1_user_path(id),
|
|
||||||
microposts: api_v1_microposts_path(user_id: id)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
Using the `JSON` adapter, this will result in:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"user": {
|
|
||||||
"id": "1",
|
|
||||||
"name": "John",
|
|
||||||
"links": {
|
|
||||||
"self": "/api/v1/users/1",
|
|
||||||
"microposts": "/api/v1/microposts?user_id=1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Links as a property of the resource definiton
|
|
||||||
**This is only applicable to JSONAPI adapter**
|
|
||||||
|
|
||||||
You can use the `link` class method to define the links you need in the resource's primary data.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class Api::V1::UserSerializer < ActiveModel::Serializer
|
|
||||||
attributes :id, :name
|
|
||||||
|
|
||||||
link(:self) { api_v1_user_path(object.id) }
|
|
||||||
link(:microposts) { api_v1_microposts_path(user_id: object.id) }
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
Using the `JSONAPI` adapter, this will result in:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"id": "1",
|
|
||||||
"type": "users",
|
|
||||||
"attributes": {
|
|
||||||
"name": "Example User"
|
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"self": "/api/v1/users/1",
|
|
||||||
"microposts": "/api/v1/microposts?user_id=1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Links that follow the JSONAPI spec
|
|
||||||
**This is only applicable to JSONAPI adapter**
|
|
||||||
|
|
||||||
If you have a JSONAPI-strict client that you are working with (like `ember-data`)
|
|
||||||
you need to construct the links inside the relationships. Also the link to fetch the
|
|
||||||
relationship data must be under the `related` attribute, whereas to manipulate the
|
|
||||||
relationship (in case of many-to-many relationship) must be under the `self` attribute.
|
|
||||||
|
|
||||||
You can find more info in the [spec](http://jsonapi.org/format/#document-resource-object-relationships).
|
|
||||||
|
|
||||||
Here is how you can do this:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class Api::V1::UserSerializer < ActiveModel::Serializer
|
|
||||||
attributes :id, :name
|
|
||||||
|
|
||||||
has_many :microposts, serializer: Api::V1::MicropostSerializer do
|
|
||||||
link(:related) { api_v1_microposts_path(user_id: object.id) }
|
|
||||||
|
|
||||||
microposts = object.microposts
|
|
||||||
# The following code is needed to avoid n+1 queries.
|
|
||||||
# Core devs are working to remove this necessity.
|
|
||||||
# See: https://github.com/rails-api/active_model_serializers/issues/1325
|
|
||||||
microposts.loaded? ? microposts : microposts.none
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
This will result in:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"id": "1",
|
|
||||||
"type": "users",
|
|
||||||
"attributes": {
|
|
||||||
"name": "Example User"
|
|
||||||
},
|
|
||||||
"relationships": {
|
|
||||||
"microposts": {
|
|
||||||
"data": [],
|
|
||||||
"links": {
|
|
||||||
"related": "/api/v1/microposts?user_id=1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# How to add root key
|
|
||||||
|
|
||||||
Add the root key to your API is quite simple with ActiveModelSerializers. The **Adapter** is what determines the format of your JSON response. The default adapter is the ```Attributes``` which doesn't have the root key, so your response is something similar to:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "Awesome Post Tile",
|
|
||||||
"content": "Post content"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
In order to add the root key you need to use the ```JSON``` Adapter, you can change this in an initializer:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
ActiveModelSerializers.config.adapter = :json
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also specify a class as adapter, as long as it complies with the ActiveModelSerializers adapters interface.
|
|
||||||
It will add the root key to all your serialized endpoints.
|
|
||||||
|
|
||||||
ex:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"post": {
|
|
||||||
"id": 1,
|
|
||||||
"title": "Awesome Post Tile",
|
|
||||||
"content": "Post content"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
or if it returns a collection:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"posts": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "Awesome Post Tile",
|
|
||||||
"content": "Post content"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "Another Post Tile",
|
|
||||||
"content": "Another post content"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
[There are several ways to specify root](../general/serializers.md#root) when using the JSON adapter.
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
The ActiveModelSerializers grape formatter relies on the existence of `env['grape.request']` which is implemeted by `Grape::Middleware::Globals`. You can meet his dependency by calling it before mounting the endpoints.
|
|
||||||
|
|
||||||
In the simpliest way:
|
|
||||||
|
|
||||||
```
|
|
||||||
class API < Grape::API
|
|
||||||
# @note Make sure this is above you're first +mount+
|
|
||||||
use Grape::Middleware::Globals
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
or more like what is shown in current Grape tutorials:
|
|
||||||
|
|
||||||
```
|
|
||||||
module MyApi
|
|
||||||
class ApiBase < Grape::API
|
|
||||||
use Grape::Middleware::Globals
|
|
||||||
|
|
||||||
require 'grape/active_model_serializers'
|
|
||||||
include Grape::ActiveModelSerializers
|
|
||||||
|
|
||||||
mount MyApi::V1::ApiBase
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
You could meet this dependency with your own middleware. The invocation might look like:
|
|
||||||
|
|
||||||
```
|
|
||||||
module MyApi
|
|
||||||
class ApiBase < Grape::API
|
|
||||||
use My::Middleware::Thingamabob
|
|
||||||
|
|
||||||
require 'grape/active_model_serializers'
|
|
||||||
include Grape::ActiveModelSerializers
|
|
||||||
|
|
||||||
mount MyApi::V1::ApiBase
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
## Using ActiveModelSerializers Outside Of A Controller
|
|
||||||
|
|
||||||
### Serializing a resource
|
|
||||||
|
|
||||||
In ActiveModelSerializers versions 0.10 or later, serializing resources outside of the controller context is fairly simple:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# Create our resource
|
|
||||||
post = Post.create(title: "Sample post", body: "I love Active Model Serializers!")
|
|
||||||
|
|
||||||
# Optional options parameters for both the serializer and instance
|
|
||||||
options = {serializer: PostDetailedSerializer, username: 'sample user'}
|
|
||||||
|
|
||||||
# Create a serializable resource instance
|
|
||||||
serializable_resource = ActiveModelSerializers::SerializableResource.new(post, options)
|
|
||||||
|
|
||||||
# Convert your resource into json
|
|
||||||
model_json = serializable_resource.as_json
|
|
||||||
```
|
|
||||||
The object that is passed to `ActiveModelSerializers::SerializableResource.new` can be a single resource or a collection.
|
|
||||||
The additional options are the same options that are passed [through controllers](../general/rendering.md#explicit-serializer).
|
|
||||||
|
|
||||||
### Looking up the Serializer for a Resource
|
|
||||||
|
|
||||||
If you want to retrieve the serializer class for a specific resource, you can do the following:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# Create our resource
|
|
||||||
post = Post.create(title: "Another Example", body: "So much fun.")
|
|
||||||
|
|
||||||
# Optional options parameters
|
|
||||||
options = {}
|
|
||||||
|
|
||||||
# Retrieve the default serializer for posts
|
|
||||||
serializer = ActiveModel::Serializer.serializer_for(post, options)
|
|
||||||
```
|
|
||||||
|
|
||||||
You could also retrieve the serializer via:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
ActiveModelSerializers::SerializableResource.new(post, options).serializer
|
|
||||||
```
|
|
||||||
|
|
||||||
Both approaches will return the serializer class that will be used for the resource.
|
|
||||||
|
|
||||||
Additionally, you could retrieve the serializer instance for the resource via:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
ActiveModelSerializers::SerializableResource.new(post, options).serializer_instance
|
|
||||||
```
|
|
||||||
|
|
||||||
## Serializing before controller render
|
|
||||||
|
|
||||||
At times, you might want to use a serializer without rendering it to the view. For those cases, you can create an instance of `ActiveModelSerializers::SerializableResource` with
|
|
||||||
the resource you want to be serialized and call `.as_json`.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
def create
|
|
||||||
message = current_user.messages.create!(message_params)
|
|
||||||
message_json = ActiveModelSerializers::SerializableResource.new(message).as_json
|
|
||||||
MessageCreationWorker.perform(message_json)
|
|
||||||
head 204
|
|
||||||
end
|
|
||||||
```
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# Passing Arbitrary Options To A Serializer
|
|
||||||
|
|
||||||
In addition to the [`serialization_scope`](../general/serializers.md#scope), any options passed to `render`
|
|
||||||
that are not reserved for the [adapter](../general/rendering.md#adapter_opts)
|
|
||||||
are available in the serializer as [instance_options](../general/serializers.md#instance_options).
|
|
||||||
|
|
||||||
For example, we could pass in a field, such as `user_id` into our serializer.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# posts_controller.rb
|
|
||||||
class PostsController < ApplicationController
|
|
||||||
def dashboard
|
|
||||||
render json: @post, user_id: 12
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# post_serializer.rb
|
|
||||||
class PostSerializer < ActiveModel::Serializer
|
|
||||||
attributes :id, :title, :body
|
|
||||||
|
|
||||||
def comments_by_me
|
|
||||||
Comments.where(user_id: instance_options[:user_id], post_id: object.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# How to serialize a Plain-Old Ruby Object (PORO)
|
|
||||||
|
|
||||||
When you are first getting started with ActiveModelSerializers, it may seem only `ActiveRecord::Base` objects can be serializable,
|
|
||||||
but pretty much any object can be serializable with ActiveModelSerializers.
|
|
||||||
Here is an example of a PORO that is serializable in most situations:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# my_model.rb
|
|
||||||
class MyModel
|
|
||||||
alias :read_attribute_for_serialization :send
|
|
||||||
attr_accessor :id, :name, :level
|
|
||||||
|
|
||||||
def initialize(attributes)
|
|
||||||
@id = attributes[:id]
|
|
||||||
@name = attributes[:name]
|
|
||||||
@level = attributes[:level]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.model_name
|
|
||||||
@_model_name ||= ActiveModel::Name.new(self)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
The [ActiveModel::Serializer::Lint::Tests](../../lib/active_model/serializer/lint.rb)
|
|
||||||
define and validate which methods ActiveModelSerializers expects to be implemented.
|
|
||||||
|
|
||||||
An implementation of the complete spec is included either for use or as reference:
|
|
||||||
[`ActiveModelSerializers::Model`](../../lib/active_model_serializers/model.rb).
|
|
||||||
You can use in production code that will make your PORO a lot cleaner.
|
|
||||||
|
|
||||||
The above code now becomes:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# my_model.rb
|
|
||||||
class MyModel < ActiveModelSerializers::Model
|
|
||||||
attributes :id, :name, :level
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
The default serializer would be `MyModelSerializer`.
|
|
||||||
|
|
||||||
*IMPORTANT*: There is a surprising behavior (bug) in the current implementation of ActiveModelSerializers::Model that
|
|
||||||
prevents an accessor from modifying attributes on the instance. The fix for this bug
|
|
||||||
is a breaking change, so we have made an opt-in configuration.
|
|
||||||
|
|
||||||
New applications should set:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
ActiveModelSerializers::Model.derive_attributes_from_names_and_fix_accessors
|
|
||||||
```
|
|
||||||
|
|
||||||
Existing applications can use the fix *and* avoid breaking changes
|
|
||||||
by making a superclass for new models. For example:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class SerializablePoro < ActiveModelSerializers::Model
|
|
||||||
derive_attributes_from_names_and_fix_accessors
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
So that `MyModel` above would inherit from `SerializablePoro`.
|
|
||||||
|
|
||||||
`derive_attributes_from_names_and_fix_accessors` prepends the `DeriveAttributesFromNamesAndFixAccessors`
|
|
||||||
module and does the following:
|
|
||||||
|
|
||||||
- `id` will *always* be in the attributes. (This is until we separate out the caching requirement for POROs.)
|
|
||||||
- Overwrites the `attributes` method to that it only returns declared attributes.
|
|
||||||
`attributes` will now be a frozen hash with indifferent access.
|
|
||||||
|
|
||||||
For more information, see [README: What does a 'serializable resource' look like?](../../README.md#what-does-a-serializable-resource-look-like).
|
|
||||||
@ -1,154 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# How to test
|
|
||||||
|
|
||||||
## Controller Serializer Usage
|
|
||||||
|
|
||||||
ActiveModelSerializers provides a `assert_serializer` method to be used on your controller tests to
|
|
||||||
assert that a specific serializer was used.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class PostsControllerTest < ActionController::TestCase
|
|
||||||
test "should render post serializer" do
|
|
||||||
get :index
|
|
||||||
assert_serializer "PostSerializer"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
See [ActiveModelSerializers::Test::Serializer](../../lib/active_model_serializers/test/serializer.rb)
|
|
||||||
for more examples and documentation.
|
|
||||||
|
|
||||||
## Serialization against a schema
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
|
|
||||||
To use the `assert_response_schema` you need to have the
|
|
||||||
[`json_schema`](https://github.com/brandur/json_schema) on your Gemfile. Please
|
|
||||||
add it to your Gemfile and run `$ bundle install`.
|
|
||||||
|
|
||||||
### Minitest test helpers
|
|
||||||
|
|
||||||
ActiveModelSerializers provides a `assert_response_schema` method to be used on your controller tests to
|
|
||||||
assert the response against a [JSON Schema](http://json-schema.org/). Let's take
|
|
||||||
a look in an example.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class PostsController < ApplicationController
|
|
||||||
def show
|
|
||||||
@post = Post.find(params[:id])
|
|
||||||
|
|
||||||
render json: @post
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
To test the `posts#show` response of this controller we need to create a file
|
|
||||||
named `test/support/schemas/posts/show.json`. The helper uses a naming convention
|
|
||||||
to locate the file.
|
|
||||||
|
|
||||||
This file is a JSON Schema representation of our response.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"properties": {
|
|
||||||
"title" : { "type" : "string" },
|
|
||||||
"content" : { "type" : "string" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
With all in place we can go to our test and use the helper.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class PostsControllerTest < ActionController::TestCase
|
|
||||||
test "should render right response" do
|
|
||||||
get :index
|
|
||||||
assert_response_schema
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Load a custom schema
|
|
||||||
|
|
||||||
If we need to use another schema, for example when we have a namespaced API that
|
|
||||||
shows the same response, we can pass the path of the schema.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
module V1
|
|
||||||
class PostsController < ApplicationController
|
|
||||||
def show
|
|
||||||
@post = Post.find(params[:id])
|
|
||||||
|
|
||||||
render json: @post
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class V1::PostsControllerTest < ActionController::TestCase
|
|
||||||
test "should render right response" do
|
|
||||||
get :index
|
|
||||||
assert_response_schema('posts/show.json')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Change the schema path
|
|
||||||
|
|
||||||
By default all schemas are created at `test/support/schemas`. If we are using
|
|
||||||
RSpec for example we can change this to `spec/support/schemas` defining the
|
|
||||||
default schema path in an initializer.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
ActiveModelSerializers.config.schema_path = 'spec/support/schemas'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Using with the Heroku’s JSON Schema-based tools
|
|
||||||
|
|
||||||
To use the test helper with the [prmd](https://github.com/interagent/prmd) and
|
|
||||||
[committee](https://github.com/interagent/committee).
|
|
||||||
|
|
||||||
We need to change the schema path to the recommended by prmd:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
ActiveModelSerializers.config.schema_path = 'docs/schema/schemata'
|
|
||||||
```
|
|
||||||
|
|
||||||
We also need to structure our schemata according to Heroku's conventions
|
|
||||||
(e.g. including
|
|
||||||
[required metadata](https://github.com/interagent/prmd/blob/master/docs/schemata.md#meta-data)
|
|
||||||
and [links](https://github.com/interagent/prmd/blob/master/docs/schemata.md#links).
|
|
||||||
|
|
||||||
#### JSON Pointers
|
|
||||||
|
|
||||||
If we plan to use [JSON
|
|
||||||
Pointers](http://spacetelescope.github.io/understanding-json-schema/UnderstandingJSONSchema.pdf) we need to define the `id` attribute on the schema. Example:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// attributes.json
|
|
||||||
|
|
||||||
{
|
|
||||||
"id": "file://attributes.json#",
|
|
||||||
"properties": {
|
|
||||||
"name" : { "type" : "string" },
|
|
||||||
"description" : { "type" : "string" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
// show.json
|
|
||||||
|
|
||||||
{
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"$ref": "file://attributes.json#/properties/name"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"$ref": "file://attributes.json#/properties/description"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@ -1,265 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# How to migrate from `0.8` to `0.10` safely
|
|
||||||
|
|
||||||
## Disclaimer
|
|
||||||
### Proceed at your own risk
|
|
||||||
This document attempts to outline steps to upgrade your app based on the collective experience of
|
|
||||||
developers who have done this already. It may not cover all edge cases and situations that may cause issues,
|
|
||||||
so please proceed with a certain level of caution.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
This document outlines the steps needed to migrate from `0.8` to `0.10`. The method described
|
|
||||||
below has been created via the collective knowledge of contributions of those who have done
|
|
||||||
the migration successfully. The method has been tested specifically for migrating from `0.8.3`
|
|
||||||
to `0.10.2`.
|
|
||||||
|
|
||||||
The high level approach is to upgrade to `0.10` and change all serializers to use
|
|
||||||
a backwards-compatible `ActiveModel::V08::Serializer`or `ActiveModel::V08::CollectionSerializer`
|
|
||||||
and a `ActiveModelSerializers::Adapter::V08Adapter`. After a few more manual changes, you should have the same
|
|
||||||
functionality as you had with `AMS 0.8`. Then, you can continue to develop in your app by creating
|
|
||||||
new serializers that don't use these backwards compatible versions and slowly migrate
|
|
||||||
existing serializers to the `0.10` versions as needed.
|
|
||||||
|
|
||||||
### `0.10` breaking changes
|
|
||||||
- Passing a serializer to `render json:` is no longer supported
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
render json: CustomerSerializer.new(customer) # rendered in 0.8, errors in 0.10
|
|
||||||
```
|
|
||||||
|
|
||||||
- Passing a nil resource to serializer now fails
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
CustomerSerializer.new(nil) # returned nil in 0.8, throws error in 0.10
|
|
||||||
```
|
|
||||||
|
|
||||||
- Attribute methods are no longer defined on the serializer, and must be explicitly
|
|
||||||
accessed through `object`
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class MySerializer
|
|
||||||
attributes :foo, :bar
|
|
||||||
|
|
||||||
def foo
|
|
||||||
bar + 1 # bar does not work, needs to be object.bar in 0.10
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
- `root` option to collection serializer behaves differently
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# in 0.8
|
|
||||||
ActiveModel::ArraySerializer.new(resources, root: "resources")
|
|
||||||
# resulted in { "resources": <serialized_resources> }, does not work in 0.10
|
|
||||||
```
|
|
||||||
|
|
||||||
- No default serializer when serializer doesn't exist
|
|
||||||
- `@options` changed to `instance_options`
|
|
||||||
- Nested relationships are no longer walked by default. Use the `:include` option at **controller `render`** level to specify what relationships to walk. E.g. `render json: @post, include: {comments: :author}` if you want the `author` relationship walked, otherwise the json would only include the post with comments. See: https://github.com/rails-api/active_model_serializers/pull/1127
|
|
||||||
- To emulate `0.8`'s walking of arbitrarily deep relationships use: `include: '**'`. E.g. `render json: @post, include: '**'`
|
|
||||||
|
|
||||||
## Steps to migrate
|
|
||||||
|
|
||||||
### 1. Upgrade the `active_model_serializer` gem in you `Gemfile`
|
|
||||||
Change to `gem 'active_model_serializers', '~> 0.10'` and run `bundle install`
|
|
||||||
|
|
||||||
### 2. Add `ActiveModel::V08::Serializer`
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
module ActiveModel
|
|
||||||
module V08
|
|
||||||
class Serializer < ActiveModel::Serializer
|
|
||||||
include Rails.application.routes.url_helpers
|
|
||||||
|
|
||||||
# AMS 0.8 would delegate method calls from within the serializer to the
|
|
||||||
# object.
|
|
||||||
def method_missing(*args)
|
|
||||||
method = args.first
|
|
||||||
read_attribute_for_serialization(method)
|
|
||||||
end
|
|
||||||
|
|
||||||
alias_method :options, :instance_options
|
|
||||||
|
|
||||||
# Since attributes could be read from the `object` via `method_missing`,
|
|
||||||
# the `try` method did not behave as before. This patches `try` with the
|
|
||||||
# original implementation plus the addition of
|
|
||||||
# ` || object.respond_to?(a.first, true)` to check if the object responded to
|
|
||||||
# the given method.
|
|
||||||
def try(*a, &b)
|
|
||||||
if a.empty? || respond_to?(a.first, true) || object.respond_to?(a.first, true)
|
|
||||||
try!(*a, &b)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# AMS 0.8 would return nil if the serializer was initialized with a nil
|
|
||||||
# resource.
|
|
||||||
def serializable_hash(adapter_options = nil,
|
|
||||||
options = {},
|
|
||||||
adapter_instance =
|
|
||||||
self.class.serialization_adapter_instance)
|
|
||||||
object.nil? ? nil : super
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
```
|
|
||||||
Add this class to your app however you see fit. This is the class that your existing serializers
|
|
||||||
that inherit from `ActiveModel::Serializer` should inherit from.
|
|
||||||
|
|
||||||
### 3. Add `ActiveModel::V08::CollectionSerializer`
|
|
||||||
```ruby
|
|
||||||
module ActiveModel
|
|
||||||
module V08
|
|
||||||
class CollectionSerializer < ActiveModel::Serializer::CollectionSerializer
|
|
||||||
# In AMS 0.8, passing an ArraySerializer instance with a `root` option
|
|
||||||
# properly nested the serialized resources within the given root.
|
|
||||||
# Ex.
|
|
||||||
#
|
|
||||||
# class MyController < ActionController::Base
|
|
||||||
# def index
|
|
||||||
# render json: ActiveModel::Serializer::ArraySerializer
|
|
||||||
# .new(resources, root: "resources")
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# Produced
|
|
||||||
#
|
|
||||||
# {
|
|
||||||
# "resources": [
|
|
||||||
# <serialized_resource>,
|
|
||||||
# ...
|
|
||||||
# ]
|
|
||||||
# }
|
|
||||||
def as_json(options = {})
|
|
||||||
if root
|
|
||||||
{
|
|
||||||
root => super
|
|
||||||
}
|
|
||||||
else
|
|
||||||
super
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# AMS 0.8 used `DefaultSerializer` if it couldn't find a serializer for
|
|
||||||
# the given resource. When not using an adapter, this is not true in
|
|
||||||
# `0.10`
|
|
||||||
def serializer_from_resource(resource, serializer_context_class, options)
|
|
||||||
serializer_class =
|
|
||||||
options.fetch(:serializer) { serializer_context_class.serializer_for(resource) }
|
|
||||||
|
|
||||||
if serializer_class.nil? # rubocop:disable Style/GuardClause
|
|
||||||
DefaultSerializer.new(resource, options)
|
|
||||||
else
|
|
||||||
serializer_class.new(resource, options.except(:serializer))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class DefaultSerializer
|
|
||||||
attr_reader :object, :options
|
|
||||||
|
|
||||||
def initialize(object, options={})
|
|
||||||
@object, @options = object, options
|
|
||||||
end
|
|
||||||
|
|
||||||
def serializable_hash
|
|
||||||
@object.as_json(@options)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
Add this class to your app however you see fit. This is the class that existing uses of
|
|
||||||
`ActiveModel::ArraySerializer` should be changed to use.
|
|
||||||
|
|
||||||
### 4. Add `ActiveModelSerializers::Adapter::V08Adapter`
|
|
||||||
```ruby
|
|
||||||
module ActiveModelSerializers
|
|
||||||
module Adapter
|
|
||||||
class V08Adapter < ActiveModelSerializers::Adapter::Base
|
|
||||||
def serializable_hash(options = nil)
|
|
||||||
options ||= {}
|
|
||||||
|
|
||||||
if serializer.respond_to?(:each)
|
|
||||||
if serializer.root
|
|
||||||
delegate_to_json_adapter(options)
|
|
||||||
else
|
|
||||||
serializable_hash_for_collection(options)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
serializable_hash_for_single_resource(options)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def serializable_hash_for_collection(options)
|
|
||||||
serializer.map do |s|
|
|
||||||
V08Adapter.new(s, instance_options)
|
|
||||||
.serializable_hash(options)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def serializable_hash_for_single_resource(options)
|
|
||||||
if serializer.object.is_a?(ActiveModel::Serializer)
|
|
||||||
# It is recommended that you add some logging here to indicate
|
|
||||||
# places that should get converted to eventually allow for this
|
|
||||||
# adapter to get removed.
|
|
||||||
@serializer = serializer.object
|
|
||||||
end
|
|
||||||
|
|
||||||
if serializer.root
|
|
||||||
delegate_to_json_adapter(options)
|
|
||||||
else
|
|
||||||
options = serialization_options(options)
|
|
||||||
serializer.serializable_hash(instance_options, options, self)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def delegate_to_json_adapter(options)
|
|
||||||
ActiveModelSerializers::Adapter::Json
|
|
||||||
.new(serializer, instance_options)
|
|
||||||
.serializable_hash(options)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
Add this class to your app however you see fit.
|
|
||||||
|
|
||||||
Add
|
|
||||||
```ruby
|
|
||||||
ActiveModelSerializers.config.adapter =
|
|
||||||
ActiveModelSerializers::Adapter::V08Adapter
|
|
||||||
```
|
|
||||||
to `config/active_model_serializer.rb` to configure AMS to use this
|
|
||||||
class as the default adapter.
|
|
||||||
|
|
||||||
### 5. Change inheritors of `ActiveModel::Serializer` to inherit from `ActiveModel::V08::Serializer`
|
|
||||||
Simple find/replace
|
|
||||||
|
|
||||||
### 6. Remove `private` keyword from serializers
|
|
||||||
Simple find/replace. This is required to allow the `ActiveModel::V08::Serializer`
|
|
||||||
to have proper access to the methods defined in the serializer.
|
|
||||||
|
|
||||||
You may be able to change the `private` to `protected`, but this is hasn't been tested yet.
|
|
||||||
|
|
||||||
### 7. Remove references to `ActiveRecord::Base#active_model_serializer`
|
|
||||||
This method is no longer supported in `0.10`.
|
|
||||||
|
|
||||||
`0.10` does a good job of discovering serializers for `ActiveRecord` objects.
|
|
||||||
|
|
||||||
### 8. Rename `ActiveModel::ArraySerializer` to `ActiveModel::V08::CollectionSerializer`
|
|
||||||
Find/replace uses of `ActiveModel::ArraySerializer` with `ActiveModel::V08::CollectionSerializer`.
|
|
||||||
|
|
||||||
Also, be sure to change the `each_serializer` keyword to `serializer` when calling making the replacement.
|
|
||||||
|
|
||||||
### 9. Replace uses of `@options` to `instance_options` in serializers
|
|
||||||
Simple find/replace
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
After you've done the steps above, you should test your app to ensure that everything is still working properly.
|
|
||||||
|
|
||||||
If you run into issues, please contribute back to this document so others can benefit from your knowledge.
|
|
||||||
|
|
||||||
@ -1,147 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# Integrating with Ember and JSON API
|
|
||||||
|
|
||||||
- [Preparation](./ember-and-json-api.md#preparation)
|
|
||||||
- [Server-Side Changes](./ember-and-json-api.md#server-side-changes)
|
|
||||||
- [Adapter Changes](./ember-and-json-api.md#adapter-changes)
|
|
||||||
- [Serializer Changes](./ember-and-json-api.md#serializer-changes)
|
|
||||||
- [Including Nested Resources](./ember-and-json-api.md#including-nested-resources)
|
|
||||||
|
|
||||||
## Preparation
|
|
||||||
|
|
||||||
Note: This guide assumes that `ember-cli` is used for your ember app.
|
|
||||||
|
|
||||||
The JSON API specification calls for hyphens for multi-word separators. ActiveModelSerializers uses underscores.
|
|
||||||
To solve this, in Ember, both the adapter and the serializer will need some modifications:
|
|
||||||
|
|
||||||
### Server-Side Changes
|
|
||||||
|
|
||||||
First, set the adapter type in an initializer file:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# config/initializers/active_model_serializers.rb
|
|
||||||
ActiveModelSerializers.config.adapter = :json_api
|
|
||||||
```
|
|
||||||
|
|
||||||
or:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# config/initializers/active_model_serializers.rb
|
|
||||||
ActiveModelSerializers.config.adapter = ActiveModelSerializers::Adapter::JsonApi
|
|
||||||
```
|
|
||||||
|
|
||||||
You will also want to set the `key_transform` to `:unaltered` since you will adjust the attributes in your Ember serializer to use underscores instead of dashes later. You could also use `:underscore`, but `:unaltered` is better for performance.
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# config/initializers/active_model_serializers.rb
|
|
||||||
ActiveModelSerializers.config.key_transform = :unaltered
|
|
||||||
```
|
|
||||||
|
|
||||||
In order to properly handle JSON API responses, we need to register a JSON API renderer, like so:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# config/initializers/active_model_serializers.rb
|
|
||||||
ActiveSupport.on_load(:action_controller) do
|
|
||||||
require 'active_model_serializers/register_jsonapi_renderer'
|
|
||||||
end
|
|
||||||
```
|
|
||||||
Rails also requires your controller to tell it that you accept and generate JSONAPI data. To do that, you use `respond_to` in your controller handlers to tell rails you are consuming and returning jsonapi format data. Without this, Rails will refuse to parse the request body into params. You can add `ActionController::MimeResponds` to your application controller to enable this:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class ApplicationController < ActionController::API
|
|
||||||
include ActionController::MimeResponds
|
|
||||||
end
|
|
||||||
```
|
|
||||||
Then, in your controller you can tell rails you're accepting and rendering the jsonapi format:
|
|
||||||
```ruby
|
|
||||||
# POST /post
|
|
||||||
def create
|
|
||||||
@post = Post.new(post_params)
|
|
||||||
respond_to do |format|
|
|
||||||
if @post.save
|
|
||||||
format.jsonapi { render jsonapi: @post, status: :created, location: @post }
|
|
||||||
else
|
|
||||||
format.jsonapi { render jsonapi: @post.errors, status: :unprocessable_entity }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Only allow a trusted parameter "white list" through.
|
|
||||||
def post_params
|
|
||||||
ActiveModelSerializers::Deserialization.jsonapi_parse!(params, only: [:title, :body] )
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Note:
|
|
||||||
In Rails 5, the "unsafe" method ( `jsonapi_parse!` vs the safe `jsonapi_parse`) throws an `InvalidDocument` exception when the payload does not meet basic criteria for JSON API deserialization.
|
|
||||||
|
|
||||||
|
|
||||||
### Adapter Changes
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// app/adapters/application.js
|
|
||||||
import Ember from 'ember';
|
|
||||||
import DS from 'ember-data';
|
|
||||||
import ENV from "../config/environment";
|
|
||||||
const { underscore, pluralize } = Ember.String;
|
|
||||||
|
|
||||||
export default DS.JSONAPIAdapter.extend({
|
|
||||||
namespace: 'api',
|
|
||||||
// if your rails app is on a different port from your ember app
|
|
||||||
// this can be helpful for development.
|
|
||||||
// in production, the host for both rails and ember should be the same.
|
|
||||||
host: ENV.host,
|
|
||||||
|
|
||||||
// allows the multiword paths in urls to be underscored
|
|
||||||
pathForType: function(type) {
|
|
||||||
let underscored = underscore(type);
|
|
||||||
return pluralize(underscored);
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Serializer Changes
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// app/serializers/application.js
|
|
||||||
import Ember from 'ember';
|
|
||||||
import DS from 'ember-data';
|
|
||||||
var underscore = Ember.String.underscore;
|
|
||||||
|
|
||||||
export default DS.JSONAPISerializer.extend({
|
|
||||||
keyForAttribute: function(attr) {
|
|
||||||
return underscore(attr);
|
|
||||||
},
|
|
||||||
|
|
||||||
keyForRelationship: function(rawKey) {
|
|
||||||
return underscore(rawKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Including Nested Resources
|
|
||||||
|
|
||||||
Ember Data can request related records by using `include`. Below are some examples of how to make Ember Data request the inclusion of related records. For more on `include` usage, see: [The JSON API include examples](./../general/adapters.md#JSON-API)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
store.findRecord('post', postId, { include: 'comments' } );
|
|
||||||
```
|
|
||||||
which will generate the path /posts/{postId}?include='comments'
|
|
||||||
|
|
||||||
So then in your controller, you'll want to be sure to have something like:
|
|
||||||
```ruby
|
|
||||||
render jsonapi: @post, include: params[:include]
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want to use `include` on a collection, you'd write something like this:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
store.query('post', { include: 'comments' });
|
|
||||||
```
|
|
||||||
|
|
||||||
which will generate the path `/posts?include='comments'`
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
# Integration with Grape
|
|
||||||
|
|
||||||
[Grape](https://github.com/ruby-grape/grape) is an opinionated micro-framework for creating REST-like APIs in ruby.
|
|
||||||
|
|
||||||
ActiveModelSerializers currently supports Grape >= 0.13, < 1.0
|
|
||||||
|
|
||||||
To add [Grape](https://github.com/ruby-grape/grape) support, enable the formatter and helper functions by including `Grape::ActiveModelSerializers` in your base endpoint. For example:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
module Example
|
|
||||||
class Dummy < Grape::API
|
|
||||||
require 'grape/active_model_serializers'
|
|
||||||
include Grape::ActiveModelSerializers
|
|
||||||
mount Example::V1::Base
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
Aside from this, [configuration](../general/configuration_options.md) of ActiveModelSerializers is exactly the same.
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
# [JSON API Errors](http://jsonapi.org/format/#errors)
|
|
||||||
|
|
||||||
Rendering error documents requires specifying the error serializer(s):
|
|
||||||
|
|
||||||
- Serializer:
|
|
||||||
- For a single resource: `serializer: ActiveModel::Serializer::ErrorSerializer`.
|
|
||||||
- For a collection: `serializer: ActiveModel::Serializer::ErrorsSerializer`, `each_serializer: ActiveModel::Serializer::ErrorSerializer`.
|
|
||||||
|
|
||||||
The resource **MUST** have a non-empty associated `#errors` object.
|
|
||||||
The `errors` object must have a `#messages` method that returns a hash of error name to array of
|
|
||||||
descriptions.
|
|
||||||
|
|
||||||
## Use in controllers
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
resource = Profile.new(name: 'Name 1',
|
|
||||||
description: 'Description 1',
|
|
||||||
comments: 'Comments 1')
|
|
||||||
resource.errors.add(:name, 'cannot be nil')
|
|
||||||
resource.errors.add(:name, 'must be longer')
|
|
||||||
resource.errors.add(:id, 'must be a uuid')
|
|
||||||
|
|
||||||
render json: resource, status: 422, adapter: :json_api, serializer: ActiveModel::Serializer::ErrorSerializer
|
|
||||||
# #=>
|
|
||||||
# { :errors =>
|
|
||||||
# [
|
|
||||||
# { :source => { :pointer => '/data/attributes/name' }, :detail => 'cannot be nil' },
|
|
||||||
# { :source => { :pointer => '/data/attributes/name' }, :detail => 'must be longer' },
|
|
||||||
# { :source => { :pointer => '/data/attributes/id' }, :detail => 'must be a uuid' }
|
|
||||||
# ]
|
|
||||||
# }.to_json
|
|
||||||
```
|
|
||||||
|
|
||||||
## Direct error document generation
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
options = nil
|
|
||||||
resource = ModelWithErrors.new
|
|
||||||
resource.errors.add(:name, 'must be awesome')
|
|
||||||
|
|
||||||
serializable_resource = ActiveModelSerializers::SerializableResource.new(
|
|
||||||
resource, {
|
|
||||||
serializer: ActiveModel::Serializer::ErrorSerializer,
|
|
||||||
adapter: :json_api
|
|
||||||
})
|
|
||||||
serializable_resource.as_json(options)
|
|
||||||
# #=>
|
|
||||||
# {
|
|
||||||
# :errors =>
|
|
||||||
# [
|
|
||||||
# { :source => { :pointer => '/data/attributes/name' }, :detail => 'must be awesome' }
|
|
||||||
# ]
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
@ -1,151 +0,0 @@
|
|||||||
[Back to Guides](../README.md)
|
|
||||||
|
|
||||||
[](http://jsonapi.org/)
|
|
||||||
|
|
||||||
## JSON API Requests
|
|
||||||
|
|
||||||
- [Query Parameters Spec](http://jsonapi.org/format/#query-parameters)
|
|
||||||
|
|
||||||
Headers:
|
|
||||||
|
|
||||||
- Request: `Accept: application/vnd.api+json`
|
|
||||||
- Response: `Content-Type: application/vnd.api+json`
|
|
||||||
|
|
||||||
### [Fetching Data](http://jsonapi.org/format/#fetching)
|
|
||||||
|
|
||||||
A server MUST support fetching resource data for every URL provided as:
|
|
||||||
|
|
||||||
- a `self` link as part of the top-level links object
|
|
||||||
- a `self` link as part of a resource-level links object
|
|
||||||
- a `related` link as part of a relationship-level links object
|
|
||||||
|
|
||||||
Example supported requests
|
|
||||||
|
|
||||||
- Individual resource or collection
|
|
||||||
- GET /articles
|
|
||||||
- GET /articles/1
|
|
||||||
- GET /articles/1/author
|
|
||||||
- Relationships
|
|
||||||
- GET /articles/1/relationships/comments
|
|
||||||
- GET /articles/1/relationships/author
|
|
||||||
- Optional: [Inclusion of related resources](http://jsonapi.org/format/#fetching-includes) `JSONAPI::IncludeDirective`
|
|
||||||
- GET /articles/1?`include`=comments
|
|
||||||
- GET /articles/1?`include`=comments.author
|
|
||||||
- GET /articles/1?`include`=author,comments.author
|
|
||||||
- GET /articles/1/relationships/comments?`include`=comments.author
|
|
||||||
- Optional: [Sparse Fieldsets](http://jsonapi.org/format/#fetching-sparse-fieldsets) `ActiveModel::Serializer::Fieldset`
|
|
||||||
- GET /articles?`include`=author&`fields`[articles]=title,body&`fields`[people]=name
|
|
||||||
- Optional: [Sorting](http://jsonapi.org/format/#fetching-sorting)
|
|
||||||
- GET /people?`sort`=age
|
|
||||||
- GET /people?`sort`=age,author.name
|
|
||||||
- GET /articles?`sort`=-created,title
|
|
||||||
- Optional: [Pagination](http://jsonapi.org/format/#fetching-pagination)
|
|
||||||
- GET /articles?`page`[number]=3&`page`[size]=1
|
|
||||||
- Optional: [Filtering](http://jsonapi.org/format/#fetching-filtering)
|
|
||||||
- GET /comments?`filter`[post]=1
|
|
||||||
- GET /comments?`filter`[post]=1,2
|
|
||||||
- GET /comments?`filter`[post]=1,2
|
|
||||||
|
|
||||||
### [CRUD Actions](http://jsonapi.org/format/#crud)
|
|
||||||
|
|
||||||
### [Asynchronous Processing](http://jsonapi.org/recommendations/#asynchronous-processing)
|
|
||||||
|
|
||||||
### [Bulk Operations Extension](http://jsonapi.org/extensions/bulk/)
|
|
||||||
|
|
||||||
## JSON API Document Schema
|
|
||||||
|
|
||||||
| JSON API object | JSON API properties | Required | ActiveModelSerializers representation |
|
|
||||||
|-----------------------|----------------------------------------------------------------------------------------------------|----------|---------------------------------------|
|
|
||||||
| schema | oneOf (success, failure, info) | |
|
|
||||||
| success | data, included, meta, links, jsonapi | | AM::SerializableResource
|
|
||||||
| success.meta | meta | | AMS::Adapter::Base#meta
|
|
||||||
| success.included | UniqueArray(resource) | | AMS::Adapter::JsonApi#serializable_hash_for_collection
|
|
||||||
| success.data | data | |
|
|
||||||
| success.links | allOf (links, pagination) | | AMS::Adapter::JsonApi#links_for
|
|
||||||
| success.jsonapi | jsonapi | |
|
|
||||||
| failure | errors, meta, jsonapi | errors | AMS::Adapter::JsonApi#failure_document, #1004
|
|
||||||
| failure.errors | UniqueArray(error) | | AM::S::ErrorSerializer, #1004
|
|
||||||
| meta | Object | |
|
|
||||||
| data | oneOf (resource, UniqueArray(resource)) | | AMS::Adapter::JsonApi#serializable_hash_for_collection,#serializable_hash_for_single_resource
|
|
||||||
| resource | String(type), String(id),<br>attributes, relationships,<br>links, meta | type, id | AM::S::Adapter::JsonApi#primary_data_for
|
|
||||||
| links | Uri(self), Link(related) | | #1028, #1246, #1282
|
|
||||||
| link | oneOf (linkString, linkObject) | |
|
|
||||||
| link.linkString | Uri | |
|
|
||||||
| link.linkObject | Uri(href), meta | href |
|
|
||||||
| attributes | patternProperties(<br>`"^(?!relationships$|links$)\\w[-\\w_]*$"`),<br>any valid JSON | | AM::Serializer#attributes, AMS::Adapter::JsonApi#resource_object_for
|
|
||||||
| relationships | patternProperties(<br>`"^\\w[-\\w_]*$"`);<br>links, relationships.data, meta | | AMS::Adapter::JsonApi#relationships_for
|
|
||||||
| relationships.data | oneOf (relationshipToOne, relationshipToMany) | | AMS::Adapter::JsonApi#resource_identifier_for
|
|
||||||
| relationshipToOne | anyOf(empty, linkage) | |
|
|
||||||
| relationshipToMany | UniqueArray(linkage) | |
|
|
||||||
| empty | null | |
|
|
||||||
| linkage | String(type), String(id), meta | type, id | AMS::Adapter::JsonApi#primary_data_for
|
|
||||||
| pagination | pageObject(first), pageObject(last),<br>pageObject(prev), pageObject(next) | | AMS::Adapter::JsonApi::PaginationLinks#serializable_hash
|
|
||||||
| pagination.pageObject | oneOf(Uri, null) | |
|
|
||||||
| jsonapi | String(version), meta | | AMS::Adapter::JsonApi::Jsonapi#as_json
|
|
||||||
| error | String(id), links, String(status),<br>String(code), String(title),<br>String(detail), error.source, meta | | AM::S::ErrorSerializer, AMS::Adapter::JsonApi::Error.resource_errors
|
|
||||||
| error.source | String(pointer), String(parameter) | | AMS::Adapter::JsonApi::Error.error_source
|
|
||||||
| pointer | [JSON Pointer RFC6901](https://tools.ietf.org/html/rfc6901) | | AMS::JsonPointer
|
|
||||||
|
|
||||||
|
|
||||||
The [http://jsonapi.org/schema](schema/schema.json) makes a nice roadmap.
|
|
||||||
|
|
||||||
### Success Document
|
|
||||||
- [ ] success
|
|
||||||
- [ ] data: `"$ref": "#/definitions/data"`
|
|
||||||
- [ ] included: array of unique items of type `"$ref": "#/definitions/resource"`
|
|
||||||
- [ ] meta: `"$ref": "#/definitions/meta"`
|
|
||||||
- [ ] links:
|
|
||||||
- [ ] link: `"$ref": "#/definitions/links"`
|
|
||||||
- [ ] pagination: ` "$ref": "#/definitions/pagination"`
|
|
||||||
- [ ] jsonapi: ` "$ref": "#/definitions/jsonapi"`
|
|
||||||
|
|
||||||
### Failure Document
|
|
||||||
|
|
||||||
- [ ] failure
|
|
||||||
- [x] errors: array of unique items of type ` "$ref": "#/definitions/error"`
|
|
||||||
- [ ] meta: `"$ref": "#/definitions/meta"`
|
|
||||||
- [ ] jsonapi: `"$ref": "#/definitions/jsonapi"`
|
|
||||||
|
|
||||||
### Info Document
|
|
||||||
|
|
||||||
- [ ] info
|
|
||||||
- [ ] meta: `"$ref": "#/definitions/meta"`
|
|
||||||
- [ ] links: `"$ref": "#/definitions/links"`
|
|
||||||
- [ ] jsonapi: ` "$ref": "#/definitions/jsonapi"`
|
|
||||||
|
|
||||||
### Definitions
|
|
||||||
|
|
||||||
- [ ] definitions:
|
|
||||||
- [ ] meta
|
|
||||||
- [ ] data: oneOf (resource, array of unique resources)
|
|
||||||
- [ ] resource
|
|
||||||
- [ ] attributes
|
|
||||||
- [ ] relationships
|
|
||||||
- [ ] relationshipToOne
|
|
||||||
- [ ] empty
|
|
||||||
- [ ] linkage
|
|
||||||
- [ ] meta
|
|
||||||
- [ ] relationshipToMany
|
|
||||||
- [ ] linkage
|
|
||||||
- [ ] meta
|
|
||||||
- [ ] links
|
|
||||||
- [ ] meta
|
|
||||||
- [ ] links
|
|
||||||
- [ ] link
|
|
||||||
- [ ] uri
|
|
||||||
- [ ] href, meta
|
|
||||||
- [ ] pagination
|
|
||||||
- [ ] jsonapi
|
|
||||||
- [ ] meta
|
|
||||||
- [ ] error
|
|
||||||
- [ ] id: a unique identifier for this particular occurrence of the problem.
|
|
||||||
- [ ] links: a links object containing the following members:
|
|
||||||
- [ ] about: a link that leads to further details about this particular occurrence of the problem.
|
|
||||||
- [ ] status: the HTTP status code applicable to this problem, expressed as a string value.
|
|
||||||
- [ ] code: an application-specific error code, expressed as a string value.
|
|
||||||
- [ ] title: a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization.
|
|
||||||
- [x] detail: a human-readable explanation specific to this occurrence of the problem.
|
|
||||||
- [x] source: an object containing references to the source of the error, optionally including any of the following members:
|
|
||||||
- [x] pointer: a JSON Pointer [RFC6901](https://tools.ietf.org/html/rfc6901) to the associated entity in the request document [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute].
|
|
||||||
- [x] parameter: a string indicating which query parameter caused the error.
|
|
||||||
- [ ] meta: a meta object containing non-standard meta-information about the error.
|
|
||||||
@ -1,366 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
|
||||||
"title": "JSON API Schema",
|
|
||||||
"description": "This is a schema for responses in the JSON API format. For more, see http://jsonapi.org",
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/success"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/failure"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/info"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
"definitions": {
|
|
||||||
"success": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"data"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"data": {
|
|
||||||
"$ref": "#/definitions/data"
|
|
||||||
},
|
|
||||||
"included": {
|
|
||||||
"description": "To reduce the number of HTTP requests, servers **MAY** allow responses that include related resources along with the requested primary resources. Such responses are called \"compound documents\".",
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/resource"
|
|
||||||
},
|
|
||||||
"uniqueItems": true
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"$ref": "#/definitions/meta"
|
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"description": "Link members related to the primary data.",
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/links"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/pagination"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"jsonapi": {
|
|
||||||
"$ref": "#/definitions/jsonapi"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"failure": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"errors"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"errors": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/error"
|
|
||||||
},
|
|
||||||
"uniqueItems": true
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"$ref": "#/definitions/meta"
|
|
||||||
},
|
|
||||||
"jsonapi": {
|
|
||||||
"$ref": "#/definitions/jsonapi"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"info": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"meta"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"meta": {
|
|
||||||
"$ref": "#/definitions/meta"
|
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"$ref": "#/definitions/links"
|
|
||||||
},
|
|
||||||
"jsonapi": {
|
|
||||||
"$ref": "#/definitions/jsonapi"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
|
|
||||||
"meta": {
|
|
||||||
"description": "Non-standard meta-information that can not be represented as an attribute or relationship.",
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": true
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"description": "The document's \"primary data\" is a representation of the resource or collection of resources targeted by a request.",
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/resource"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "An array of resource objects, an array of resource identifier objects, or an empty array ([]), for requests that target resource collections.",
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/resource"
|
|
||||||
},
|
|
||||||
"uniqueItems": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"resource": {
|
|
||||||
"description": "\"Resource objects\" appear in a JSON API document to represent resources.",
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"type",
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"attributes": {
|
|
||||||
"$ref": "#/definitions/attributes"
|
|
||||||
},
|
|
||||||
"relationships": {
|
|
||||||
"$ref": "#/definitions/relationships"
|
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"$ref": "#/definitions/links"
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"$ref": "#/definitions/meta"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
|
|
||||||
"links": {
|
|
||||||
"description": "A resource object **MAY** contain references to other resource objects (\"relationships\"). Relationships may be to-one or to-many. Relationships can be specified by including a member in a resource's links object.",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"self": {
|
|
||||||
"description": "A `self` member, whose value is a URL for the relationship itself (a \"relationship URL\"). This URL allows the client to directly manipulate the relationship. For example, it would allow a client to remove an `author` from an `article` without deleting the people resource itself.",
|
|
||||||
"type": "string",
|
|
||||||
"format": "uri"
|
|
||||||
},
|
|
||||||
"related": {
|
|
||||||
"$ref": "#/definitions/link"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": true
|
|
||||||
},
|
|
||||||
"link": {
|
|
||||||
"description": "A link **MUST** be represented as either: a string containing the link's URL or a link object.",
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"description": "A string containing the link's URL.",
|
|
||||||
"type": "string",
|
|
||||||
"format": "uri"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"href"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"href": {
|
|
||||||
"description": "A string containing the link's URL.",
|
|
||||||
"type": "string",
|
|
||||||
"format": "uri"
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"$ref": "#/definitions/meta"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
"attributes": {
|
|
||||||
"description": "Members of the attributes object (\"attributes\") represent information about the resource object in which it's defined.",
|
|
||||||
"type": "object",
|
|
||||||
"patternProperties": {
|
|
||||||
"^(?!relationships$|links$)\\w[-\\w_]*$": {
|
|
||||||
"description": "Attributes may contain any valid JSON value."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
|
|
||||||
"relationships": {
|
|
||||||
"description": "Members of the relationships object (\"relationships\") represent references from the resource object in which it's defined to other resource objects.",
|
|
||||||
"type": "object",
|
|
||||||
"patternProperties": {
|
|
||||||
"^\\w[-\\w_]*$": {
|
|
||||||
"properties": {
|
|
||||||
"links": {
|
|
||||||
"$ref": "#/definitions/links"
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"description": "Member, whose value represents \"resource linkage\".",
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/relationshipToOne"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/relationshipToMany"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"$ref": "#/definitions/meta"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"relationshipToOne": {
|
|
||||||
"description": "References to other resource objects in a to-one (\"relationship\"). Relationships can be specified by including a member in a resource's links object.",
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/empty"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/linkage"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"relationshipToMany": {
|
|
||||||
"description": "An array of objects each containing \"type\" and \"id\" members for to-many relationships.",
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/linkage"
|
|
||||||
},
|
|
||||||
"uniqueItems": true
|
|
||||||
},
|
|
||||||
"empty": {
|
|
||||||
"description": "Describes an empty to-one relationship.",
|
|
||||||
"type": "null"
|
|
||||||
},
|
|
||||||
"linkage": {
|
|
||||||
"description": "The \"type\" and \"id\" to non-empty members.",
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"type",
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"$ref": "#/definitions/meta"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"pagination": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"first": {
|
|
||||||
"description": "The first page of data",
|
|
||||||
"oneOf": [
|
|
||||||
{ "type": "string", "format": "uri" },
|
|
||||||
{ "type": "null" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"last": {
|
|
||||||
"description": "The last page of data",
|
|
||||||
"oneOf": [
|
|
||||||
{ "type": "string", "format": "uri" },
|
|
||||||
{ "type": "null" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"prev": {
|
|
||||||
"description": "The previous page of data",
|
|
||||||
"oneOf": [
|
|
||||||
{ "type": "string", "format": "uri" },
|
|
||||||
{ "type": "null" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"next": {
|
|
||||||
"description": "The next page of data",
|
|
||||||
"oneOf": [
|
|
||||||
{ "type": "string", "format": "uri" },
|
|
||||||
{ "type": "null" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"jsonapi": {
|
|
||||||
"description": "An object describing the server's implementation",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"version": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"$ref": "#/definitions/meta"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
|
|
||||||
"error": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"description": "A unique identifier for this particular occurrence of the problem.",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"$ref": "#/definitions/links"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"description": "The HTTP status code applicable to this problem, expressed as a string value.",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"code": {
|
|
||||||
"description": "An application-specific error code, expressed as a string value.",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization.",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"detail": {
|
|
||||||
"description": "A human-readable explanation specific to this occurrence of the problem.",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"source": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"pointer": {
|
|
||||||
"description": "A JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute].",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"parameter": {
|
|
||||||
"description": "A string indicating which query parameter caused the error.",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"$ref": "#/definitions/meta"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
- Start Date: (2015-10-29)
|
|
||||||
- RFC PR: https://github.com/rails-api/active_model_serializers/pull/1310
|
|
||||||
- ActiveModelSerializers Issue: https://github.com/rails-api/active_model_serializers/issues/1298
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
|
|
||||||
Provide a consistent API for the user of the AMS.
|
|
||||||
|
|
||||||
# Motivation
|
|
||||||
|
|
||||||
The actual public API is defined under `ActiveModelSerializers`,
|
|
||||||
`ActiveModel::Serializer` and `ActiveModel`.
|
|
||||||
|
|
||||||
At the `ActiveModel::Serializer` we have:
|
|
||||||
|
|
||||||
- `ActiveModel::Serializer.config`
|
|
||||||
- `ActiveModel::Serializer`
|
|
||||||
|
|
||||||
At the `ActiveModelSerializers` we have:
|
|
||||||
|
|
||||||
- `ActiveModelSerializers::Model`
|
|
||||||
- `ActiveModelSerializers.logger`
|
|
||||||
|
|
||||||
At `ActiveModel` we have:
|
|
||||||
|
|
||||||
- `ActiveModel::SerializableResource`
|
|
||||||
|
|
||||||
The idea here is to provide a single namespace `ActiveModelSerializers` to the user.
|
|
||||||
Following the same idea we have on other gems like
|
|
||||||
[Devise](https://github.com/plataformatec/devise/blob/e9c82472ffe7c43a448945f77e034a0e47dde0bb/lib/devise.rb),
|
|
||||||
[Refile](https://github.com/refile/refile/blob/6b24c293d044862dafbf1bfa4606672a64903aa2/lib/refile.rb) and
|
|
||||||
[Active Job](https://github.com/rails/rails/blob/30bacc26f8f258b39e12f63fe52389a968d9c1ea/activejob/lib/active_job.rb)
|
|
||||||
for example.
|
|
||||||
|
|
||||||
This way we are clarifing the boundaries of
|
|
||||||
[ActiveModelSerializers and Rails](https://github.com/rails-api/active_model_serializers/blob/master/CHANGELOG.md#prehistory)
|
|
||||||
and make clear that the `ActiveModel::Serializer` class is no longer the primary
|
|
||||||
behavior of the ActiveModelSerializers.
|
|
||||||
|
|
||||||
# Detailed design
|
|
||||||
|
|
||||||
## New classes and modules organization
|
|
||||||
|
|
||||||
Since this will be a big change we can do this on baby steps, read small pull requests. A
|
|
||||||
possible approach is:
|
|
||||||
|
|
||||||
- All new code will be in `lib/active_model_serializers/` using
|
|
||||||
the module namespace `ActiveModelSerializers`.
|
|
||||||
- Move all content under `ActiveModel::Serializer` to be under
|
|
||||||
`ActiveModelSerializers`, the adapter is on this steps;
|
|
||||||
- Move all content under `ActiveModel` to be under `ActiveModelSerializers`,
|
|
||||||
the `SerializableResource` is on this step;
|
|
||||||
- Change all public API that doesn't make sense, keeping in mind only to keep
|
|
||||||
this in the same namespace
|
|
||||||
- Update the README;
|
|
||||||
- Update the docs;
|
|
||||||
|
|
||||||
The following table represents the current and the desired classes and modules
|
|
||||||
at the first moment.
|
|
||||||
|
|
||||||
| Current | Desired | Notes |
|
|
||||||
|--------------------------------------------------------|--------------------------------------------------|--------------------|
|
|
||||||
| `ActiveModelSerializers` and `ActiveModel::Serializer` | `ActiveModelSerializers` | The main namespace |
|
|
||||||
| `ActiveModelSerializers.logger` | `ActiveModelSerializers.logger` ||
|
|
||||||
| `ActiveModelSerializers::Model` | `ActiveModelSerializers::Model` ||
|
|
||||||
| `ActiveModel::SerializableResource` | `ActiveModelSerializers::SerializableResource` ||
|
|
||||||
| `ActiveModel::Serializer` | `ActiveModelSerializers::Serializer` | The name can be discussed in a future pull request. For example, we can rename this to `Resource` [following this idea](https://github.com/rails-api/active_model_serializers/pull/1301/files#r42963185) more info about naming in the next section|
|
|
||||||
| `ActiveModel::Serializer.config` | `ActiveModelSerializers.config` ||
|
|
||||||
|
|
||||||
## Renaming of class and modules
|
|
||||||
|
|
||||||
When moving some content to the new namespace we can find some names that does
|
|
||||||
not make much sense like `ActiveModel::Serializer::Adapter::JsonApi`.
|
|
||||||
Discussion of renaming existing classes / modules and JsonApi objects will
|
|
||||||
happen in separate pull requests, and issues, and in the google doc
|
|
||||||
https://docs.google.com/document/d/1rcrJr0sVcazY2Opd_6Kmv1iIwuHbI84s1P_NzFn-05c/edit?usp=sharing
|
|
||||||
|
|
||||||
Some of names already have a definition.
|
|
||||||
|
|
||||||
- Adapters get their own namespace under ActiveModelSerializers. E.g
|
|
||||||
`ActiveModelSerializers::Adapter`
|
|
||||||
- Serializers get their own namespace under ActiveModelSerializers. E.g
|
|
||||||
`ActiveModelSerializers::Serializer`
|
|
||||||
|
|
||||||
## Keeping compatibility
|
|
||||||
|
|
||||||
All moved classes or modules be aliased to their old name and location with
|
|
||||||
deprecation warnings, such as
|
|
||||||
[was done for CollectionSerializer](https://github.com/rails-api/active_model_serializers/pull/1251).
|
|
||||||
|
|
||||||
# Drawbacks
|
|
||||||
|
|
||||||
This will be a breaking change, so all users serializers will be broken after a
|
|
||||||
major bump.
|
|
||||||
All pull requests will need to rebase since the architeture will change a lot.
|
|
||||||
|
|
||||||
# Alternatives
|
|
||||||
|
|
||||||
We can keep the way it is, and keep in mind to not add another namespace as a
|
|
||||||
public API.
|
|
||||||
|
|
||||||
# Unresolved questions
|
|
||||||
|
|
||||||
What is the better class name to be used to the class that will be inherited at
|
|
||||||
the creation of a serializer. This can be discussed in other RFC or directly via
|
|
||||||
pull request.
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
- Start Date: (YYYY-MM-DD)
|
|
||||||
- RFC PR: https://github.com/rails-api/active_model_serializers/pull/dddd
|
|
||||||
- ActiveModelSerializers Issue: https://github.com/rails-api/active_model_serializers/issues/dddd
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
|
|
||||||
# Motivation
|
|
||||||
|
|
||||||
# Detailed design
|
|
||||||
|
|
||||||
# Drawbacks
|
|
||||||
|
|
||||||
# Alternatives
|
|
||||||
|
|
||||||
# Unresolved questions
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
require 'active_support/core_ext/class/attribute'
|
|
||||||
require 'active_model_serializers/serialization_context'
|
|
||||||
|
|
||||||
module ActionController
|
|
||||||
module Serialization
|
|
||||||
extend ActiveSupport::Concern
|
|
||||||
|
|
||||||
include ActionController::Renderers
|
|
||||||
|
|
||||||
module ClassMethods
|
|
||||||
def serialization_scope(scope)
|
|
||||||
self._serialization_scope = scope
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
included do
|
|
||||||
class_attribute :_serialization_scope
|
|
||||||
self._serialization_scope = :current_user
|
|
||||||
|
|
||||||
attr_writer :namespace_for_serializer
|
|
||||||
end
|
|
||||||
|
|
||||||
def namespace_for_serializer
|
|
||||||
@namespace_for_serializer ||= self.class.parent unless self.class.parent == Object
|
|
||||||
end
|
|
||||||
|
|
||||||
def serialization_scope
|
|
||||||
return unless _serialization_scope && respond_to?(_serialization_scope, true)
|
|
||||||
|
|
||||||
send(_serialization_scope)
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_serializer(resource, options = {})
|
|
||||||
unless use_adapter?
|
|
||||||
warn 'ActionController::Serialization#use_adapter? has been removed. '\
|
|
||||||
"Please pass 'adapter: false' or see ActiveSupport::SerializableResource.new"
|
|
||||||
options[:adapter] = false
|
|
||||||
end
|
|
||||||
|
|
||||||
options.fetch(:namespace) { options[:namespace] = namespace_for_serializer }
|
|
||||||
|
|
||||||
serializable_resource = ActiveModelSerializers::SerializableResource.new(resource, options)
|
|
||||||
serializable_resource.serialization_scope ||= options.fetch(:scope) { serialization_scope }
|
|
||||||
serializable_resource.serialization_scope_name = options.fetch(:scope_name) { _serialization_scope }
|
|
||||||
# For compatibility with the JSON renderer: `json.to_json(options) if json.is_a?(String)`.
|
|
||||||
# Otherwise, since `serializable_resource` is not a string, the renderer would call
|
|
||||||
# `to_json` on a String and given odd results, such as `"".to_json #=> '""'`
|
|
||||||
serializable_resource.adapter.is_a?(String) ? serializable_resource.adapter : serializable_resource
|
|
||||||
end
|
|
||||||
|
|
||||||
# Deprecated
|
|
||||||
def use_adapter?
|
|
||||||
true
|
|
||||||
end
|
|
||||||
|
|
||||||
[:_render_option_json, :_render_with_renderer_json].each do |renderer_method|
|
|
||||||
define_method renderer_method do |resource, options|
|
|
||||||
options.fetch(:serialization_context) do
|
|
||||||
options[:serialization_context] = ActiveModelSerializers::SerializationContext.new(request, options)
|
|
||||||
end
|
|
||||||
serializable_resource = get_serializer(resource, options)
|
|
||||||
super(serializable_resource, options)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
require 'set'
|
|
||||||
|
|
||||||
module ActiveModel
|
|
||||||
class SerializableResource
|
|
||||||
class << self
|
|
||||||
extend ActiveModelSerializers::Deprecate
|
|
||||||
|
|
||||||
delegate_and_deprecate :new, ActiveModelSerializers::SerializableResource
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,448 +0,0 @@
|
|||||||
require 'thread_safe'
|
|
||||||
require 'jsonapi/include_directive'
|
|
||||||
require 'active_model/serializer/collection_serializer'
|
|
||||||
require 'active_model/serializer/array_serializer'
|
|
||||||
require 'active_model/serializer/error_serializer'
|
|
||||||
require 'active_model/serializer/errors_serializer'
|
|
||||||
require 'active_model/serializer/concerns/caching'
|
|
||||||
require 'active_model/serializer/fieldset'
|
|
||||||
require 'active_model/serializer/lint'
|
|
||||||
|
|
||||||
# ActiveModel::Serializer is an abstract class that is
|
|
||||||
# reified when subclassed to decorate a resource.
|
|
||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
# @see #serializable_hash for more details on these valid keys.
|
|
||||||
SERIALIZABLE_HASH_VALID_KEYS = [:only, :except, :methods, :include, :root].freeze
|
|
||||||
extend ActiveSupport::Autoload
|
|
||||||
autoload :Adapter
|
|
||||||
autoload :Null
|
|
||||||
autoload :Attribute
|
|
||||||
autoload :Association
|
|
||||||
autoload :Reflection
|
|
||||||
autoload :SingularReflection
|
|
||||||
autoload :CollectionReflection
|
|
||||||
autoload :BelongsToReflection
|
|
||||||
autoload :HasOneReflection
|
|
||||||
autoload :HasManyReflection
|
|
||||||
include ActiveSupport::Configurable
|
|
||||||
include Caching
|
|
||||||
|
|
||||||
# @param resource [ActiveRecord::Base, ActiveModelSerializers::Model]
|
|
||||||
# @return [ActiveModel::Serializer]
|
|
||||||
# Preferentially returns
|
|
||||||
# 1. resource.serializer_class
|
|
||||||
# 2. ArraySerializer when resource is a collection
|
|
||||||
# 3. options[:serializer]
|
|
||||||
# 4. lookup serializer when resource is a Class
|
|
||||||
def self.serializer_for(resource_or_class, options = {})
|
|
||||||
if resource_or_class.respond_to?(:serializer_class)
|
|
||||||
resource_or_class.serializer_class
|
|
||||||
elsif resource_or_class.respond_to?(:to_ary)
|
|
||||||
config.collection_serializer
|
|
||||||
else
|
|
||||||
resource_class = resource_or_class.class == Class ? resource_or_class : resource_or_class.class
|
|
||||||
options.fetch(:serializer) { get_serializer_for(resource_class, options[:namespace]) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# @see ActiveModelSerializers::Adapter.lookup
|
|
||||||
# Deprecated
|
|
||||||
def self.adapter
|
|
||||||
ActiveModelSerializers::Adapter.lookup(config.adapter)
|
|
||||||
end
|
|
||||||
class << self
|
|
||||||
extend ActiveModelSerializers::Deprecate
|
|
||||||
deprecate :adapter, 'ActiveModelSerializers::Adapter.configured_adapter'
|
|
||||||
end
|
|
||||||
|
|
||||||
# @api private
|
|
||||||
def self.serializer_lookup_chain_for(klass, namespace = nil)
|
|
||||||
lookups = ActiveModelSerializers.config.serializer_lookup_chain
|
|
||||||
Array[*lookups].flat_map do |lookup|
|
|
||||||
lookup.call(klass, self, namespace)
|
|
||||||
end.compact
|
|
||||||
end
|
|
||||||
|
|
||||||
# Used to cache serializer name => serializer class
|
|
||||||
# when looked up by Serializer.get_serializer_for.
|
|
||||||
def self.serializers_cache
|
|
||||||
@serializers_cache ||= ThreadSafe::Cache.new
|
|
||||||
end
|
|
||||||
|
|
||||||
# @api private
|
|
||||||
# Find a serializer from a class and caches the lookup.
|
|
||||||
# Preferentially returns:
|
|
||||||
# 1. class name appended with "Serializer"
|
|
||||||
# 2. try again with superclass, if present
|
|
||||||
# 3. nil
|
|
||||||
def self.get_serializer_for(klass, namespace = nil)
|
|
||||||
return nil unless config.serializer_lookup_enabled
|
|
||||||
|
|
||||||
cache_key = ActiveSupport::Cache.expand_cache_key(klass, namespace)
|
|
||||||
serializers_cache.fetch_or_store(cache_key) do
|
|
||||||
# NOTE(beauby): When we drop 1.9.3 support we can lazify the map for perfs.
|
|
||||||
lookup_chain = serializer_lookup_chain_for(klass, namespace)
|
|
||||||
serializer_class = lookup_chain.map(&:safe_constantize).find { |x| x && x < ActiveModel::Serializer }
|
|
||||||
|
|
||||||
if serializer_class
|
|
||||||
serializer_class
|
|
||||||
elsif klass.superclass
|
|
||||||
get_serializer_for(klass.superclass)
|
|
||||||
else
|
|
||||||
nil # No serializer found
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# @api private
|
|
||||||
def self.include_directive_from_options(options)
|
|
||||||
if options[:include_directive]
|
|
||||||
options[:include_directive]
|
|
||||||
elsif options[:include]
|
|
||||||
JSONAPI::IncludeDirective.new(options[:include], allow_wildcard: true)
|
|
||||||
else
|
|
||||||
ActiveModelSerializers.default_include_directive
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# @api private
|
|
||||||
def self.serialization_adapter_instance
|
|
||||||
@serialization_adapter_instance ||= ActiveModelSerializers::Adapter::Attributes
|
|
||||||
end
|
|
||||||
|
|
||||||
# Preferred interface is ActiveModelSerializers.config
|
|
||||||
# BEGIN DEFAULT CONFIGURATION
|
|
||||||
config.collection_serializer = ActiveModel::Serializer::CollectionSerializer
|
|
||||||
config.serializer_lookup_enabled = true
|
|
||||||
|
|
||||||
# @deprecated Use {#config.collection_serializer=} instead of this. Is
|
|
||||||
# compatibilty layer for ArraySerializer.
|
|
||||||
def config.array_serializer=(collection_serializer)
|
|
||||||
self.collection_serializer = collection_serializer
|
|
||||||
end
|
|
||||||
|
|
||||||
# @deprecated Use {#config.collection_serializer} instead of this. Is
|
|
||||||
# compatibilty layer for ArraySerializer.
|
|
||||||
def config.array_serializer
|
|
||||||
collection_serializer
|
|
||||||
end
|
|
||||||
|
|
||||||
config.default_includes = '*'
|
|
||||||
config.adapter = :attributes
|
|
||||||
config.key_transform = nil
|
|
||||||
config.jsonapi_pagination_links_enabled = true
|
|
||||||
config.jsonapi_resource_type = :plural
|
|
||||||
config.jsonapi_namespace_separator = '-'.freeze
|
|
||||||
config.jsonapi_version = '1.0'
|
|
||||||
config.jsonapi_toplevel_meta = {}
|
|
||||||
# Make JSON API top-level jsonapi member opt-in
|
|
||||||
# ref: http://jsonapi.org/format/#document-top-level
|
|
||||||
config.jsonapi_include_toplevel_object = false
|
|
||||||
config.include_data_default = true
|
|
||||||
|
|
||||||
# For configuring how serializers are found.
|
|
||||||
# This should be an array of procs.
|
|
||||||
#
|
|
||||||
# The priority of the output is that the first item
|
|
||||||
# in the evaluated result array will take precedence
|
|
||||||
# over other possible serializer paths.
|
|
||||||
#
|
|
||||||
# i.e.: First match wins.
|
|
||||||
#
|
|
||||||
# @example output
|
|
||||||
# => [
|
|
||||||
# "CustomNamespace::ResourceSerializer",
|
|
||||||
# "ParentSerializer::ResourceSerializer",
|
|
||||||
# "ResourceNamespace::ResourceSerializer" ,
|
|
||||||
# "ResourceSerializer"]
|
|
||||||
#
|
|
||||||
# If CustomNamespace::ResourceSerializer exists, it will be used
|
|
||||||
# for serialization
|
|
||||||
config.serializer_lookup_chain = ActiveModelSerializers::LookupChain::DEFAULT.dup
|
|
||||||
|
|
||||||
config.schema_path = 'test/support/schemas'
|
|
||||||
# END DEFAULT CONFIGURATION
|
|
||||||
|
|
||||||
with_options instance_writer: false, instance_reader: false do |serializer|
|
|
||||||
serializer.class_attribute :_attributes_data # @api private
|
|
||||||
self._attributes_data ||= {}
|
|
||||||
end
|
|
||||||
with_options instance_writer: false, instance_reader: true do |serializer|
|
|
||||||
serializer.class_attribute :_reflections
|
|
||||||
self._reflections ||= {}
|
|
||||||
serializer.class_attribute :_links # @api private
|
|
||||||
self._links ||= {}
|
|
||||||
serializer.class_attribute :_meta # @api private
|
|
||||||
serializer.class_attribute :_type # @api private
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.inherited(base)
|
|
||||||
super
|
|
||||||
base._attributes_data = _attributes_data.dup
|
|
||||||
base._reflections = _reflections.dup
|
|
||||||
base._links = _links.dup
|
|
||||||
end
|
|
||||||
|
|
||||||
# @return [Array<Symbol>] Key names of declared attributes
|
|
||||||
# @see Serializer::attribute
|
|
||||||
def self._attributes
|
|
||||||
_attributes_data.keys
|
|
||||||
end
|
|
||||||
|
|
||||||
# BEGIN SERIALIZER MACROS
|
|
||||||
|
|
||||||
# @example
|
|
||||||
# class AdminAuthorSerializer < ActiveModel::Serializer
|
|
||||||
# attributes :id, :name, :recent_edits
|
|
||||||
def self.attributes(*attrs)
|
|
||||||
attrs = attrs.first if attrs.first.class == Array
|
|
||||||
|
|
||||||
attrs.each do |attr|
|
|
||||||
attribute(attr)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# @example
|
|
||||||
# class AdminAuthorSerializer < ActiveModel::Serializer
|
|
||||||
# attributes :id, :recent_edits
|
|
||||||
# attribute :name, key: :title
|
|
||||||
#
|
|
||||||
# attribute :full_name do
|
|
||||||
# "#{object.first_name} #{object.last_name}"
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# def recent_edits
|
|
||||||
# object.edits.last(5)
|
|
||||||
# end
|
|
||||||
def self.attribute(attr, options = {}, &block)
|
|
||||||
key = options.fetch(:key, attr)
|
|
||||||
_attributes_data[key] = Attribute.new(attr, options, block)
|
|
||||||
end
|
|
||||||
|
|
||||||
# @param [Symbol] name of the association
|
|
||||||
# @param [Hash<Symbol => any>] options for the reflection
|
|
||||||
# @return [void]
|
|
||||||
#
|
|
||||||
# @example
|
|
||||||
# has_many :comments, serializer: CommentSummarySerializer
|
|
||||||
#
|
|
||||||
def self.has_many(name, options = {}, &block) # rubocop:disable Style/PredicateName
|
|
||||||
associate(HasManyReflection.new(name, options, block))
|
|
||||||
end
|
|
||||||
|
|
||||||
# @param [Symbol] name of the association
|
|
||||||
# @param [Hash<Symbol => any>] options for the reflection
|
|
||||||
# @return [void]
|
|
||||||
#
|
|
||||||
# @example
|
|
||||||
# belongs_to :author, serializer: AuthorSerializer
|
|
||||||
#
|
|
||||||
def self.belongs_to(name, options = {}, &block)
|
|
||||||
associate(BelongsToReflection.new(name, options, block))
|
|
||||||
end
|
|
||||||
|
|
||||||
# @param [Symbol] name of the association
|
|
||||||
# @param [Hash<Symbol => any>] options for the reflection
|
|
||||||
# @return [void]
|
|
||||||
#
|
|
||||||
# @example
|
|
||||||
# has_one :author, serializer: AuthorSerializer
|
|
||||||
#
|
|
||||||
def self.has_one(name, options = {}, &block) # rubocop:disable Style/PredicateName
|
|
||||||
associate(HasOneReflection.new(name, options, block))
|
|
||||||
end
|
|
||||||
|
|
||||||
# Add reflection and define {name} accessor.
|
|
||||||
# @param [ActiveModel::Serializer::Reflection] reflection
|
|
||||||
# @return [void]
|
|
||||||
#
|
|
||||||
# @api private
|
|
||||||
def self.associate(reflection)
|
|
||||||
key = reflection.options[:key] || reflection.name
|
|
||||||
self._reflections[key] = reflection
|
|
||||||
end
|
|
||||||
private_class_method :associate
|
|
||||||
|
|
||||||
# Define a link on a serializer.
|
|
||||||
# @example
|
|
||||||
# link(:self) { resource_url(object) }
|
|
||||||
# @example
|
|
||||||
# link(:self) { "http://example.com/resource/#{object.id}" }
|
|
||||||
# @example
|
|
||||||
# link :resource, "http://example.com/resource"
|
|
||||||
#
|
|
||||||
def self.link(name, value = nil, &block)
|
|
||||||
_links[name] = block || value
|
|
||||||
end
|
|
||||||
|
|
||||||
# Set the JSON API meta attribute of a serializer.
|
|
||||||
# @example
|
|
||||||
# class AdminAuthorSerializer < ActiveModel::Serializer
|
|
||||||
# meta { stuff: 'value' }
|
|
||||||
# @example
|
|
||||||
# meta do
|
|
||||||
# { comment_count: object.comments.count }
|
|
||||||
# end
|
|
||||||
def self.meta(value = nil, &block)
|
|
||||||
self._meta = block || value
|
|
||||||
end
|
|
||||||
|
|
||||||
# Set the JSON API type of a serializer.
|
|
||||||
# @example
|
|
||||||
# class AdminAuthorSerializer < ActiveModel::Serializer
|
|
||||||
# type 'authors'
|
|
||||||
def self.type(type)
|
|
||||||
self._type = type && type.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
# END SERIALIZER MACROS
|
|
||||||
|
|
||||||
attr_accessor :object, :root, :scope
|
|
||||||
|
|
||||||
# `scope_name` is set as :current_user by default in the controller.
|
|
||||||
# If the instance does not have a method named `scope_name`, it
|
|
||||||
# defines the method so that it calls the +scope+.
|
|
||||||
def initialize(object, options = {})
|
|
||||||
self.object = object
|
|
||||||
self.instance_options = options
|
|
||||||
self.root = instance_options[:root]
|
|
||||||
self.scope = instance_options[:scope]
|
|
||||||
|
|
||||||
return if !(scope_name = instance_options[:scope_name]) || respond_to?(scope_name)
|
|
||||||
|
|
||||||
define_singleton_method scope_name, -> { scope }
|
|
||||||
end
|
|
||||||
|
|
||||||
def success?
|
|
||||||
true
|
|
||||||
end
|
|
||||||
|
|
||||||
# Return the +attributes+ of +object+ as presented
|
|
||||||
# by the serializer.
|
|
||||||
def attributes(requested_attrs = nil, reload = false)
|
|
||||||
@attributes = nil if reload
|
|
||||||
@attributes ||= self.class._attributes_data.each_with_object({}) do |(key, attr), hash|
|
|
||||||
next if attr.excluded?(self)
|
|
||||||
next unless requested_attrs.nil? || requested_attrs.include?(key)
|
|
||||||
hash[key] = attr.value(self)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# @param [JSONAPI::IncludeDirective] include_directive (defaults to the
|
|
||||||
# +default_include_directive+ config value when not provided)
|
|
||||||
# @return [Enumerator<Association>]
|
|
||||||
#
|
|
||||||
def associations(include_directive = ActiveModelSerializers.default_include_directive, include_slice = nil)
|
|
||||||
include_slice ||= include_directive
|
|
||||||
return unless object
|
|
||||||
|
|
||||||
Enumerator.new do |y|
|
|
||||||
self.class._reflections.values.each do |reflection|
|
|
||||||
next if reflection.excluded?(self)
|
|
||||||
key = reflection.options.fetch(:key, reflection.name)
|
|
||||||
next unless include_directive.key?(key)
|
|
||||||
|
|
||||||
y.yield reflection.build_association(self, instance_options, include_slice)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# @return [Hash] containing the attributes and first level
|
|
||||||
# associations, similar to how ActiveModel::Serializers::JSON is used
|
|
||||||
# in ActiveRecord::Base.
|
|
||||||
#
|
|
||||||
# TODO: Include <tt>ActiveModel::Serializers::JSON</tt>.
|
|
||||||
# So that the below is true:
|
|
||||||
# @param options [nil, Hash] The same valid options passed to `serializable_hash`
|
|
||||||
# (:only, :except, :methods, and :include).
|
|
||||||
#
|
|
||||||
# See
|
|
||||||
# https://github.com/rails/rails/blob/v5.0.0.beta2/activemodel/lib/active_model/serializers/json.rb#L17-L101
|
|
||||||
# https://github.com/rails/rails/blob/v5.0.0.beta2/activemodel/lib/active_model/serialization.rb#L85-L123
|
|
||||||
# https://github.com/rails/rails/blob/v5.0.0.beta2/activerecord/lib/active_record/serialization.rb#L11-L17
|
|
||||||
# https://github.com/rails/rails/blob/v5.0.0.beta2/activesupport/lib/active_support/core_ext/object/json.rb#L147-L162
|
|
||||||
#
|
|
||||||
# @example
|
|
||||||
# # The :only and :except options can be used to limit the attributes included, and work
|
|
||||||
# # similar to the attributes method.
|
|
||||||
# serializer.as_json(only: [:id, :name])
|
|
||||||
# serializer.as_json(except: [:id, :created_at, :age])
|
|
||||||
#
|
|
||||||
# # To include the result of some method calls on the model use :methods:
|
|
||||||
# serializer.as_json(methods: :permalink)
|
|
||||||
#
|
|
||||||
# # To include associations use :include:
|
|
||||||
# serializer.as_json(include: :posts)
|
|
||||||
# # Second level and higher order associations work as well:
|
|
||||||
# serializer.as_json(include: { posts: { include: { comments: { only: :body } }, only: :title } })
|
|
||||||
def serializable_hash(adapter_options = nil, options = {}, adapter_instance = self.class.serialization_adapter_instance)
|
|
||||||
adapter_options ||= {}
|
|
||||||
options[:include_directive] ||= ActiveModel::Serializer.include_directive_from_options(adapter_options)
|
|
||||||
cached_attributes = adapter_options[:cached_attributes] ||= {}
|
|
||||||
resource = fetch_attributes(options[:fields], cached_attributes, adapter_instance)
|
|
||||||
relationships = resource_relationships(adapter_options, options, adapter_instance)
|
|
||||||
resource.merge(relationships)
|
|
||||||
end
|
|
||||||
alias to_hash serializable_hash
|
|
||||||
alias to_h serializable_hash
|
|
||||||
|
|
||||||
# @see #serializable_hash
|
|
||||||
# TODO: When moving attributes adapter logic here, @see #serializable_hash
|
|
||||||
# So that the below is true:
|
|
||||||
# @param options [nil, Hash] The same valid options passed to `as_json`
|
|
||||||
# (:root, :only, :except, :methods, and :include).
|
|
||||||
# The default for `root` is nil.
|
|
||||||
# The default value for include_root is false. You can change it to true if the given
|
|
||||||
# JSON string includes a single root node.
|
|
||||||
def as_json(adapter_opts = nil)
|
|
||||||
serializable_hash(adapter_opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Used by adapter as resource root.
|
|
||||||
def json_key
|
|
||||||
root || _type || object.class.model_name.to_s.underscore
|
|
||||||
end
|
|
||||||
|
|
||||||
def read_attribute_for_serialization(attr)
|
|
||||||
if respond_to?(attr)
|
|
||||||
send(attr)
|
|
||||||
else
|
|
||||||
object.read_attribute_for_serialization(attr)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# @api private
|
|
||||||
def resource_relationships(adapter_options, options, adapter_instance)
|
|
||||||
relationships = {}
|
|
||||||
include_directive = options.fetch(:include_directive)
|
|
||||||
associations(include_directive).each do |association|
|
|
||||||
adapter_opts = adapter_options.merge(include_directive: include_directive[association.key])
|
|
||||||
relationships[association.key] ||= relationship_value_for(association, adapter_opts, adapter_instance)
|
|
||||||
end
|
|
||||||
|
|
||||||
relationships
|
|
||||||
end
|
|
||||||
|
|
||||||
# @api private
|
|
||||||
def relationship_value_for(association, adapter_options, adapter_instance)
|
|
||||||
return association.options[:virtual_value] if association.options[:virtual_value]
|
|
||||||
association_serializer = association.serializer
|
|
||||||
association_object = association_serializer && association_serializer.object
|
|
||||||
return unless association_object
|
|
||||||
|
|
||||||
relationship_value = association_serializer.serializable_hash(adapter_options, {}, adapter_instance)
|
|
||||||
|
|
||||||
if association.options[:polymorphic] && relationship_value
|
|
||||||
polymorphic_type = association_object.class.name.underscore
|
|
||||||
relationship_value = { type: polymorphic_type, polymorphic_type.to_sym => relationship_value }
|
|
||||||
end
|
|
||||||
|
|
||||||
relationship_value
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
attr_accessor :instance_options
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
require 'active_model_serializers/adapter'
|
|
||||||
require 'active_model_serializers/deprecate'
|
|
||||||
|
|
||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
# @deprecated Use ActiveModelSerializers::Adapter instead
|
|
||||||
module Adapter
|
|
||||||
class << self
|
|
||||||
extend ActiveModelSerializers::Deprecate
|
|
||||||
|
|
||||||
DEPRECATED_METHODS = [:create, :adapter_class, :adapter_map, :adapters, :register, :lookup].freeze
|
|
||||||
DEPRECATED_METHODS.each do |method|
|
|
||||||
delegate_and_deprecate method, ActiveModelSerializers::Adapter
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
require 'active_model/serializer/adapter/base'
|
|
||||||
require 'active_model/serializer/adapter/null'
|
|
||||||
require 'active_model/serializer/adapter/attributes'
|
|
||||||
require 'active_model/serializer/adapter/json'
|
|
||||||
require 'active_model/serializer/adapter/json_api'
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
module Adapter
|
|
||||||
class Attributes < DelegateClass(ActiveModelSerializers::Adapter::Attributes)
|
|
||||||
def initialize(serializer, options = {})
|
|
||||||
super(ActiveModelSerializers::Adapter::Attributes.new(serializer, options))
|
|
||||||
end
|
|
||||||
class << self
|
|
||||||
extend ActiveModelSerializers::Deprecate
|
|
||||||
deprecate :new, 'ActiveModelSerializers::Adapter::Json.'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
module Adapter
|
|
||||||
class Base < DelegateClass(ActiveModelSerializers::Adapter::Base)
|
|
||||||
class << self
|
|
||||||
extend ActiveModelSerializers::Deprecate
|
|
||||||
deprecate :inherited, 'ActiveModelSerializers::Adapter::Base.'
|
|
||||||
end
|
|
||||||
|
|
||||||
# :nocov:
|
|
||||||
def initialize(serializer, options = {})
|
|
||||||
super(ActiveModelSerializers::Adapter::Base.new(serializer, options))
|
|
||||||
end
|
|
||||||
# :nocov:
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
module Adapter
|
|
||||||
class Json < DelegateClass(ActiveModelSerializers::Adapter::Json)
|
|
||||||
def initialize(serializer, options = {})
|
|
||||||
super(ActiveModelSerializers::Adapter::Json.new(serializer, options))
|
|
||||||
end
|
|
||||||
class << self
|
|
||||||
extend ActiveModelSerializers::Deprecate
|
|
||||||
deprecate :new, 'ActiveModelSerializers::Adapter::Json.new'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
module Adapter
|
|
||||||
class JsonApi < DelegateClass(ActiveModelSerializers::Adapter::JsonApi)
|
|
||||||
def initialize(serializer, options = {})
|
|
||||||
super(ActiveModelSerializers::Adapter::JsonApi.new(serializer, options))
|
|
||||||
end
|
|
||||||
class << self
|
|
||||||
extend ActiveModelSerializers::Deprecate
|
|
||||||
deprecate :new, 'ActiveModelSerializers::Adapter::JsonApi.new'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
module Adapter
|
|
||||||
class Null < DelegateClass(ActiveModelSerializers::Adapter::Null)
|
|
||||||
def initialize(serializer, options = {})
|
|
||||||
super(ActiveModelSerializers::Adapter::Null.new(serializer, options))
|
|
||||||
end
|
|
||||||
class << self
|
|
||||||
extend ActiveModelSerializers::Deprecate
|
|
||||||
deprecate :new, 'ActiveModelSerializers::Adapter::Null.new'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
require 'active_model/serializer/collection_serializer'
|
|
||||||
|
|
||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
class ArraySerializer < CollectionSerializer
|
|
||||||
class << self
|
|
||||||
extend ActiveModelSerializers::Deprecate
|
|
||||||
deprecate :new, 'ActiveModel::Serializer::CollectionSerializer.'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
# This class holds all information about serializer's association.
|
|
||||||
#
|
|
||||||
# @attr [Symbol] name
|
|
||||||
# @attr [Hash{Symbol => Object}] options
|
|
||||||
# @attr [block]
|
|
||||||
#
|
|
||||||
# @example
|
|
||||||
# Association.new(:comments, { serializer: CommentSummarySerializer })
|
|
||||||
#
|
|
||||||
class Association < Field
|
|
||||||
# @return [Symbol]
|
|
||||||
def key
|
|
||||||
options.fetch(:key, name)
|
|
||||||
end
|
|
||||||
|
|
||||||
# @return [ActiveModel::Serializer, nil]
|
|
||||||
def serializer
|
|
||||||
options[:serializer]
|
|
||||||
end
|
|
||||||
|
|
||||||
# @return [Hash]
|
|
||||||
def links
|
|
||||||
options.fetch(:links) || {}
|
|
||||||
end
|
|
||||||
|
|
||||||
# @return [Hash, nil]
|
|
||||||
def meta
|
|
||||||
options[:meta]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
require 'active_model/serializer/field'
|
|
||||||
|
|
||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
# Holds all the meta-data about an attribute as it was specified in the
|
|
||||||
# ActiveModel::Serializer class.
|
|
||||||
#
|
|
||||||
# @example
|
|
||||||
# class PostSerializer < ActiveModel::Serializer
|
|
||||||
# attribute :content
|
|
||||||
# attribute :name, key: :title
|
|
||||||
# attribute :email, key: :author_email, if: :user_logged_in?
|
|
||||||
# attribute :preview do
|
|
||||||
# truncate(object.content)
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# def user_logged_in?
|
|
||||||
# current_user.logged_in?
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
class Attribute < Field
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
# @api private
|
|
||||||
class BelongsToReflection < SingularReflection
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
# @api private
|
|
||||||
class CollectionReflection < Reflection
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
class CollectionSerializer
|
|
||||||
include Enumerable
|
|
||||||
delegate :each, to: :@serializers
|
|
||||||
|
|
||||||
attr_reader :object, :root
|
|
||||||
|
|
||||||
def initialize(resources, options = {})
|
|
||||||
@object = resources
|
|
||||||
@options = options
|
|
||||||
@root = options[:root]
|
|
||||||
@serializers = serializers_from_resources
|
|
||||||
end
|
|
||||||
|
|
||||||
def success?
|
|
||||||
true
|
|
||||||
end
|
|
||||||
|
|
||||||
# @api private
|
|
||||||
def serializable_hash(adapter_options, options, adapter_instance)
|
|
||||||
include_directive = ActiveModel::Serializer.include_directive_from_options(adapter_options)
|
|
||||||
adapter_options[:cached_attributes] ||= ActiveModel::Serializer.cache_read_multi(self, adapter_instance, include_directive)
|
|
||||||
adapter_opts = adapter_options.merge(include_directive: include_directive)
|
|
||||||
serializers.map do |serializer|
|
|
||||||
serializer.serializable_hash(adapter_opts, options, adapter_instance)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: unify naming of root, json_key, and _type. Right now, a serializer's
|
|
||||||
# json_key comes from the root option or the object's model name, by default.
|
|
||||||
# But, if a dev defines a custom `json_key` method with an explicit value,
|
|
||||||
# we have no simple way to know that it is safe to call that instance method.
|
|
||||||
# (which is really a class property at this point, anyhow).
|
|
||||||
# rubocop:disable Metrics/CyclomaticComplexity
|
|
||||||
# Disabling cop since it's good to highlight the complexity of this method by
|
|
||||||
# including all the logic right here.
|
|
||||||
def json_key
|
|
||||||
return root if root
|
|
||||||
# 1. get from options[:serializer] for empty resource collection
|
|
||||||
key = object.empty? &&
|
|
||||||
(explicit_serializer_class = options[:serializer]) &&
|
|
||||||
explicit_serializer_class._type
|
|
||||||
# 2. get from first serializer instance in collection
|
|
||||||
key ||= (serializer = serializers.first) && serializer.json_key
|
|
||||||
# 3. get from collection name, if a named collection
|
|
||||||
key ||= object.respond_to?(:name) ? object.name && object.name.underscore : nil
|
|
||||||
# 4. key may be nil for empty collection and no serializer option
|
|
||||||
key && key.pluralize
|
|
||||||
end
|
|
||||||
# rubocop:enable Metrics/CyclomaticComplexity
|
|
||||||
|
|
||||||
def paginated?
|
|
||||||
ActiveModelSerializers.config.jsonapi_pagination_links_enabled &&
|
|
||||||
object.respond_to?(:current_page) &&
|
|
||||||
object.respond_to?(:total_pages) &&
|
|
||||||
object.respond_to?(:size)
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
attr_reader :serializers, :options
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def serializers_from_resources
|
|
||||||
serializer_context_class = options.fetch(:serializer_context_class, ActiveModel::Serializer)
|
|
||||||
object.map do |resource|
|
|
||||||
serializer_from_resource(resource, serializer_context_class, options)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def serializer_from_resource(resource, serializer_context_class, options)
|
|
||||||
serializer_class = options.fetch(:serializer) do
|
|
||||||
serializer_context_class.serializer_for(resource, namespace: options[:namespace])
|
|
||||||
end
|
|
||||||
|
|
||||||
if serializer_class.nil?
|
|
||||||
ActiveModelSerializers.logger.debug "No serializer found for resource: #{resource.inspect}"
|
|
||||||
throw :no_serializer
|
|
||||||
else
|
|
||||||
serializer_class.new(resource, options.except(:serializer))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,304 +0,0 @@
|
|||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
UndefinedCacheKey = Class.new(StandardError)
|
|
||||||
module Caching
|
|
||||||
extend ActiveSupport::Concern
|
|
||||||
|
|
||||||
included do
|
|
||||||
with_options instance_writer: false, instance_reader: false do |serializer|
|
|
||||||
serializer.class_attribute :_cache # @api private : the cache store
|
|
||||||
serializer.class_attribute :_cache_key # @api private : when present, is first item in cache_key. Ignored if the serializable object defines #cache_key.
|
|
||||||
serializer.class_attribute :_cache_only # @api private : when fragment caching, whitelists fetch_attributes. Cannot combine with except
|
|
||||||
serializer.class_attribute :_cache_except # @api private : when fragment caching, blacklists fetch_attributes. Cannot combine with only
|
|
||||||
serializer.class_attribute :_cache_options # @api private : used by CachedSerializer, passed to _cache.fetch
|
|
||||||
# _cache_options include:
|
|
||||||
# expires_in
|
|
||||||
# compress
|
|
||||||
# force
|
|
||||||
# race_condition_ttl
|
|
||||||
# Passed to ::_cache as
|
|
||||||
# serializer.cache_store.fetch(cache_key, @klass._cache_options)
|
|
||||||
# Passed as second argument to serializer.cache_store.fetch(cache_key, serializer_class._cache_options)
|
|
||||||
serializer.class_attribute :_cache_digest_file_path # @api private : Derived at inheritance
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Matches
|
|
||||||
# "c:/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb:1:in `<top (required)>'"
|
|
||||||
# AND
|
|
||||||
# "/c/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb:1:in `<top (required)>'"
|
|
||||||
# AS
|
|
||||||
# c/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb
|
|
||||||
CALLER_FILE = /
|
|
||||||
\A # start of string
|
|
||||||
.+ # file path (one or more characters)
|
|
||||||
(?= # stop previous match when
|
|
||||||
:\d+ # a colon is followed by one or more digits
|
|
||||||
:in # followed by a colon followed by in
|
|
||||||
)
|
|
||||||
/x
|
|
||||||
|
|
||||||
module ClassMethods
|
|
||||||
def inherited(base)
|
|
||||||
caller_line = caller[1]
|
|
||||||
base._cache_digest_file_path = caller_line
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
def _cache_digest
|
|
||||||
return @_cache_digest if defined?(@_cache_digest)
|
|
||||||
@_cache_digest = digest_caller_file(_cache_digest_file_path)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Hashes contents of file for +_cache_digest+
|
|
||||||
def digest_caller_file(caller_line)
|
|
||||||
serializer_file_path = caller_line[CALLER_FILE]
|
|
||||||
serializer_file_contents = IO.read(serializer_file_path)
|
|
||||||
Digest::MD5.hexdigest(serializer_file_contents)
|
|
||||||
rescue TypeError, Errno::ENOENT
|
|
||||||
warn <<-EOF.strip_heredoc
|
|
||||||
Cannot digest non-existent file: '#{caller_line}'.
|
|
||||||
Please set `::_cache_digest` of the serializer
|
|
||||||
if you'd like to cache it.
|
|
||||||
EOF
|
|
||||||
''.freeze
|
|
||||||
end
|
|
||||||
|
|
||||||
def _skip_digest?
|
|
||||||
_cache_options && _cache_options[:skip_digest]
|
|
||||||
end
|
|
||||||
|
|
||||||
# @api private
|
|
||||||
# maps attribute value to explicit key name
|
|
||||||
# @see Serializer::attribute
|
|
||||||
# @see Serializer::fragmented_attributes
|
|
||||||
def _attributes_keys
|
|
||||||
_attributes_data
|
|
||||||
.each_with_object({}) do |(key, attr), hash|
|
|
||||||
next if key == attr.name
|
|
||||||
hash[attr.name] = { key: key }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def fragmented_attributes
|
|
||||||
cached = _cache_only ? _cache_only : _attributes - _cache_except
|
|
||||||
cached = cached.map! { |field| _attributes_keys.fetch(field, field) }
|
|
||||||
non_cached = _attributes - cached
|
|
||||||
non_cached = non_cached.map! { |field| _attributes_keys.fetch(field, field) }
|
|
||||||
{
|
|
||||||
cached: cached,
|
|
||||||
non_cached: non_cached
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Enables a serializer to be automatically cached
|
|
||||||
#
|
|
||||||
# Sets +::_cache+ object to <tt>ActionController::Base.cache_store</tt>
|
|
||||||
# when Rails.configuration.action_controller.perform_caching
|
|
||||||
#
|
|
||||||
# @param options [Hash] with valid keys:
|
|
||||||
# cache_store : @see ::_cache
|
|
||||||
# key : @see ::_cache_key
|
|
||||||
# only : @see ::_cache_only
|
|
||||||
# except : @see ::_cache_except
|
|
||||||
# skip_digest : does not include digest in cache_key
|
|
||||||
# all else : @see ::_cache_options
|
|
||||||
#
|
|
||||||
# @example
|
|
||||||
# class PostSerializer < ActiveModel::Serializer
|
|
||||||
# cache key: 'post', expires_in: 3.hours
|
|
||||||
# attributes :title, :body
|
|
||||||
#
|
|
||||||
# has_many :comments
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# @todo require less code comments. See
|
|
||||||
# https://github.com/rails-api/active_model_serializers/pull/1249#issuecomment-146567837
|
|
||||||
def cache(options = {})
|
|
||||||
self._cache =
|
|
||||||
options.delete(:cache_store) ||
|
|
||||||
ActiveModelSerializers.config.cache_store ||
|
|
||||||
ActiveSupport::Cache.lookup_store(:null_store)
|
|
||||||
self._cache_key = options.delete(:key)
|
|
||||||
self._cache_only = options.delete(:only)
|
|
||||||
self._cache_except = options.delete(:except)
|
|
||||||
self._cache_options = options.empty? ? nil : options
|
|
||||||
end
|
|
||||||
|
|
||||||
# Value is from ActiveModelSerializers.config.perform_caching. Is used to
|
|
||||||
# globally enable or disable all serializer caching, just like
|
|
||||||
# Rails.configuration.action_controller.perform_caching, which is its
|
|
||||||
# default value in a Rails application.
|
|
||||||
# @return [true, false]
|
|
||||||
# Memoizes value of config first time it is called with a non-nil value.
|
|
||||||
# rubocop:disable Style/ClassVars
|
|
||||||
def perform_caching
|
|
||||||
return @@perform_caching if defined?(@@perform_caching) && !@@perform_caching.nil?
|
|
||||||
@@perform_caching = ActiveModelSerializers.config.perform_caching
|
|
||||||
end
|
|
||||||
alias perform_caching? perform_caching
|
|
||||||
# rubocop:enable Style/ClassVars
|
|
||||||
|
|
||||||
# The canonical method for getting the cache store for the serializer.
|
|
||||||
#
|
|
||||||
# @return [nil] when _cache is not set (i.e. when `cache` has not been called)
|
|
||||||
# @return [._cache] when _cache is not the NullStore
|
|
||||||
# @return [ActiveModelSerializers.config.cache_store] when _cache is the NullStore.
|
|
||||||
# This is so we can use `cache` being called to mean the serializer should be cached
|
|
||||||
# even if ActiveModelSerializers.config.cache_store has not yet been set.
|
|
||||||
# That means that when _cache is the NullStore and ActiveModelSerializers.config.cache_store
|
|
||||||
# is configured, `cache_store` becomes `ActiveModelSerializers.config.cache_store`.
|
|
||||||
# @return [nil] when _cache is the NullStore and ActiveModelSerializers.config.cache_store is nil.
|
|
||||||
def cache_store
|
|
||||||
return nil if _cache.nil?
|
|
||||||
return _cache if _cache.class != ActiveSupport::Cache::NullStore
|
|
||||||
if ActiveModelSerializers.config.cache_store
|
|
||||||
self._cache = ActiveModelSerializers.config.cache_store
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def cache_enabled?
|
|
||||||
perform_caching? && cache_store && !_cache_only && !_cache_except
|
|
||||||
end
|
|
||||||
|
|
||||||
def fragment_cache_enabled?
|
|
||||||
perform_caching? && cache_store &&
|
|
||||||
(_cache_only && !_cache_except || !_cache_only && _cache_except)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Read cache from cache_store
|
|
||||||
# @return [Hash]
|
|
||||||
def cache_read_multi(collection_serializer, adapter_instance, include_directive)
|
|
||||||
return {} if ActiveModelSerializers.config.cache_store.blank?
|
|
||||||
|
|
||||||
keys = object_cache_keys(collection_serializer, adapter_instance, include_directive)
|
|
||||||
|
|
||||||
return {} if keys.blank?
|
|
||||||
|
|
||||||
ActiveModelSerializers.config.cache_store.read_multi(*keys)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Find all cache_key for the collection_serializer
|
|
||||||
# @param serializers [ActiveModel::Serializer::CollectionSerializer]
|
|
||||||
# @param adapter_instance [ActiveModelSerializers::Adapter::Base]
|
|
||||||
# @param include_directive [JSONAPI::IncludeDirective]
|
|
||||||
# @return [Array] all cache_key of collection_serializer
|
|
||||||
def object_cache_keys(collection_serializer, adapter_instance, include_directive)
|
|
||||||
cache_keys = []
|
|
||||||
|
|
||||||
collection_serializer.each do |serializer|
|
|
||||||
cache_keys << object_cache_key(serializer, adapter_instance)
|
|
||||||
|
|
||||||
serializer.associations(include_directive).each do |association|
|
|
||||||
if association.serializer.respond_to?(:each)
|
|
||||||
association.serializer.each do |sub_serializer|
|
|
||||||
cache_keys << object_cache_key(sub_serializer, adapter_instance)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
cache_keys << object_cache_key(association.serializer, adapter_instance)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
cache_keys.compact.uniq
|
|
||||||
end
|
|
||||||
|
|
||||||
# @return [String, nil] the cache_key of the serializer or nil
|
|
||||||
def object_cache_key(serializer, adapter_instance)
|
|
||||||
return unless serializer.present? && serializer.object.present?
|
|
||||||
|
|
||||||
(serializer.class.cache_enabled? || serializer.class.fragment_cache_enabled?) ? serializer.cache_key(adapter_instance) : nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
### INSTANCE METHODS
|
|
||||||
def fetch_attributes(fields, cached_attributes, adapter_instance)
|
|
||||||
if serializer_class.cache_enabled?
|
|
||||||
key = cache_key(adapter_instance)
|
|
||||||
cached_attributes.fetch(key) do
|
|
||||||
serializer_class.cache_store.fetch(key, serializer_class._cache_options) do
|
|
||||||
attributes(fields, true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
elsif serializer_class.fragment_cache_enabled?
|
|
||||||
fetch_attributes_fragment(adapter_instance, cached_attributes)
|
|
||||||
else
|
|
||||||
attributes(fields, true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch(adapter_instance, cache_options = serializer_class._cache_options)
|
|
||||||
if serializer_class.cache_store
|
|
||||||
serializer_class.cache_store.fetch(cache_key(adapter_instance), cache_options) do
|
|
||||||
yield
|
|
||||||
end
|
|
||||||
else
|
|
||||||
yield
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# 1. Determine cached fields from serializer class options
|
|
||||||
# 2. Get non_cached_fields and fetch cache_fields
|
|
||||||
# 3. Merge the two hashes using adapter_instance#fragment_cache
|
|
||||||
# rubocop:disable Metrics/AbcSize
|
|
||||||
def fetch_attributes_fragment(adapter_instance, cached_attributes = {})
|
|
||||||
serializer_class._cache_options ||= {}
|
|
||||||
serializer_class._cache_options[:key] = serializer_class._cache_key if serializer_class._cache_key
|
|
||||||
fields = serializer_class.fragmented_attributes
|
|
||||||
|
|
||||||
non_cached_fields = fields[:non_cached].dup
|
|
||||||
non_cached_hash = attributes(non_cached_fields, true)
|
|
||||||
include_directive = JSONAPI::IncludeDirective.new(non_cached_fields - non_cached_hash.keys)
|
|
||||||
non_cached_hash.merge! resource_relationships({}, { include_directive: include_directive }, adapter_instance)
|
|
||||||
|
|
||||||
cached_fields = fields[:cached].dup
|
|
||||||
key = cache_key(adapter_instance)
|
|
||||||
cached_hash =
|
|
||||||
cached_attributes.fetch(key) do
|
|
||||||
serializer_class.cache_store.fetch(key, serializer_class._cache_options) do
|
|
||||||
hash = attributes(cached_fields, true)
|
|
||||||
include_directive = JSONAPI::IncludeDirective.new(cached_fields - hash.keys)
|
|
||||||
hash.merge! resource_relationships({}, { include_directive: include_directive }, adapter_instance)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
# Merge both results
|
|
||||||
adapter_instance.fragment_cache(cached_hash, non_cached_hash)
|
|
||||||
end
|
|
||||||
# rubocop:enable Metrics/AbcSize
|
|
||||||
|
|
||||||
def cache_key(adapter_instance)
|
|
||||||
return @cache_key if defined?(@cache_key)
|
|
||||||
|
|
||||||
parts = []
|
|
||||||
parts << object_cache_key
|
|
||||||
parts << adapter_instance.cache_key
|
|
||||||
parts << serializer_class._cache_digest unless serializer_class._skip_digest?
|
|
||||||
@cache_key = expand_cache_key(parts)
|
|
||||||
end
|
|
||||||
|
|
||||||
def expand_cache_key(parts)
|
|
||||||
ActiveSupport::Cache.expand_cache_key(parts)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Use object's cache_key if available, else derive a key from the object
|
|
||||||
# Pass the `key` option to the `cache` declaration or override this method to customize the cache key
|
|
||||||
def object_cache_key
|
|
||||||
if object.respond_to?(:cache_key)
|
|
||||||
object.cache_key
|
|
||||||
elsif (serializer_cache_key = (serializer_class._cache_key || serializer_class._cache_options[:key]))
|
|
||||||
object_time_safe = object.updated_at
|
|
||||||
object_time_safe = object_time_safe.strftime('%Y%m%d%H%M%S%9N') if object_time_safe.respond_to?(:strftime)
|
|
||||||
"#{serializer_cache_key}/#{object.id}-#{object_time_safe}"
|
|
||||||
else
|
|
||||||
fail UndefinedCacheKey, "#{object.class} must define #cache_key, or the 'key:' option must be passed into '#{serializer_class}.cache'"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def serializer_class
|
|
||||||
@serializer_class ||= self.class
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
class ErrorSerializer < ActiveModel::Serializer
|
|
||||||
# @return [Hash<field_name,Array<error_message>>]
|
|
||||||
def as_json
|
|
||||||
object.errors.messages
|
|
||||||
end
|
|
||||||
|
|
||||||
def success?
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
require 'active_model/serializer/error_serializer'
|
|
||||||
|
|
||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
class ErrorsSerializer
|
|
||||||
include Enumerable
|
|
||||||
delegate :each, to: :@serializers
|
|
||||||
attr_reader :object, :root
|
|
||||||
|
|
||||||
def initialize(resources, options = {})
|
|
||||||
@root = options[:root]
|
|
||||||
@object = resources
|
|
||||||
@serializers = resources.map do |resource|
|
|
||||||
serializer_class = options.fetch(:serializer) { ActiveModel::Serializer::ErrorSerializer }
|
|
||||||
serializer_class.new(resource, options.except(:serializer))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def success?
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
def json_key
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
attr_reader :serializers
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,90 +0,0 @@
|
|||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
# Holds all the meta-data about a field (i.e. attribute or association) as it was
|
|
||||||
# specified in the ActiveModel::Serializer class.
|
|
||||||
# Notice that the field block is evaluated in the context of the serializer.
|
|
||||||
Field = Struct.new(:name, :options, :block) do
|
|
||||||
def initialize(*)
|
|
||||||
super
|
|
||||||
|
|
||||||
validate_condition!
|
|
||||||
end
|
|
||||||
|
|
||||||
# Compute the actual value of a field for a given serializer instance.
|
|
||||||
# @param [Serializer] The serializer instance for which the value is computed.
|
|
||||||
# @return [Object] value
|
|
||||||
#
|
|
||||||
# @api private
|
|
||||||
#
|
|
||||||
def value(serializer)
|
|
||||||
if block
|
|
||||||
serializer.instance_eval(&block)
|
|
||||||
else
|
|
||||||
serializer.read_attribute_for_serialization(name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Decide whether the field should be serialized by the given serializer instance.
|
|
||||||
# @param [Serializer] The serializer instance
|
|
||||||
# @return [Bool]
|
|
||||||
#
|
|
||||||
# @api private
|
|
||||||
#
|
|
||||||
def excluded?(serializer)
|
|
||||||
case condition_type
|
|
||||||
when :if
|
|
||||||
!evaluate_condition(serializer)
|
|
||||||
when :unless
|
|
||||||
evaluate_condition(serializer)
|
|
||||||
else
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def validate_condition!
|
|
||||||
return if condition_type == :none
|
|
||||||
|
|
||||||
case condition
|
|
||||||
when Symbol, String, Proc
|
|
||||||
# noop
|
|
||||||
else
|
|
||||||
fail TypeError, "#{condition_type.inspect} should be a Symbol, String or Proc"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def evaluate_condition(serializer)
|
|
||||||
case condition
|
|
||||||
when Symbol
|
|
||||||
serializer.public_send(condition)
|
|
||||||
when String
|
|
||||||
serializer.instance_eval(condition)
|
|
||||||
when Proc
|
|
||||||
if condition.arity.zero?
|
|
||||||
serializer.instance_exec(&condition)
|
|
||||||
else
|
|
||||||
serializer.instance_exec(serializer, &condition)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def condition_type
|
|
||||||
@condition_type ||=
|
|
||||||
if options.key?(:if)
|
|
||||||
:if
|
|
||||||
elsif options.key?(:unless)
|
|
||||||
:unless
|
|
||||||
else
|
|
||||||
:none
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def condition
|
|
||||||
options[condition_type]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
class Fieldset
|
|
||||||
def initialize(fields)
|
|
||||||
@raw_fields = fields || {}
|
|
||||||
end
|
|
||||||
|
|
||||||
def fields
|
|
||||||
@fields ||= parsed_fields
|
|
||||||
end
|
|
||||||
|
|
||||||
def fields_for(type)
|
|
||||||
fields[type.singularize.to_sym] || fields[type.pluralize.to_sym]
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
attr_reader :raw_fields
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def parsed_fields
|
|
||||||
if raw_fields.is_a?(Hash)
|
|
||||||
raw_fields.each_with_object({}) { |(k, v), h| h[k.to_sym] = v.map(&:to_sym) }
|
|
||||||
else
|
|
||||||
{}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
# @api private
|
|
||||||
class HasManyReflection < CollectionReflection
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
# @api private
|
|
||||||
class HasOneReflection < SingularReflection
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,150 +0,0 @@
|
|||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
module Lint
|
|
||||||
# == Active \Model \Serializer \Lint \Tests
|
|
||||||
#
|
|
||||||
# You can test whether an object is compliant with the Active \Model \Serializers
|
|
||||||
# API by including <tt>ActiveModel::Serializer::Lint::Tests</tt> in your TestCase.
|
|
||||||
# It will include tests that tell you whether your object is fully compliant,
|
|
||||||
# or if not, which aspects of the API are not implemented.
|
|
||||||
#
|
|
||||||
# Note an object is not required to implement all APIs in order to work
|
|
||||||
# with Active \Model \Serializers. This module only intends to provide guidance in case
|
|
||||||
# you want all features out of the box.
|
|
||||||
#
|
|
||||||
# These tests do not attempt to determine the semantic correctness of the
|
|
||||||
# returned values. For instance, you could implement <tt>serializable_hash</tt> to
|
|
||||||
# always return +{}+, and the tests would pass. It is up to you to ensure
|
|
||||||
# that the values are semantically meaningful.
|
|
||||||
module Tests
|
|
||||||
# Passes if the object responds to <tt>serializable_hash</tt> and if it takes
|
|
||||||
# zero or one arguments.
|
|
||||||
# Fails otherwise.
|
|
||||||
#
|
|
||||||
# <tt>serializable_hash</tt> returns a hash representation of a object's attributes.
|
|
||||||
# Typically, it is implemented by including ActiveModel::Serialization.
|
|
||||||
def test_serializable_hash
|
|
||||||
assert_respond_to resource, :serializable_hash, 'The resource should respond to serializable_hash'
|
|
||||||
resource.serializable_hash
|
|
||||||
resource.serializable_hash(nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Passes if the object responds to <tt>read_attribute_for_serialization</tt>
|
|
||||||
# and if it requires one argument (the attribute to be read).
|
|
||||||
# Fails otherwise.
|
|
||||||
#
|
|
||||||
# <tt>read_attribute_for_serialization</tt> gets the attribute value for serialization
|
|
||||||
# Typically, it is implemented by including ActiveModel::Serialization.
|
|
||||||
def test_read_attribute_for_serialization
|
|
||||||
assert_respond_to resource, :read_attribute_for_serialization, 'The resource should respond to read_attribute_for_serialization'
|
|
||||||
actual_arity = resource.method(:read_attribute_for_serialization).arity
|
|
||||||
# using absolute value since arity is:
|
|
||||||
# 1 for def read_attribute_for_serialization(name); end
|
|
||||||
# -1 for alias :read_attribute_for_serialization :send
|
|
||||||
assert_equal 1, actual_arity.abs, "expected #{actual_arity.inspect}.abs to be 1 or -1"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Passes if the object responds to <tt>as_json</tt> and if it takes
|
|
||||||
# zero or one arguments.
|
|
||||||
# Fails otherwise.
|
|
||||||
#
|
|
||||||
# <tt>as_json</tt> returns a hash representation of a serialized object.
|
|
||||||
# It may delegate to <tt>serializable_hash</tt>
|
|
||||||
# Typically, it is implemented either by including ActiveModel::Serialization
|
|
||||||
# which includes ActiveModel::Serializers::JSON.
|
|
||||||
# or by the JSON gem when required.
|
|
||||||
def test_as_json
|
|
||||||
assert_respond_to resource, :as_json
|
|
||||||
resource.as_json
|
|
||||||
resource.as_json(nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Passes if the object responds to <tt>to_json</tt> and if it takes
|
|
||||||
# zero or one arguments.
|
|
||||||
# Fails otherwise.
|
|
||||||
#
|
|
||||||
# <tt>to_json</tt> returns a string representation (JSON) of a serialized object.
|
|
||||||
# It may be called on the result of <tt>as_json</tt>.
|
|
||||||
# Typically, it is implemented on all objects when the JSON gem is required.
|
|
||||||
def test_to_json
|
|
||||||
assert_respond_to resource, :to_json
|
|
||||||
resource.to_json
|
|
||||||
resource.to_json(nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Passes if the object responds to <tt>cache_key</tt>
|
|
||||||
# Fails otherwise.
|
|
||||||
#
|
|
||||||
# <tt>cache_key</tt> returns a (self-expiring) unique key for the object,
|
|
||||||
# and is part of the (self-expiring) cache_key, which is used by the
|
|
||||||
# adapter. It is not required unless caching is enabled.
|
|
||||||
def test_cache_key
|
|
||||||
assert_respond_to resource, :cache_key
|
|
||||||
actual_arity = resource.method(:cache_key).arity
|
|
||||||
assert_includes [-1, 0], actual_arity, "expected #{actual_arity.inspect} to be 0 or -1"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Passes if the object responds to <tt>updated_at</tt> and if it takes no
|
|
||||||
# arguments.
|
|
||||||
# Fails otherwise.
|
|
||||||
#
|
|
||||||
# <tt>updated_at</tt> returns a Time object or iso8601 string and
|
|
||||||
# is part of the (self-expiring) cache_key, which is used by the adapter.
|
|
||||||
# It is not required unless caching is enabled.
|
|
||||||
def test_updated_at
|
|
||||||
assert_respond_to resource, :updated_at
|
|
||||||
actual_arity = resource.method(:updated_at).arity
|
|
||||||
assert_equal 0, actual_arity
|
|
||||||
end
|
|
||||||
|
|
||||||
# Passes if the object responds to <tt>id</tt> and if it takes no
|
|
||||||
# arguments.
|
|
||||||
# Fails otherwise.
|
|
||||||
#
|
|
||||||
# <tt>id</tt> returns a unique identifier for the object.
|
|
||||||
# It is not required unless caching is enabled.
|
|
||||||
def test_id
|
|
||||||
assert_respond_to resource, :id
|
|
||||||
assert_equal 0, resource.method(:id).arity
|
|
||||||
end
|
|
||||||
|
|
||||||
# Passes if the object's class responds to <tt>model_name</tt> and if it
|
|
||||||
# is in an instance of +ActiveModel::Name+.
|
|
||||||
# Fails otherwise.
|
|
||||||
#
|
|
||||||
# <tt>model_name</tt> returns an ActiveModel::Name instance.
|
|
||||||
# It is used by the serializer to identify the object's type.
|
|
||||||
# It is not required unless caching is enabled.
|
|
||||||
def test_model_name
|
|
||||||
resource_class = resource.class
|
|
||||||
assert_respond_to resource_class, :model_name
|
|
||||||
assert_instance_of resource_class.model_name, ActiveModel::Name
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_active_model_errors
|
|
||||||
assert_respond_to resource, :errors
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_active_model_errors_human_attribute_name
|
|
||||||
assert_respond_to resource.class, :human_attribute_name
|
|
||||||
assert_equal(-2, resource.class.method(:human_attribute_name).arity)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_active_model_errors_lookup_ancestors
|
|
||||||
assert_respond_to resource.class, :lookup_ancestors
|
|
||||||
assert_equal 0, resource.class.method(:lookup_ancestors).arity
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def resource
|
|
||||||
@resource or fail "'@resource' must be set as the linted object"
|
|
||||||
end
|
|
||||||
|
|
||||||
def assert_instance_of(result, name)
|
|
||||||
assert result.instance_of?(name), "#{result} should be an instance of #{name}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
class Null < Serializer
|
|
||||||
def attributes(*)
|
|
||||||
{}
|
|
||||||
end
|
|
||||||
|
|
||||||
def associations(*)
|
|
||||||
{}
|
|
||||||
end
|
|
||||||
|
|
||||||
def serializable_hash(*)
|
|
||||||
{}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,163 +0,0 @@
|
|||||||
require 'active_model/serializer/field'
|
|
||||||
|
|
||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
# Holds all the meta-data about an association as it was specified in the
|
|
||||||
# ActiveModel::Serializer class.
|
|
||||||
#
|
|
||||||
# @example
|
|
||||||
# class PostSerializer < ActiveModel::Serializer
|
|
||||||
# has_one :author, serializer: AuthorSerializer
|
|
||||||
# has_many :comments
|
|
||||||
# has_many :comments, key: :last_comments do
|
|
||||||
# object.comments.last(1)
|
|
||||||
# end
|
|
||||||
# has_many :secret_meta_data, if: :is_admin?
|
|
||||||
#
|
|
||||||
# def is_admin?
|
|
||||||
# current_user.admin?
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# Specifically, the association 'comments' is evaluated two different ways:
|
|
||||||
# 1) as 'comments' and named 'comments'.
|
|
||||||
# 2) as 'object.comments.last(1)' and named 'last_comments'.
|
|
||||||
#
|
|
||||||
# PostSerializer._reflections #=>
|
|
||||||
# # [
|
|
||||||
# # HasOneReflection.new(:author, serializer: AuthorSerializer),
|
|
||||||
# # HasManyReflection.new(:comments)
|
|
||||||
# # HasManyReflection.new(:comments, { key: :last_comments }, #<Block>)
|
|
||||||
# # HasManyReflection.new(:secret_meta_data, { if: :is_admin? })
|
|
||||||
# # ]
|
|
||||||
#
|
|
||||||
# So you can inspect reflections in your Adapters.
|
|
||||||
#
|
|
||||||
class Reflection < Field
|
|
||||||
def initialize(*)
|
|
||||||
super
|
|
||||||
@_links = {}
|
|
||||||
@_include_data = Serializer.config.include_data_default
|
|
||||||
@_meta = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def link(name, value = nil, &block)
|
|
||||||
@_links[name] = block || value
|
|
||||||
:nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def meta(value = nil, &block)
|
|
||||||
@_meta = block || value
|
|
||||||
:nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def include_data(value = true)
|
|
||||||
@_include_data = value
|
|
||||||
:nil
|
|
||||||
end
|
|
||||||
|
|
||||||
# @param serializer [ActiveModel::Serializer]
|
|
||||||
# @yield [ActiveModel::Serializer]
|
|
||||||
# @return [:nil, associated resource or resource collection]
|
|
||||||
# @example
|
|
||||||
# has_one :blog do |serializer|
|
|
||||||
# serializer.cached_blog
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# def cached_blog
|
|
||||||
# cache_store.fetch("cached_blog:#{object.updated_at}") do
|
|
||||||
# Blog.find(object.blog_id)
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
def value(serializer, include_slice)
|
|
||||||
@object = serializer.object
|
|
||||||
@scope = serializer.scope
|
|
||||||
|
|
||||||
block_value = instance_exec(serializer, &block) if block
|
|
||||||
return unless include_data?(include_slice)
|
|
||||||
|
|
||||||
if block && block_value != :nil
|
|
||||||
block_value
|
|
||||||
else
|
|
||||||
serializer.read_attribute_for_serialization(name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Build association. This method is used internally to
|
|
||||||
# build serializer's association by its reflection.
|
|
||||||
#
|
|
||||||
# @param [Serializer] parent_serializer for given association
|
|
||||||
# @param [Hash{Symbol => Object}] parent_serializer_options
|
|
||||||
#
|
|
||||||
# @example
|
|
||||||
# # Given the following serializer defined:
|
|
||||||
# class PostSerializer < ActiveModel::Serializer
|
|
||||||
# has_many :comments, serializer: CommentSummarySerializer
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# # Then you instantiate your serializer
|
|
||||||
# post_serializer = PostSerializer.new(post, foo: 'bar') #
|
|
||||||
# # to build association for comments you need to get reflection
|
|
||||||
# comments_reflection = PostSerializer._reflections.detect { |r| r.name == :comments }
|
|
||||||
# # and #build_association
|
|
||||||
# comments_reflection.build_association(post_serializer, foo: 'bar')
|
|
||||||
#
|
|
||||||
# @api private
|
|
||||||
#
|
|
||||||
def build_association(parent_serializer, parent_serializer_options, include_slice = {})
|
|
||||||
reflection_options = options.dup
|
|
||||||
|
|
||||||
# Pass the parent's namespace onto the child serializer
|
|
||||||
reflection_options[:namespace] ||= parent_serializer_options[:namespace]
|
|
||||||
|
|
||||||
association_value = value(parent_serializer, include_slice)
|
|
||||||
serializer_class = parent_serializer.class.serializer_for(association_value, reflection_options)
|
|
||||||
reflection_options[:include_data] = include_data?(include_slice)
|
|
||||||
reflection_options[:links] = @_links
|
|
||||||
reflection_options[:meta] = @_meta
|
|
||||||
|
|
||||||
if serializer_class
|
|
||||||
serializer = catch(:no_serializer) do
|
|
||||||
serializer_class.new(
|
|
||||||
association_value,
|
|
||||||
serializer_options(parent_serializer, parent_serializer_options, reflection_options)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
if serializer.nil?
|
|
||||||
reflection_options[:virtual_value] = association_value.try(:as_json) || association_value
|
|
||||||
else
|
|
||||||
reflection_options[:serializer] = serializer
|
|
||||||
end
|
|
||||||
elsif !association_value.nil? && !association_value.instance_of?(Object)
|
|
||||||
reflection_options[:virtual_value] = association_value
|
|
||||||
end
|
|
||||||
|
|
||||||
block = nil
|
|
||||||
Association.new(name, reflection_options, block)
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
attr_accessor :object, :scope
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def include_data?(include_slice)
|
|
||||||
if @_include_data == :if_sideloaded
|
|
||||||
include_slice.key?(name)
|
|
||||||
else
|
|
||||||
@_include_data
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def serializer_options(parent_serializer, parent_serializer_options, reflection_options)
|
|
||||||
serializer = reflection_options.fetch(:serializer, nil)
|
|
||||||
|
|
||||||
serializer_options = parent_serializer_options.except(:serializer)
|
|
||||||
serializer_options[:serializer] = serializer if serializer
|
|
||||||
serializer_options[:serializer_context_class] = parent_serializer.class
|
|
||||||
serializer_options
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
# @api private
|
|
||||||
class SingularReflection < Reflection
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
module ActiveModel
|
|
||||||
class Serializer
|
|
||||||
VERSION = '0.10.5'.freeze
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
require 'active_model'
|
|
||||||
require 'active_support'
|
|
||||||
require 'active_support/core_ext/object/with_options'
|
|
||||||
require 'active_support/core_ext/string/inflections'
|
|
||||||
require 'active_support/json'
|
|
||||||
module ActiveModelSerializers
|
|
||||||
extend ActiveSupport::Autoload
|
|
||||||
autoload :Model
|
|
||||||
autoload :Callbacks
|
|
||||||
autoload :Deserialization
|
|
||||||
autoload :SerializableResource
|
|
||||||
autoload :Logging
|
|
||||||
autoload :Test
|
|
||||||
autoload :Adapter
|
|
||||||
autoload :JsonPointer
|
|
||||||
autoload :Deprecate
|
|
||||||
autoload :LookupChain
|
|
||||||
|
|
||||||
class << self; attr_accessor :logger; end
|
|
||||||
self.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
|
|
||||||
|
|
||||||
def self.config
|
|
||||||
ActiveModel::Serializer.config
|
|
||||||
end
|
|
||||||
|
|
||||||
# The file name and line number of the caller of the caller of this method.
|
|
||||||
def self.location_of_caller
|
|
||||||
caller[1] =~ /(.*?):(\d+).*?$/i
|
|
||||||
file = Regexp.last_match(1)
|
|
||||||
lineno = Regexp.last_match(2).to_i
|
|
||||||
|
|
||||||
[file, lineno]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Memoized default include directive
|
|
||||||
# @return [JSONAPI::IncludeDirective]
|
|
||||||
def self.default_include_directive
|
|
||||||
@default_include_directive ||= JSONAPI::IncludeDirective.new(config.default_includes, allow_wildcard: true)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.silence_warnings
|
|
||||||
original_verbose = $VERBOSE
|
|
||||||
$VERBOSE = nil
|
|
||||||
yield
|
|
||||||
ensure
|
|
||||||
$VERBOSE = original_verbose
|
|
||||||
end
|
|
||||||
|
|
||||||
require 'active_model/serializer/version'
|
|
||||||
require 'active_model/serializer'
|
|
||||||
require 'active_model/serializable_resource'
|
|
||||||
require 'active_model_serializers/railtie' if defined?(::Rails)
|
|
||||||
end
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
module ActiveModelSerializers
|
|
||||||
module Adapter
|
|
||||||
UnknownAdapterError = Class.new(ArgumentError)
|
|
||||||
ADAPTER_MAP = {} # rubocop:disable Style/MutableConstant
|
|
||||||
private_constant :ADAPTER_MAP if defined?(private_constant)
|
|
||||||
|
|
||||||
class << self # All methods are class functions
|
|
||||||
# :nocov:
|
|
||||||
def new(*args)
|
|
||||||
fail ArgumentError, 'Adapters inherit from Adapter::Base.' \
|
|
||||||
"Adapter.new called with args: '#{args.inspect}', from" \
|
|
||||||
"'caller[0]'."
|
|
||||||
end
|
|
||||||
# :nocov:
|
|
||||||
|
|
||||||
def configured_adapter
|
|
||||||
lookup(ActiveModelSerializers.config.adapter)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create(resource, options = {})
|
|
||||||
override = options.delete(:adapter)
|
|
||||||
klass = override ? adapter_class(override) : configured_adapter
|
|
||||||
klass.new(resource, options)
|
|
||||||
end
|
|
||||||
|
|
||||||
# @see ActiveModelSerializers::Adapter.lookup
|
|
||||||
def adapter_class(adapter)
|
|
||||||
ActiveModelSerializers::Adapter.lookup(adapter)
|
|
||||||
end
|
|
||||||
|
|
||||||
# @return [Hash<adapter_name, adapter_class>]
|
|
||||||
def adapter_map
|
|
||||||
ADAPTER_MAP
|
|
||||||
end
|
|
||||||
|
|
||||||
# @return [Array<Symbol>] list of adapter names
|
|
||||||
def adapters
|
|
||||||
adapter_map.keys.sort
|
|
||||||
end
|
|
||||||
|
|
||||||
# Adds an adapter 'klass' with 'name' to the 'adapter_map'
|
|
||||||
# Names are stringified and underscored
|
|
||||||
# @param name [Symbol, String, Class] name of the registered adapter
|
|
||||||
# @param klass [Class] adapter class itself, optional if name is the class
|
|
||||||
# @example
|
|
||||||
# AMS::Adapter.register(:my_adapter, MyAdapter)
|
|
||||||
# @note The registered name strips out 'ActiveModelSerializers::Adapter::'
|
|
||||||
# so that registering 'ActiveModelSerializers::Adapter::Json' and
|
|
||||||
# 'Json' will both register as 'json'.
|
|
||||||
def register(name, klass = name)
|
|
||||||
name = name.to_s.gsub(/\AActiveModelSerializers::Adapter::/, ''.freeze)
|
|
||||||
adapter_map[name.underscore] = klass
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
def registered_name(adapter_class)
|
|
||||||
ADAPTER_MAP.key adapter_class
|
|
||||||
end
|
|
||||||
|
|
||||||
# @param adapter [String, Symbol, Class] name to fetch adapter by
|
|
||||||
# @return [ActiveModelSerializers::Adapter] subclass of Adapter
|
|
||||||
# @raise [UnknownAdapterError]
|
|
||||||
def lookup(adapter)
|
|
||||||
# 1. return if is a class
|
|
||||||
return adapter if adapter.is_a?(Class)
|
|
||||||
adapter_name = adapter.to_s.underscore
|
|
||||||
# 2. return if registered
|
|
||||||
adapter_map.fetch(adapter_name) do
|
|
||||||
# 3. try to find adapter class from environment
|
|
||||||
adapter_class = find_by_name(adapter_name)
|
|
||||||
register(adapter_name, adapter_class)
|
|
||||||
adapter_class
|
|
||||||
end
|
|
||||||
rescue NameError, ArgumentError => e
|
|
||||||
failure_message =
|
|
||||||
"NameError: #{e.message}. Unknown adapter: #{adapter.inspect}. Valid adapters are: #{adapters}"
|
|
||||||
raise UnknownAdapterError, failure_message, e.backtrace
|
|
||||||
end
|
|
||||||
|
|
||||||
# @api private
|
|
||||||
def find_by_name(adapter_name)
|
|
||||||
adapter_name = adapter_name.to_s.classify.tr('API', 'Api')
|
|
||||||
"ActiveModelSerializers::Adapter::#{adapter_name}".safe_constantize ||
|
|
||||||
"ActiveModelSerializers::Adapter::#{adapter_name.pluralize}".safe_constantize or # rubocop:disable Style/AndOr
|
|
||||||
fail UnknownAdapterError
|
|
||||||
end
|
|
||||||
private :find_by_name
|
|
||||||
end
|
|
||||||
|
|
||||||
# Gotta be at the bottom to use the code above it :(
|
|
||||||
extend ActiveSupport::Autoload
|
|
||||||
autoload :Base
|
|
||||||
autoload :Null
|
|
||||||
autoload :Attributes
|
|
||||||
autoload :Json
|
|
||||||
autoload :JsonApi
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
module ActiveModelSerializers
|
|
||||||
module Adapter
|
|
||||||
class Attributes < Base
|
|
||||||
def serializable_hash(options = nil)
|
|
||||||
options = serialization_options(options)
|
|
||||||
options[:fields] ||= instance_options[:fields]
|
|
||||||
serialized_hash = serializer.serializable_hash(instance_options, options, self)
|
|
||||||
|
|
||||||
self.class.transform_key_casing!(serialized_hash, instance_options)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
require 'case_transform'
|
|
||||||
|
|
||||||
module ActiveModelSerializers
|
|
||||||
module Adapter
|
|
||||||
class Base
|
|
||||||
# Automatically register adapters when subclassing
|
|
||||||
def self.inherited(subclass)
|
|
||||||
ActiveModelSerializers::Adapter.register(subclass)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Sets the default transform for the adapter.
|
|
||||||
#
|
|
||||||
# @return [Symbol] the default transform for the adapter
|
|
||||||
def self.default_key_transform
|
|
||||||
:unaltered
|
|
||||||
end
|
|
||||||
|
|
||||||
# Determines the transform to use in order of precedence:
|
|
||||||
# adapter option, global config, adapter default.
|
|
||||||
#
|
|
||||||
# @param options [Object]
|
|
||||||
# @return [Symbol] the transform to use
|
|
||||||
def self.transform(options)
|
|
||||||
return options[:key_transform] if options && options[:key_transform]
|
|
||||||
ActiveModelSerializers.config.key_transform || default_key_transform
|
|
||||||
end
|
|
||||||
|
|
||||||
# Transforms the casing of the supplied value.
|
|
||||||
#
|
|
||||||
# @param value [Object] the value to be transformed
|
|
||||||
# @param options [Object] serializable resource options
|
|
||||||
# @return [Symbol] the default transform for the adapter
|
|
||||||
def self.transform_key_casing!(value, options)
|
|
||||||
CaseTransform.send(transform(options), value)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.cache_key
|
|
||||||
@cache_key ||= ActiveModelSerializers::Adapter.registered_name(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.fragment_cache(cached_hash, non_cached_hash)
|
|
||||||
non_cached_hash.merge cached_hash
|
|
||||||
end
|
|
||||||
|
|
||||||
attr_reader :serializer, :instance_options
|
|
||||||
|
|
||||||
def initialize(serializer, options = {})
|
|
||||||
@serializer = serializer
|
|
||||||
@instance_options = options
|
|
||||||
end
|
|
||||||
|
|
||||||
# Subclasses that implement this method must first call
|
|
||||||
# options = serialization_options(options)
|
|
||||||
def serializable_hash(_options = nil)
|
|
||||||
fail NotImplementedError, 'This is an abstract method. Should be implemented at the concrete adapter.'
|
|
||||||
end
|
|
||||||
|
|
||||||
def as_json(options = nil)
|
|
||||||
serializable_hash(options)
|
|
||||||
end
|
|
||||||
|
|
||||||
def cache_key
|
|
||||||
self.class.cache_key
|
|
||||||
end
|
|
||||||
|
|
||||||
def fragment_cache(cached_hash, non_cached_hash)
|
|
||||||
self.class.fragment_cache(cached_hash, non_cached_hash)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# see https://github.com/rails-api/active_model_serializers/pull/965
|
|
||||||
# When <tt>options</tt> is +nil+, sets it to +{}+
|
|
||||||
def serialization_options(options)
|
|
||||||
options ||= {} # rubocop:disable Lint/UselessAssignment
|
|
||||||
end
|
|
||||||
|
|
||||||
def root
|
|
||||||
serializer.json_key.to_sym if serializer.json_key
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
module ActiveModelSerializers
|
|
||||||
module Adapter
|
|
||||||
class Json < Base
|
|
||||||
def serializable_hash(options = nil)
|
|
||||||
options = serialization_options(options)
|
|
||||||
serialized_hash = { root => Attributes.new(serializer, instance_options).serializable_hash(options) }
|
|
||||||
serialized_hash[meta_key] = meta unless meta.blank?
|
|
||||||
|
|
||||||
self.class.transform_key_casing!(serialized_hash, instance_options)
|
|
||||||
end
|
|
||||||
|
|
||||||
def meta
|
|
||||||
instance_options.fetch(:meta, nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
def meta_key
|
|
||||||
instance_options.fetch(:meta_key, 'meta'.freeze)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,517 +0,0 @@
|
|||||||
# {http://jsonapi.org/format/ JSON API specification}
|
|
||||||
# rubocop:disable Style/AsciiComments
|
|
||||||
# TODO: implement!
|
|
||||||
# ☐ https://github.com/rails-api/active_model_serializers/issues/1235
|
|
||||||
# TODO: use uri_template in link generation?
|
|
||||||
# ☐ https://github.com/rails-api/active_model_serializers/pull/1282#discussion_r42528812
|
|
||||||
# see gem https://github.com/hannesg/uri_template
|
|
||||||
# spec http://tools.ietf.org/html/rfc6570
|
|
||||||
# impl https://developer.github.com/v3/#schema https://api.github.com/
|
|
||||||
# TODO: validate against a JSON schema document?
|
|
||||||
# ☐ https://github.com/rails-api/active_model_serializers/issues/1162
|
|
||||||
# ☑ https://github.com/rails-api/active_model_serializers/pull/1270
|
|
||||||
# TODO: Routing
|
|
||||||
# ☐ https://github.com/rails-api/active_model_serializers/pull/1476
|
|
||||||
# TODO: Query Params
|
|
||||||
# ☑ `include` https://github.com/rails-api/active_model_serializers/pull/1131
|
|
||||||
# ☑ `fields` https://github.com/rails-api/active_model_serializers/pull/700
|
|
||||||
# ☑ `page[number]=3&page[size]=1` https://github.com/rails-api/active_model_serializers/pull/1041
|
|
||||||
# ☐ `filter`
|
|
||||||
# ☐ `sort`
|
|
||||||
module ActiveModelSerializers
|
|
||||||
module Adapter
|
|
||||||
class JsonApi < Base
|
|
||||||
extend ActiveSupport::Autoload
|
|
||||||
autoload :Jsonapi
|
|
||||||
autoload :ResourceIdentifier
|
|
||||||
autoload :Relationship
|
|
||||||
autoload :Link
|
|
||||||
autoload :PaginationLinks
|
|
||||||
autoload :Meta
|
|
||||||
autoload :Error
|
|
||||||
autoload :Deserialization
|
|
||||||
|
|
||||||
def self.default_key_transform
|
|
||||||
:dash
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.fragment_cache(cached_hash, non_cached_hash, root = true)
|
|
||||||
core_cached = cached_hash.first
|
|
||||||
core_non_cached = non_cached_hash.first
|
|
||||||
no_root_cache = cached_hash.delete_if { |key, _value| key == core_cached[0] }
|
|
||||||
no_root_non_cache = non_cached_hash.delete_if { |key, _value| key == core_non_cached[0] }
|
|
||||||
cached_resource = (core_cached[1]) ? core_cached[1].deep_merge(core_non_cached[1]) : core_non_cached[1]
|
|
||||||
hash = root ? { root => cached_resource } : cached_resource
|
|
||||||
|
|
||||||
hash.deep_merge no_root_non_cache.deep_merge no_root_cache
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(serializer, options = {})
|
|
||||||
super
|
|
||||||
@include_directive = JSONAPI::IncludeDirective.new(options[:include], allow_wildcard: true)
|
|
||||||
@fieldset = options[:fieldset] || ActiveModel::Serializer::Fieldset.new(options.delete(:fields))
|
|
||||||
end
|
|
||||||
|
|
||||||
# {http://jsonapi.org/format/#crud Requests are transactional, i.e. success or failure}
|
|
||||||
# {http://jsonapi.org/format/#document-top-level data and errors MUST NOT coexist in the same document.}
|
|
||||||
def serializable_hash(*)
|
|
||||||
document = if serializer.success?
|
|
||||||
success_document
|
|
||||||
else
|
|
||||||
failure_document
|
|
||||||
end
|
|
||||||
self.class.transform_key_casing!(document, instance_options)
|
|
||||||
end
|
|
||||||
|
|
||||||
def fragment_cache(cached_hash, non_cached_hash)
|
|
||||||
root = !instance_options.include?(:include)
|
|
||||||
self.class.fragment_cache(cached_hash, non_cached_hash, root)
|
|
||||||
end
|
|
||||||
|
|
||||||
# {http://jsonapi.org/format/#document-top-level Primary data}
|
|
||||||
# definition:
|
|
||||||
# ☐ toplevel_data (required)
|
|
||||||
# ☐ toplevel_included
|
|
||||||
# ☑ toplevel_meta
|
|
||||||
# ☑ toplevel_links
|
|
||||||
# ☑ toplevel_jsonapi
|
|
||||||
# structure:
|
|
||||||
# {
|
|
||||||
# data: toplevel_data,
|
|
||||||
# included: toplevel_included,
|
|
||||||
# meta: toplevel_meta,
|
|
||||||
# links: toplevel_links,
|
|
||||||
# jsonapi: toplevel_jsonapi
|
|
||||||
# }.reject! {|_,v| v.nil? }
|
|
||||||
# rubocop:disable Metrics/CyclomaticComplexity
|
|
||||||
def success_document
|
|
||||||
is_collection = serializer.respond_to?(:each)
|
|
||||||
serializers = is_collection ? serializer : [serializer]
|
|
||||||
primary_data, included = resource_objects_for(serializers)
|
|
||||||
|
|
||||||
hash = {}
|
|
||||||
# toplevel_data
|
|
||||||
# definition:
|
|
||||||
# oneOf
|
|
||||||
# resource
|
|
||||||
# array of unique items of type 'resource'
|
|
||||||
# null
|
|
||||||
#
|
|
||||||
# description:
|
|
||||||
# The document's "primary data" is a representation of the resource or collection of resources
|
|
||||||
# targeted by a request.
|
|
||||||
#
|
|
||||||
# Singular: the resource object.
|
|
||||||
#
|
|
||||||
# Collection: one of an array of resource objects, an array of resource identifier objects, or
|
|
||||||
# an empty array ([]), for requests that target resource collections.
|
|
||||||
#
|
|
||||||
# None: null if the request is one that might correspond to a single resource, but doesn't currently.
|
|
||||||
# structure:
|
|
||||||
# if serializable_resource.resource?
|
|
||||||
# resource
|
|
||||||
# elsif serializable_resource.collection?
|
|
||||||
# [
|
|
||||||
# resource,
|
|
||||||
# resource
|
|
||||||
# ]
|
|
||||||
# else
|
|
||||||
# nil
|
|
||||||
# end
|
|
||||||
hash[:data] = is_collection ? primary_data : primary_data[0]
|
|
||||||
# toplevel_included
|
|
||||||
# alias included
|
|
||||||
# definition:
|
|
||||||
# array of unique items of type 'resource'
|
|
||||||
#
|
|
||||||
# description:
|
|
||||||
# To reduce the number of HTTP requests, servers **MAY** allow
|
|
||||||
# responses that include related resources along with the requested primary
|
|
||||||
# resources. Such responses are called "compound documents".
|
|
||||||
# structure:
|
|
||||||
# [
|
|
||||||
# resource,
|
|
||||||
# resource
|
|
||||||
# ]
|
|
||||||
hash[:included] = included if included.any?
|
|
||||||
|
|
||||||
Jsonapi.add!(hash)
|
|
||||||
|
|
||||||
if instance_options[:links]
|
|
||||||
hash[:links] ||= {}
|
|
||||||
hash[:links].update(instance_options[:links])
|
|
||||||
end
|
|
||||||
|
|
||||||
if is_collection && serializer.paginated?
|
|
||||||
hash[:links] ||= {}
|
|
||||||
hash[:links].update(pagination_links_for(serializer))
|
|
||||||
end
|
|
||||||
|
|
||||||
hash[:meta] = instance_options[:meta] unless instance_options[:meta].blank?
|
|
||||||
|
|
||||||
hash
|
|
||||||
end
|
|
||||||
# rubocop:enable Metrics/CyclomaticComplexity
|
|
||||||
|
|
||||||
# {http://jsonapi.org/format/#errors JSON API Errors}
|
|
||||||
# TODO: look into caching
|
|
||||||
# definition:
|
|
||||||
# ☑ toplevel_errors array (required)
|
|
||||||
# ☐ toplevel_meta
|
|
||||||
# ☐ toplevel_jsonapi
|
|
||||||
# structure:
|
|
||||||
# {
|
|
||||||
# errors: toplevel_errors,
|
|
||||||
# meta: toplevel_meta,
|
|
||||||
# jsonapi: toplevel_jsonapi
|
|
||||||
# }.reject! {|_,v| v.nil? }
|
|
||||||
# prs:
|
|
||||||
# https://github.com/rails-api/active_model_serializers/pull/1004
|
|
||||||
def failure_document
|
|
||||||
hash = {}
|
|
||||||
# PR Please :)
|
|
||||||
# Jsonapi.add!(hash)
|
|
||||||
|
|
||||||
# toplevel_errors
|
|
||||||
# definition:
|
|
||||||
# array of unique items of type 'error'
|
|
||||||
# structure:
|
|
||||||
# [
|
|
||||||
# error,
|
|
||||||
# error
|
|
||||||
# ]
|
|
||||||
if serializer.respond_to?(:each)
|
|
||||||
hash[:errors] = serializer.flat_map do |error_serializer|
|
|
||||||
Error.resource_errors(error_serializer, instance_options)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
hash[:errors] = Error.resource_errors(serializer, instance_options)
|
|
||||||
end
|
|
||||||
hash
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
attr_reader :fieldset
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# {http://jsonapi.org/format/#document-resource-objects Primary data}
|
|
||||||
# resource
|
|
||||||
# definition:
|
|
||||||
# JSON Object
|
|
||||||
#
|
|
||||||
# properties:
|
|
||||||
# type (required) : String
|
|
||||||
# id (required) : String
|
|
||||||
# attributes
|
|
||||||
# relationships
|
|
||||||
# links
|
|
||||||
# meta
|
|
||||||
#
|
|
||||||
# description:
|
|
||||||
# "Resource objects" appear in a JSON API document to represent resources
|
|
||||||
# structure:
|
|
||||||
# {
|
|
||||||
# type: 'admin--some-user',
|
|
||||||
# id: '1336',
|
|
||||||
# attributes: attributes,
|
|
||||||
# relationships: relationships,
|
|
||||||
# links: links,
|
|
||||||
# meta: meta,
|
|
||||||
# }.reject! {|_,v| v.nil? }
|
|
||||||
# prs:
|
|
||||||
# type
|
|
||||||
# https://github.com/rails-api/active_model_serializers/pull/1122
|
|
||||||
# [x] https://github.com/rails-api/active_model_serializers/pull/1213
|
|
||||||
# https://github.com/rails-api/active_model_serializers/pull/1216
|
|
||||||
# https://github.com/rails-api/active_model_serializers/pull/1029
|
|
||||||
# links
|
|
||||||
# [x] https://github.com/rails-api/active_model_serializers/pull/1246
|
|
||||||
# [x] url helpers https://github.com/rails-api/active_model_serializers/issues/1269
|
|
||||||
# meta
|
|
||||||
# [x] https://github.com/rails-api/active_model_serializers/pull/1340
|
|
||||||
def resource_objects_for(serializers)
|
|
||||||
@primary = []
|
|
||||||
@included = []
|
|
||||||
@resource_identifiers = Set.new
|
|
||||||
serializers.each { |serializer| process_resource(serializer, true, @include_directive) }
|
|
||||||
serializers.each { |serializer| process_relationships(serializer, @include_directive) }
|
|
||||||
|
|
||||||
[@primary, @included]
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_resource(serializer, primary, include_slice = {})
|
|
||||||
resource_identifier = ResourceIdentifier.new(serializer, instance_options).as_json
|
|
||||||
return false unless @resource_identifiers.add?(resource_identifier)
|
|
||||||
|
|
||||||
resource_object = resource_object_for(serializer, include_slice)
|
|
||||||
if primary
|
|
||||||
@primary << resource_object
|
|
||||||
else
|
|
||||||
@included << resource_object
|
|
||||||
end
|
|
||||||
|
|
||||||
true
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_relationships(serializer, include_slice)
|
|
||||||
serializer.associations(include_slice).each do |association|
|
|
||||||
process_relationship(association.serializer, include_slice[association.key])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_relationship(serializer, include_slice)
|
|
||||||
if serializer.respond_to?(:each)
|
|
||||||
serializer.each { |s| process_relationship(s, include_slice) }
|
|
||||||
return
|
|
||||||
end
|
|
||||||
return unless serializer && serializer.object
|
|
||||||
return unless process_resource(serializer, false, include_slice)
|
|
||||||
|
|
||||||
process_relationships(serializer, include_slice)
|
|
||||||
end
|
|
||||||
|
|
||||||
# {http://jsonapi.org/format/#document-resource-object-attributes Document Resource Object Attributes}
|
|
||||||
# attributes
|
|
||||||
# definition:
|
|
||||||
# JSON Object
|
|
||||||
#
|
|
||||||
# patternProperties:
|
|
||||||
# ^(?!relationships$|links$)\\w[-\\w_]*$
|
|
||||||
#
|
|
||||||
# description:
|
|
||||||
# Members of the attributes object ("attributes") represent information about the resource
|
|
||||||
# object in which it's defined.
|
|
||||||
# Attributes may contain any valid JSON value
|
|
||||||
# structure:
|
|
||||||
# {
|
|
||||||
# foo: 'bar'
|
|
||||||
# }
|
|
||||||
def attributes_for(serializer, fields)
|
|
||||||
serializer.attributes(fields).except(:id)
|
|
||||||
end
|
|
||||||
|
|
||||||
# {http://jsonapi.org/format/#document-resource-objects Document Resource Objects}
|
|
||||||
def resource_object_for(serializer, include_slice = {})
|
|
||||||
resource_object = serializer.fetch(self) do
|
|
||||||
resource_object = ResourceIdentifier.new(serializer, instance_options).as_json
|
|
||||||
|
|
||||||
requested_fields = fieldset && fieldset.fields_for(resource_object[:type])
|
|
||||||
attributes = attributes_for(serializer, requested_fields)
|
|
||||||
resource_object[:attributes] = attributes if attributes.any?
|
|
||||||
resource_object
|
|
||||||
end
|
|
||||||
|
|
||||||
requested_associations = fieldset.fields_for(resource_object[:type]) || '*'
|
|
||||||
relationships = relationships_for(serializer, requested_associations, include_slice)
|
|
||||||
resource_object[:relationships] = relationships if relationships.any?
|
|
||||||
|
|
||||||
links = links_for(serializer)
|
|
||||||
# toplevel_links
|
|
||||||
# definition:
|
|
||||||
# allOf
|
|
||||||
# ☐ links
|
|
||||||
# ☐ pagination
|
|
||||||
#
|
|
||||||
# description:
|
|
||||||
# Link members related to the primary data.
|
|
||||||
# structure:
|
|
||||||
# links.merge!(pagination)
|
|
||||||
# prs:
|
|
||||||
# https://github.com/rails-api/active_model_serializers/pull/1247
|
|
||||||
# https://github.com/rails-api/active_model_serializers/pull/1018
|
|
||||||
resource_object[:links] = links if links.any?
|
|
||||||
|
|
||||||
# toplevel_meta
|
|
||||||
# alias meta
|
|
||||||
# definition:
|
|
||||||
# meta
|
|
||||||
# structure
|
|
||||||
# {
|
|
||||||
# :'git-ref' => 'abc123'
|
|
||||||
# }
|
|
||||||
meta = meta_for(serializer)
|
|
||||||
resource_object[:meta] = meta unless meta.blank?
|
|
||||||
|
|
||||||
resource_object
|
|
||||||
end
|
|
||||||
|
|
||||||
# {http://jsonapi.org/format/#document-resource-object-relationships Document Resource Object Relationship}
|
|
||||||
# relationships
|
|
||||||
# definition:
|
|
||||||
# JSON Object
|
|
||||||
#
|
|
||||||
# patternProperties:
|
|
||||||
# ^\\w[-\\w_]*$"
|
|
||||||
#
|
|
||||||
# properties:
|
|
||||||
# data : relationshipsData
|
|
||||||
# links
|
|
||||||
# meta
|
|
||||||
#
|
|
||||||
# description:
|
|
||||||
#
|
|
||||||
# Members of the relationships object ("relationships") represent references from the
|
|
||||||
# resource object in which it's defined to other resource objects."
|
|
||||||
# structure:
|
|
||||||
# {
|
|
||||||
# links: links,
|
|
||||||
# meta: meta,
|
|
||||||
# data: relationshipsData
|
|
||||||
# }.reject! {|_,v| v.nil? }
|
|
||||||
#
|
|
||||||
# prs:
|
|
||||||
# links
|
|
||||||
# [x] https://github.com/rails-api/active_model_serializers/pull/1454
|
|
||||||
# meta
|
|
||||||
# [x] https://github.com/rails-api/active_model_serializers/pull/1454
|
|
||||||
# polymorphic
|
|
||||||
# [ ] https://github.com/rails-api/active_model_serializers/pull/1420
|
|
||||||
#
|
|
||||||
# relationshipsData
|
|
||||||
# definition:
|
|
||||||
# oneOf
|
|
||||||
# relationshipToOne
|
|
||||||
# relationshipToMany
|
|
||||||
#
|
|
||||||
# description:
|
|
||||||
# Member, whose value represents "resource linkage"
|
|
||||||
# structure:
|
|
||||||
# if has_one?
|
|
||||||
# relationshipToOne
|
|
||||||
# else
|
|
||||||
# relationshipToMany
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# definition:
|
|
||||||
# anyOf
|
|
||||||
# null
|
|
||||||
# linkage
|
|
||||||
#
|
|
||||||
# relationshipToOne
|
|
||||||
# description:
|
|
||||||
#
|
|
||||||
# References to other resource objects in a to-one ("relationship"). Relationships can be
|
|
||||||
# specified by including a member in a resource's links object.
|
|
||||||
#
|
|
||||||
# None: Describes an empty to-one relationship.
|
|
||||||
# structure:
|
|
||||||
# if has_related?
|
|
||||||
# linkage
|
|
||||||
# else
|
|
||||||
# nil
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# relationshipToMany
|
|
||||||
# definition:
|
|
||||||
# array of unique items of type 'linkage'
|
|
||||||
#
|
|
||||||
# description:
|
|
||||||
# An array of objects each containing "type" and "id" members for to-many relationships
|
|
||||||
# structure:
|
|
||||||
# [
|
|
||||||
# linkage,
|
|
||||||
# linkage
|
|
||||||
# ]
|
|
||||||
# prs:
|
|
||||||
# polymorphic
|
|
||||||
# [ ] https://github.com/rails-api/active_model_serializers/pull/1282
|
|
||||||
#
|
|
||||||
# linkage
|
|
||||||
# definition:
|
|
||||||
# type (required) : String
|
|
||||||
# id (required) : String
|
|
||||||
# meta
|
|
||||||
#
|
|
||||||
# description:
|
|
||||||
# The "type" and "id" to non-empty members.
|
|
||||||
# structure:
|
|
||||||
# {
|
|
||||||
# type: 'required-type',
|
|
||||||
# id: 'required-id',
|
|
||||||
# meta: meta
|
|
||||||
# }.reject! {|_,v| v.nil? }
|
|
||||||
def relationships_for(serializer, requested_associations, include_slice)
|
|
||||||
include_directive = JSONAPI::IncludeDirective.new(
|
|
||||||
requested_associations,
|
|
||||||
allow_wildcard: true
|
|
||||||
)
|
|
||||||
serializer.associations(include_directive, include_slice).each_with_object({}) do |association, hash|
|
|
||||||
hash[association.key] = Relationship.new(serializer, instance_options, association).as_json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# {http://jsonapi.org/format/#document-links Document Links}
|
|
||||||
# links
|
|
||||||
# definition:
|
|
||||||
# JSON Object
|
|
||||||
#
|
|
||||||
# properties:
|
|
||||||
# self : URI
|
|
||||||
# related : link
|
|
||||||
#
|
|
||||||
# description:
|
|
||||||
# A resource object **MAY** contain references to other resource objects ("relationships").
|
|
||||||
# Relationships may be to-one or to-many. Relationships can be specified by including a member
|
|
||||||
# in a resource's links object.
|
|
||||||
#
|
|
||||||
# A `self` member’s value is a URL for the relationship itself (a "relationship URL"). This
|
|
||||||
# URL allows the client to directly manipulate the relationship. For example, it would allow
|
|
||||||
# a client to remove an `author` from an `article` without deleting the people resource
|
|
||||||
# itself.
|
|
||||||
# structure:
|
|
||||||
# {
|
|
||||||
# self: 'http://example.com/etc',
|
|
||||||
# related: link
|
|
||||||
# }.reject! {|_,v| v.nil? }
|
|
||||||
def links_for(serializer)
|
|
||||||
serializer._links.each_with_object({}) do |(name, value), hash|
|
|
||||||
result = Link.new(serializer, value).as_json
|
|
||||||
hash[name] = result if result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# {http://jsonapi.org/format/#fetching-pagination Pagination Links}
|
|
||||||
# pagination
|
|
||||||
# definition:
|
|
||||||
# first : pageObject
|
|
||||||
# last : pageObject
|
|
||||||
# prev : pageObject
|
|
||||||
# next : pageObject
|
|
||||||
# structure:
|
|
||||||
# {
|
|
||||||
# first: pageObject,
|
|
||||||
# last: pageObject,
|
|
||||||
# prev: pageObject,
|
|
||||||
# next: pageObject
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# pageObject
|
|
||||||
# definition:
|
|
||||||
# oneOf
|
|
||||||
# URI
|
|
||||||
# null
|
|
||||||
#
|
|
||||||
# description:
|
|
||||||
# The <x> page of data
|
|
||||||
# structure:
|
|
||||||
# if has_page?
|
|
||||||
# 'http://example.com/some-page?page[number][x]'
|
|
||||||
# else
|
|
||||||
# nil
|
|
||||||
# end
|
|
||||||
# prs:
|
|
||||||
# https://github.com/rails-api/active_model_serializers/pull/1041
|
|
||||||
def pagination_links_for(serializer)
|
|
||||||
PaginationLinks.new(serializer.object, instance_options).as_json
|
|
||||||
end
|
|
||||||
|
|
||||||
# {http://jsonapi.org/format/#document-meta Docment Meta}
|
|
||||||
def meta_for(serializer)
|
|
||||||
Meta.new(serializer).as_json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
# rubocop:enable Style/AsciiComments
|
|
||||||
@ -1,213 +0,0 @@
|
|||||||
module ActiveModelSerializers
|
|
||||||
module Adapter
|
|
||||||
class JsonApi
|
|
||||||
# NOTE(Experimental):
|
|
||||||
# This is an experimental feature. Both the interface and internals could be subject
|
|
||||||
# to changes.
|
|
||||||
module Deserialization
|
|
||||||
InvalidDocument = Class.new(ArgumentError)
|
|
||||||
|
|
||||||
module_function
|
|
||||||
|
|
||||||
# Transform a JSON API document, containing a single data object,
|
|
||||||
# into a hash that is ready for ActiveRecord::Base.new() and such.
|
|
||||||
# Raises InvalidDocument if the payload is not properly formatted.
|
|
||||||
#
|
|
||||||
# @param [Hash|ActionController::Parameters] document
|
|
||||||
# @param [Hash] options
|
|
||||||
# only: Array of symbols of whitelisted fields.
|
|
||||||
# except: Array of symbols of blacklisted fields.
|
|
||||||
# keys: Hash of translated keys (e.g. :author => :user).
|
|
||||||
# polymorphic: Array of symbols of polymorphic fields.
|
|
||||||
# @return [Hash]
|
|
||||||
#
|
|
||||||
# @example
|
|
||||||
# document = {
|
|
||||||
# data: {
|
|
||||||
# id: 1,
|
|
||||||
# type: 'post',
|
|
||||||
# attributes: {
|
|
||||||
# title: 'Title 1',
|
|
||||||
# date: '2015-12-20'
|
|
||||||
# },
|
|
||||||
# associations: {
|
|
||||||
# author: {
|
|
||||||
# data: {
|
|
||||||
# type: 'user',
|
|
||||||
# id: 2
|
|
||||||
# }
|
|
||||||
# },
|
|
||||||
# second_author: {
|
|
||||||
# data: nil
|
|
||||||
# },
|
|
||||||
# comments: {
|
|
||||||
# data: [{
|
|
||||||
# type: 'comment',
|
|
||||||
# id: 3
|
|
||||||
# },{
|
|
||||||
# type: 'comment',
|
|
||||||
# id: 4
|
|
||||||
# }]
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# parse(document) #=>
|
|
||||||
# # {
|
|
||||||
# # title: 'Title 1',
|
|
||||||
# # date: '2015-12-20',
|
|
||||||
# # author_id: 2,
|
|
||||||
# # second_author_id: nil
|
|
||||||
# # comment_ids: [3, 4]
|
|
||||||
# # }
|
|
||||||
#
|
|
||||||
# parse(document, only: [:title, :date, :author],
|
|
||||||
# keys: { date: :published_at },
|
|
||||||
# polymorphic: [:author]) #=>
|
|
||||||
# # {
|
|
||||||
# # title: 'Title 1',
|
|
||||||
# # published_at: '2015-12-20',
|
|
||||||
# # author_id: '2',
|
|
||||||
# # author_type: 'people'
|
|
||||||
# # }
|
|
||||||
#
|
|
||||||
def parse!(document, options = {})
|
|
||||||
parse(document, options) do |invalid_payload, reason|
|
|
||||||
fail InvalidDocument, "Invalid payload (#{reason}): #{invalid_payload}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Same as parse!, but returns an empty hash instead of raising InvalidDocument
|
|
||||||
# on invalid payloads.
|
|
||||||
def parse(document, options = {})
|
|
||||||
document = document.dup.permit!.to_h if document.is_a?(ActionController::Parameters)
|
|
||||||
|
|
||||||
validate_payload(document) do |invalid_document, reason|
|
|
||||||
yield invalid_document, reason if block_given?
|
|
||||||
return {}
|
|
||||||
end
|
|
||||||
|
|
||||||
primary_data = document['data']
|
|
||||||
attributes = primary_data['attributes'] || {}
|
|
||||||
attributes['id'] = primary_data['id'] if primary_data['id']
|
|
||||||
relationships = primary_data['relationships'] || {}
|
|
||||||
|
|
||||||
filter_fields(attributes, options)
|
|
||||||
filter_fields(relationships, options)
|
|
||||||
|
|
||||||
hash = {}
|
|
||||||
hash.merge!(parse_attributes(attributes, options))
|
|
||||||
hash.merge!(parse_relationships(relationships, options))
|
|
||||||
|
|
||||||
hash
|
|
||||||
end
|
|
||||||
|
|
||||||
# Checks whether a payload is compliant with the JSON API spec.
|
|
||||||
#
|
|
||||||
# @api private
|
|
||||||
# rubocop:disable Metrics/CyclomaticComplexity
|
|
||||||
def validate_payload(payload)
|
|
||||||
unless payload.is_a?(Hash)
|
|
||||||
yield payload, 'Expected hash'
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
primary_data = payload['data']
|
|
||||||
unless primary_data.is_a?(Hash)
|
|
||||||
yield payload, { data: 'Expected hash' }
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
attributes = primary_data['attributes'] || {}
|
|
||||||
unless attributes.is_a?(Hash)
|
|
||||||
yield payload, { data: { attributes: 'Expected hash or nil' } }
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
relationships = primary_data['relationships'] || {}
|
|
||||||
unless relationships.is_a?(Hash)
|
|
||||||
yield payload, { data: { relationships: 'Expected hash or nil' } }
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
relationships.each do |(key, value)|
|
|
||||||
unless value.is_a?(Hash) && value.key?('data')
|
|
||||||
yield payload, { data: { relationships: { key => 'Expected hash with :data key' } } }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
# rubocop:enable Metrics/CyclomaticComplexity
|
|
||||||
|
|
||||||
# @api private
|
|
||||||
def filter_fields(fields, options)
|
|
||||||
if (only = options[:only])
|
|
||||||
fields.slice!(*Array(only).map(&:to_s))
|
|
||||||
elsif (except = options[:except])
|
|
||||||
fields.except!(*Array(except).map(&:to_s))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# @api private
|
|
||||||
def field_key(field, options)
|
|
||||||
(options[:keys] || {}).fetch(field.to_sym, field).to_sym
|
|
||||||
end
|
|
||||||
|
|
||||||
# @api private
|
|
||||||
def parse_attributes(attributes, options)
|
|
||||||
transform_keys(attributes, options)
|
|
||||||
.map { |(k, v)| { field_key(k, options) => v } }
|
|
||||||
.reduce({}, :merge)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Given an association name, and a relationship data attribute, build a hash
|
|
||||||
# mapping the corresponding ActiveRecord attribute to the corresponding value.
|
|
||||||
#
|
|
||||||
# @example
|
|
||||||
# parse_relationship(:comments, [{ 'id' => '1', 'type' => 'comments' },
|
|
||||||
# { 'id' => '2', 'type' => 'comments' }],
|
|
||||||
# {})
|
|
||||||
# # => { :comment_ids => ['1', '2'] }
|
|
||||||
# parse_relationship(:author, { 'id' => '1', 'type' => 'users' }, {})
|
|
||||||
# # => { :author_id => '1' }
|
|
||||||
# parse_relationship(:author, nil, {})
|
|
||||||
# # => { :author_id => nil }
|
|
||||||
# @param [Symbol] assoc_name
|
|
||||||
# @param [Hash] assoc_data
|
|
||||||
# @param [Hash] options
|
|
||||||
# @return [Hash{Symbol, Object}]
|
|
||||||
#
|
|
||||||
# @api private
|
|
||||||
def parse_relationship(assoc_name, assoc_data, options)
|
|
||||||
prefix_key = field_key(assoc_name, options).to_s.singularize
|
|
||||||
hash =
|
|
||||||
if assoc_data.is_a?(Array)
|
|
||||||
{ "#{prefix_key}_ids".to_sym => assoc_data.map { |ri| ri['id'] } }
|
|
||||||
else
|
|
||||||
{ "#{prefix_key}_id".to_sym => assoc_data ? assoc_data['id'] : nil }
|
|
||||||
end
|
|
||||||
|
|
||||||
polymorphic = (options[:polymorphic] || []).include?(assoc_name.to_sym)
|
|
||||||
if polymorphic
|
|
||||||
hash["#{prefix_key}_type".to_sym] = assoc_data.present? ? assoc_data['type'] : nil
|
|
||||||
end
|
|
||||||
|
|
||||||
hash
|
|
||||||
end
|
|
||||||
|
|
||||||
# @api private
|
|
||||||
def parse_relationships(relationships, options)
|
|
||||||
transform_keys(relationships, options)
|
|
||||||
.map { |(k, v)| parse_relationship(k, v['data'], options) }
|
|
||||||
.reduce({}, :merge)
|
|
||||||
end
|
|
||||||
|
|
||||||
# @api private
|
|
||||||
def transform_keys(hash, options)
|
|
||||||
transform = options[:key_transform] || :underscore
|
|
||||||
CaseTransform.send(transform, hash)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
module ActiveModelSerializers
|
|
||||||
module Adapter
|
|
||||||
class JsonApi < Base
|
|
||||||
module Error
|
|
||||||
# rubocop:disable Style/AsciiComments
|
|
||||||
UnknownSourceTypeError = Class.new(ArgumentError)
|
|
||||||
|
|
||||||
# Builds a JSON API Errors Object
|
|
||||||
# {http://jsonapi.org/format/#errors JSON API Errors}
|
|
||||||
#
|
|
||||||
# @param [ActiveModel::Serializer::ErrorSerializer] error_serializer
|
|
||||||
# @return [Array<Symbol, Array<String>>] i.e. attribute_name, [attribute_errors]
|
|
||||||
def self.resource_errors(error_serializer, options)
|
|
||||||
error_serializer.as_json.flat_map do |attribute_name, attribute_errors|
|
|
||||||
attribute_name = JsonApi.send(:transform_key_casing!, attribute_name,
|
|
||||||
options)
|
|
||||||
attribute_error_objects(attribute_name, attribute_errors)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# definition:
|
|
||||||
# JSON Object
|
|
||||||
#
|
|
||||||
# properties:
|
|
||||||
# ☐ id : String
|
|
||||||
# ☐ status : String
|
|
||||||
# ☐ code : String
|
|
||||||
# ☐ title : String
|
|
||||||
# ☑ detail : String
|
|
||||||
# ☐ links
|
|
||||||
# ☐ meta
|
|
||||||
# ☑ error_source
|
|
||||||
#
|
|
||||||
# description:
|
|
||||||
# id : A unique identifier for this particular occurrence of the problem.
|
|
||||||
# status : The HTTP status code applicable to this problem, expressed as a string value
|
|
||||||
# code : An application-specific error code, expressed as a string value.
|
|
||||||
# title : A short, human-readable summary of the problem. It **SHOULD NOT** change from
|
|
||||||
# occurrence to occurrence of the problem, except for purposes of localization.
|
|
||||||
# detail : A human-readable explanation specific to this occurrence of the problem.
|
|
||||||
# structure:
|
|
||||||
# {
|
|
||||||
# title: 'SystemFailure',
|
|
||||||
# detail: 'something went terribly wrong',
|
|
||||||
# status: '500'
|
|
||||||
# }.merge!(errorSource)
|
|
||||||
def self.attribute_error_objects(attribute_name, attribute_errors)
|
|
||||||
attribute_errors.map do |attribute_error|
|
|
||||||
{
|
|
||||||
source: error_source(:pointer, attribute_name),
|
|
||||||
detail: attribute_error
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# errorSource
|
|
||||||
# description:
|
|
||||||
# oneOf
|
|
||||||
# ☑ pointer : String
|
|
||||||
# ☑ parameter : String
|
|
||||||
#
|
|
||||||
# description:
|
|
||||||
# pointer: A JSON Pointer RFC6901 to the associated entity in the request document e.g. "/data"
|
|
||||||
# for a primary data object, or "/data/attributes/title" for a specific attribute.
|
|
||||||
# https://tools.ietf.org/html/rfc6901
|
|
||||||
#
|
|
||||||
# parameter: A string indicating which query parameter caused the error
|
|
||||||
# structure:
|
|
||||||
# if is_attribute?
|
|
||||||
# {
|
|
||||||
# pointer: '/data/attributes/red-button'
|
|
||||||
# }
|
|
||||||
# else
|
|
||||||
# {
|
|
||||||
# parameter: 'pres'
|
|
||||||
# }
|
|
||||||
# end
|
|
||||||
def self.error_source(source_type, attribute_name)
|
|
||||||
case source_type
|
|
||||||
when :pointer
|
|
||||||
{
|
|
||||||
pointer: ActiveModelSerializers::JsonPointer.new(:attribute, attribute_name)
|
|
||||||
}
|
|
||||||
when :parameter
|
|
||||||
{
|
|
||||||
parameter: attribute_name
|
|
||||||
}
|
|
||||||
else
|
|
||||||
fail UnknownSourceTypeError, "Unknown source type '#{source_type}' for attribute_name '#{attribute_name}'"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
# rubocop:enable Style/AsciiComments
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
module ActiveModelSerializers
|
|
||||||
module Adapter
|
|
||||||
class JsonApi < Base
|
|
||||||
# {http://jsonapi.org/format/#document-jsonapi-object Jsonapi Object}
|
|
||||||
|
|
||||||
# toplevel_jsonapi
|
|
||||||
# definition:
|
|
||||||
# JSON Object
|
|
||||||
#
|
|
||||||
# properties:
|
|
||||||
# version : String
|
|
||||||
# meta
|
|
||||||
#
|
|
||||||
# description:
|
|
||||||
# An object describing the server's implementation
|
|
||||||
# structure:
|
|
||||||
# {
|
|
||||||
# version: ActiveModelSerializers.config.jsonapi_version,
|
|
||||||
# meta: ActiveModelSerializers.config.jsonapi_toplevel_meta
|
|
||||||
# }.reject! { |_, v| v.blank? }
|
|
||||||
# prs:
|
|
||||||
# https://github.com/rails-api/active_model_serializers/pull/1050
|
|
||||||
module Jsonapi
|
|
||||||
module_function
|
|
||||||
|
|
||||||
def add!(hash)
|
|
||||||
hash.merge!(object) if include_object?
|
|
||||||
end
|
|
||||||
|
|
||||||
def include_object?
|
|
||||||
ActiveModelSerializers.config.jsonapi_include_toplevel_object
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: see if we can cache this
|
|
||||||
def object
|
|
||||||
object = {
|
|
||||||
jsonapi: {
|
|
||||||
version: ActiveModelSerializers.config.jsonapi_version,
|
|
||||||
meta: ActiveModelSerializers.config.jsonapi_toplevel_meta
|
|
||||||
}
|
|
||||||
}
|
|
||||||
object[:jsonapi].reject! { |_, v| v.blank? }
|
|
||||||
|
|
||||||
object
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
module ActiveModelSerializers
|
|
||||||
module Adapter
|
|
||||||
class JsonApi
|
|
||||||
# link
|
|
||||||
# definition:
|
|
||||||
# oneOf
|
|
||||||
# linkString
|
|
||||||
# linkObject
|
|
||||||
#
|
|
||||||
# description:
|
|
||||||
# A link **MUST** be represented as either: a string containing the link's URL or a link
|
|
||||||
# object."
|
|
||||||
# structure:
|
|
||||||
# if href?
|
|
||||||
# linkString
|
|
||||||
# else
|
|
||||||
# linkObject
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# linkString
|
|
||||||
# definition:
|
|
||||||
# URI
|
|
||||||
#
|
|
||||||
# description:
|
|
||||||
# A string containing the link's URL.
|
|
||||||
# structure:
|
|
||||||
# 'http://example.com/link-string'
|
|
||||||
#
|
|
||||||
# linkObject
|
|
||||||
# definition:
|
|
||||||
# JSON Object
|
|
||||||
#
|
|
||||||
# properties:
|
|
||||||
# href (required) : URI
|
|
||||||
# meta
|
|
||||||
# structure:
|
|
||||||
# {
|
|
||||||
# href: 'http://example.com/link-object',
|
|
||||||
# meta: meta,
|
|
||||||
# }.reject! {|_,v| v.nil? }
|
|
||||||
class Link
|
|
||||||
include SerializationContext::UrlHelpers
|
|
||||||
|
|
||||||
def initialize(serializer, value)
|
|
||||||
@_routes ||= nil # handles warning
|
|
||||||
# actionpack-4.0.13/lib/action_dispatch/routing/route_set.rb:417: warning: instance variable @_routes not initialized
|
|
||||||
@object = serializer.object
|
|
||||||
@scope = serializer.scope
|
|
||||||
# Use the return value of the block unless it is nil.
|
|
||||||
if value.respond_to?(:call)
|
|
||||||
@value = instance_eval(&value)
|
|
||||||
else
|
|
||||||
@value = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def href(value)
|
|
||||||
@href = value
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def meta(value)
|
|
||||||
@meta = value
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def as_json
|
|
||||||
return @value if @value
|
|
||||||
|
|
||||||
hash = {}
|
|
||||||
hash[:href] = @href if defined?(@href)
|
|
||||||
hash[:meta] = @meta if defined?(@meta)
|
|
||||||
|
|
||||||
hash.any? ? hash : nil
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
attr_reader :object, :scope
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
module ActiveModelSerializers
|
|
||||||
module Adapter
|
|
||||||
class JsonApi
|
|
||||||
# meta
|
|
||||||
# definition:
|
|
||||||
# JSON Object
|
|
||||||
#
|
|
||||||
# description:
|
|
||||||
# Non-standard meta-information that can not be represented as an attribute or relationship.
|
|
||||||
# structure:
|
|
||||||
# {
|
|
||||||
# attitude: 'adjustable'
|
|
||||||
# }
|
|
||||||
class Meta
|
|
||||||
def initialize(serializer)
|
|
||||||
@object = serializer.object
|
|
||||||
@scope = serializer.scope
|
|
||||||
|
|
||||||
# Use the return value of the block unless it is nil.
|
|
||||||
if serializer._meta.respond_to?(:call)
|
|
||||||
@value = instance_eval(&serializer._meta)
|
|
||||||
else
|
|
||||||
@value = serializer._meta
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def as_json
|
|
||||||
@value
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
attr_reader :object, :scope
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
module ActiveModelSerializers
|
|
||||||
module Adapter
|
|
||||||
class JsonApi < Base
|
|
||||||
class PaginationLinks
|
|
||||||
MissingSerializationContextError = Class.new(KeyError)
|
|
||||||
FIRST_PAGE = 1
|
|
||||||
|
|
||||||
attr_reader :collection, :context
|
|
||||||
|
|
||||||
def initialize(collection, adapter_options)
|
|
||||||
@collection = collection
|
|
||||||
@adapter_options = adapter_options
|
|
||||||
@context = adapter_options.fetch(:serialization_context) do
|
|
||||||
fail MissingSerializationContextError, <<-EOF.freeze
|
|
||||||
JsonApi::PaginationLinks requires a ActiveModelSerializers::SerializationContext.
|
|
||||||
Please pass a ':serialization_context' option or
|
|
||||||
override CollectionSerializer#paginated? to return 'false'.
|
|
||||||
EOF
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def as_json
|
|
||||||
per_page = collection.try(:per_page) || collection.try(:limit_value) || collection.size
|
|
||||||
pages_from.each_with_object({}) do |(key, value), hash|
|
|
||||||
params = query_parameters.merge(page: { number: value, size: per_page }).to_query
|
|
||||||
|
|
||||||
hash[key] = "#{url(adapter_options)}?#{params}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
attr_reader :adapter_options
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def pages_from
|
|
||||||
return {} if collection.total_pages <= FIRST_PAGE
|
|
||||||
|
|
||||||
{}.tap do |pages|
|
|
||||||
pages[:self] = collection.current_page
|
|
||||||
|
|
||||||
unless collection.current_page == FIRST_PAGE
|
|
||||||
pages[:first] = FIRST_PAGE
|
|
||||||
pages[:prev] = collection.current_page - FIRST_PAGE
|
|
||||||
end
|
|
||||||
|
|
||||||
unless collection.current_page == collection.total_pages
|
|
||||||
pages[:next] = collection.current_page + FIRST_PAGE
|
|
||||||
pages[:last] = collection.total_pages
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def url(options)
|
|
||||||
@url ||= options.fetch(:links, {}).fetch(:self, nil) || request_url
|
|
||||||
end
|
|
||||||
|
|
||||||
def request_url
|
|
||||||
@request_url ||= context.request_url
|
|
||||||
end
|
|
||||||
|
|
||||||
def query_parameters
|
|
||||||
@query_parameters ||= context.query_parameters
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
module ActiveModelSerializers
|
|
||||||
module Adapter
|
|
||||||
class JsonApi
|
|
||||||
class Relationship
|
|
||||||
# {http://jsonapi.org/format/#document-resource-object-related-resource-links Document Resource Object Related Resource Links}
|
|
||||||
# {http://jsonapi.org/format/#document-links Document Links}
|
|
||||||
# {http://jsonapi.org/format/#document-resource-object-linkage Document Resource Relationship Linkage}
|
|
||||||
# {http://jsonapi.org/format/#document-meta Document Meta}
|
|
||||||
def initialize(parent_serializer, serializable_resource_options, association)
|
|
||||||
@parent_serializer = parent_serializer
|
|
||||||
@association = association
|
|
||||||
@serializable_resource_options = serializable_resource_options
|
|
||||||
end
|
|
||||||
|
|
||||||
def as_json
|
|
||||||
hash = {}
|
|
||||||
|
|
||||||
if association.options[:include_data]
|
|
||||||
hash[:data] = data_for(association)
|
|
||||||
end
|
|
||||||
|
|
||||||
links = links_for(association)
|
|
||||||
hash[:links] = links if links.any?
|
|
||||||
|
|
||||||
meta = meta_for(association)
|
|
||||||
hash[:meta] = meta if meta
|
|
||||||
hash[:meta] = {} if hash.empty?
|
|
||||||
|
|
||||||
hash
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
attr_reader :parent_serializer, :serializable_resource_options, :association
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def data_for(association)
|
|
||||||
serializer = association.serializer
|
|
||||||
if serializer.respond_to?(:each)
|
|
||||||
serializer.map { |s| ResourceIdentifier.new(s, serializable_resource_options).as_json }
|
|
||||||
elsif (virtual_value = association.options[:virtual_value])
|
|
||||||
virtual_value
|
|
||||||
elsif serializer && serializer.object
|
|
||||||
ResourceIdentifier.new(serializer, serializable_resource_options).as_json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def links_for(association)
|
|
||||||
association.links.each_with_object({}) do |(key, value), hash|
|
|
||||||
result = Link.new(parent_serializer, value).as_json
|
|
||||||
hash[key] = result if result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def meta_for(association)
|
|
||||||
meta = association.meta
|
|
||||||
meta.respond_to?(:call) ? parent_serializer.instance_eval(&meta) : meta
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
module ActiveModelSerializers
|
|
||||||
module Adapter
|
|
||||||
class JsonApi
|
|
||||||
class ResourceIdentifier
|
|
||||||
def self.type_for(class_name, serializer_type = nil, transform_options = {})
|
|
||||||
if serializer_type
|
|
||||||
raw_type = serializer_type
|
|
||||||
else
|
|
||||||
inflection =
|
|
||||||
if ActiveModelSerializers.config.jsonapi_resource_type == :singular
|
|
||||||
:singularize
|
|
||||||
else
|
|
||||||
:pluralize
|
|
||||||
end
|
|
||||||
|
|
||||||
raw_type = class_name.underscore
|
|
||||||
raw_type = ActiveSupport::Inflector.public_send(inflection, raw_type)
|
|
||||||
raw_type
|
|
||||||
.gsub!('/'.freeze, ActiveModelSerializers.config.jsonapi_namespace_separator)
|
|
||||||
raw_type
|
|
||||||
end
|
|
||||||
JsonApi.send(:transform_key_casing!, raw_type, transform_options)
|
|
||||||
end
|
|
||||||
|
|
||||||
# {http://jsonapi.org/format/#document-resource-identifier-objects Resource Identifier Objects}
|
|
||||||
def initialize(serializer, options)
|
|
||||||
@id = id_for(serializer)
|
|
||||||
@type = type_for(serializer, options)
|
|
||||||
end
|
|
||||||
|
|
||||||
def as_json
|
|
||||||
{ id: id, type: type }
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
attr_reader :id, :type
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def type_for(serializer, transform_options)
|
|
||||||
self.class.type_for(serializer.object.class.name, serializer._type, transform_options)
|
|
||||||
end
|
|
||||||
|
|
||||||
def id_for(serializer)
|
|
||||||
serializer.read_attribute_for_serialization(:id).to_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
module ActiveModelSerializers
|
|
||||||
module Adapter
|
|
||||||
class Null < Base
|
|
||||||
def serializable_hash(*)
|
|
||||||
{}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
# Adapted from
|
|
||||||
# https://github.com/rails/rails/blob/7f18ea14c8/activejob/lib/active_job/callbacks.rb
|
|
||||||
require 'active_support/callbacks'
|
|
||||||
|
|
||||||
module ActiveModelSerializers
|
|
||||||
# = ActiveModelSerializers Callbacks
|
|
||||||
#
|
|
||||||
# ActiveModelSerializers provides hooks during the life cycle of serialization and
|
|
||||||
# allow you to trigger logic. Available callbacks are:
|
|
||||||
#
|
|
||||||
# * <tt>around_render</tt>
|
|
||||||
#
|
|
||||||
module Callbacks
|
|
||||||
extend ActiveSupport::Concern
|
|
||||||
include ActiveSupport::Callbacks
|
|
||||||
|
|
||||||
included do
|
|
||||||
define_callbacks :render
|
|
||||||
end
|
|
||||||
|
|
||||||
# These methods will be included into any ActiveModelSerializers object, adding
|
|
||||||
# callbacks for +render+.
|
|
||||||
module ClassMethods
|
|
||||||
# Defines a callback that will get called around the render method,
|
|
||||||
# whether it is as_json, to_json, or serializable_hash
|
|
||||||
#
|
|
||||||
# class ActiveModelSerializers::SerializableResource
|
|
||||||
# include ActiveModelSerializers::Callbacks
|
|
||||||
#
|
|
||||||
# around_render do |args, block|
|
|
||||||
# tag_logger do
|
|
||||||
# notify_render do
|
|
||||||
# block.call(args)
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# def as_json
|
|
||||||
# run_callbacks :render do
|
|
||||||
# adapter.as_json
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
# # Note: So that we can re-use the instrumenter for as_json, to_json, and
|
|
||||||
# # serializable_hash, we aren't using the usual format, which would be:
|
|
||||||
# # def render(args)
|
|
||||||
# # adapter.as_json
|
|
||||||
# # end
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
def around_render(*filters, &blk)
|
|
||||||
set_callback(:render, :around, *filters, &blk)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
##
|
|
||||||
# Provides a single method +deprecate+ to be used to declare when
|
|
||||||
# something is going away.
|
|
||||||
#
|
|
||||||
# class Legacy
|
|
||||||
# def self.klass_method
|
|
||||||
# # ...
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# def instance_method
|
|
||||||
# # ...
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# extend ActiveModelSerializers::Deprecate
|
|
||||||
# deprecate :instance_method, "ActiveModelSerializers::NewPlace#new_method"
|
|
||||||
#
|
|
||||||
# class << self
|
|
||||||
# extend ActiveModelSerializers::Deprecate
|
|
||||||
# deprecate :klass_method, :none
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# Adapted from https://github.com/rubygems/rubygems/blob/1591331/lib/rubygems/deprecate.rb
|
|
||||||
module ActiveModelSerializers
|
|
||||||
module Deprecate
|
|
||||||
##
|
|
||||||
# Simple deprecation method that deprecates +name+ by wrapping it up
|
|
||||||
# in a dummy method. It warns on each call to the dummy method
|
|
||||||
# telling the user of +replacement+ (unless +replacement+ is :none) that it is planned to go away.
|
|
||||||
|
|
||||||
def deprecate(name, replacement)
|
|
||||||
old = "_deprecated_#{name}"
|
|
||||||
alias_method old, name
|
|
||||||
class_eval do
|
|
||||||
define_method(name) do |*args, &block|
|
|
||||||
target = is_a?(Module) ? "#{self}." : "#{self.class}#"
|
|
||||||
msg = ["NOTE: #{target}#{name} is deprecated",
|
|
||||||
replacement == :none ? ' with no replacement' : "; use #{replacement} instead",
|
|
||||||
"\n#{target}#{name} called from #{ActiveModelSerializers.location_of_caller.join(':')}"]
|
|
||||||
warn "#{msg.join}."
|
|
||||||
send old, *args, &block
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def delegate_and_deprecate(method, delegee)
|
|
||||||
delegate method, to: delegee
|
|
||||||
deprecate method, "#{delegee.name}."
|
|
||||||
end
|
|
||||||
|
|
||||||
module_function :deprecate
|
|
||||||
module_function :delegate_and_deprecate
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
module ActiveModelSerializers
|
|
||||||
module Deserialization
|
|
||||||
module_function
|
|
||||||
|
|
||||||
def jsonapi_parse(*args)
|
|
||||||
Adapter::JsonApi::Deserialization.parse(*args)
|
|
||||||
end
|
|
||||||
|
|
||||||
# :nocov:
|
|
||||||
def jsonapi_parse!(*args)
|
|
||||||
Adapter::JsonApi::Deserialization.parse!(*args)
|
|
||||||
end
|
|
||||||
# :nocov:
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
module ActiveModelSerializers
|
|
||||||
module JsonPointer
|
|
||||||
module_function
|
|
||||||
|
|
||||||
POINTERS = {
|
|
||||||
attribute: '/data/attributes/%s'.freeze,
|
|
||||||
primary_data: '/data%s'.freeze
|
|
||||||
}.freeze
|
|
||||||
|
|
||||||
def new(pointer_type, value = nil)
|
|
||||||
format(POINTERS[pointer_type], value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
##
|
|
||||||
# ActiveModelSerializers::Logging
|
|
||||||
#
|
|
||||||
# https://github.com/rails/rails/blob/280654ef88/activejob/lib/active_job/logging.rb
|
|
||||||
#
|
|
||||||
module ActiveModelSerializers
|
|
||||||
module Logging
|
|
||||||
RENDER_EVENT = 'render.active_model_serializers'.freeze
|
|
||||||
extend ActiveSupport::Concern
|
|
||||||
|
|
||||||
included do
|
|
||||||
include ActiveModelSerializers::Callbacks
|
|
||||||
extend Macros
|
|
||||||
instrument_rendering
|
|
||||||
end
|
|
||||||
|
|
||||||
module ClassMethods
|
|
||||||
def instrument_rendering
|
|
||||||
around_render do |args, block|
|
|
||||||
tag_logger do
|
|
||||||
notify_render do
|
|
||||||
block.call(args)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Macros that can be used to customize the logging of class or instance methods,
|
|
||||||
# by extending the class or its singleton.
|
|
||||||
#
|
|
||||||
# Adapted from:
|
|
||||||
# https://github.com/rubygems/rubygems/blob/cb28f5e991/lib/rubygems/deprecate.rb
|
|
||||||
#
|
|
||||||
# Provides a single method +notify+ to be used to declare when
|
|
||||||
# something a method notifies, with the argument +callback_name+ of the notification callback.
|
|
||||||
#
|
|
||||||
# class Adapter
|
|
||||||
# def self.klass_method
|
|
||||||
# # ...
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# def instance_method
|
|
||||||
# # ...
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# include ActiveModelSerializers::Logging::Macros
|
|
||||||
# notify :instance_method, :render
|
|
||||||
#
|
|
||||||
# class << self
|
|
||||||
# extend ActiveModelSerializers::Logging::Macros
|
|
||||||
# notify :klass_method, :render
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
module Macros
|
|
||||||
##
|
|
||||||
# Simple notify method that wraps up +name+
|
|
||||||
# in a dummy method. It notifies on with the +callback_name+ notifier on
|
|
||||||
# each call to the dummy method, telling what the current serializer and adapter
|
|
||||||
# are being rendered.
|
|
||||||
# Adapted from:
|
|
||||||
# https://github.com/rubygems/rubygems/blob/cb28f5e991/lib/rubygems/deprecate.rb
|
|
||||||
def notify(name, callback_name)
|
|
||||||
class_eval do
|
|
||||||
old = "_notifying_#{callback_name}_#{name}"
|
|
||||||
alias_method old, name
|
|
||||||
define_method name do |*args, &block|
|
|
||||||
run_callbacks callback_name do
|
|
||||||
send old, *args, &block
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def notify_render(*)
|
|
||||||
event_name = RENDER_EVENT
|
|
||||||
ActiveSupport::Notifications.instrument(event_name, notify_render_payload) do
|
|
||||||
yield
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def notify_render_payload
|
|
||||||
{
|
|
||||||
serializer: serializer || ActiveModel::Serializer::Null,
|
|
||||||
adapter: adapter || ActiveModelSerializers::Adapter::Null
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def tag_logger(*tags)
|
|
||||||
if ActiveModelSerializers.logger.respond_to?(:tagged)
|
|
||||||
tags.unshift 'active_model_serializers'.freeze unless logger_tagged_by_active_model_serializers?
|
|
||||||
ActiveModelSerializers.logger.tagged(*tags) { yield }
|
|
||||||
else
|
|
||||||
yield
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def logger_tagged_by_active_model_serializers?
|
|
||||||
ActiveModelSerializers.logger.formatter.current_tags.include?('active_model_serializers'.freeze)
|
|
||||||
end
|
|
||||||
|
|
||||||
class LogSubscriber < ActiveSupport::LogSubscriber
|
|
||||||
def render(event)
|
|
||||||
info do
|
|
||||||
serializer = event.payload[:serializer]
|
|
||||||
adapter = event.payload[:adapter]
|
|
||||||
duration = event.duration.round(2)
|
|
||||||
"Rendered #{serializer.name} with #{adapter.class} (#{duration}ms)"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def logger
|
|
||||||
ActiveModelSerializers.logger
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
ActiveModelSerializers::Logging::LogSubscriber.attach_to :active_model_serializers
|
|
||||||
@ -1,80 +0,0 @@
|
|||||||
module ActiveModelSerializers
|
|
||||||
module LookupChain
|
|
||||||
# Standard appending of Serializer to the resource name.
|
|
||||||
#
|
|
||||||
# Example:
|
|
||||||
# Author => AuthorSerializer
|
|
||||||
BY_RESOURCE = lambda do |resource_class, _serializer_class, _namespace|
|
|
||||||
serializer_from(resource_class)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Uses the namespace of the resource to find the serializer
|
|
||||||
#
|
|
||||||
# Example:
|
|
||||||
# British::Author => British::AuthorSerializer
|
|
||||||
BY_RESOURCE_NAMESPACE = lambda do |resource_class, _serializer_class, _namespace|
|
|
||||||
resource_namespace = namespace_for(resource_class)
|
|
||||||
serializer_name = serializer_from(resource_class)
|
|
||||||
|
|
||||||
"#{resource_namespace}::#{serializer_name}"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Uses the controller namespace of the resource to find the serializer
|
|
||||||
#
|
|
||||||
# Example:
|
|
||||||
# Api::V3::AuthorsController => Api::V3::AuthorSerializer
|
|
||||||
BY_NAMESPACE = lambda do |resource_class, _serializer_class, namespace|
|
|
||||||
resource_name = resource_class_name(resource_class)
|
|
||||||
namespace ? "#{namespace}::#{resource_name}Serializer" : nil
|
|
||||||
end
|
|
||||||
|
|
||||||
# Allows for serializers to be defined in parent serializers
|
|
||||||
# - useful if a relationship only needs a different set of attributes
|
|
||||||
# than if it were rendered independently.
|
|
||||||
#
|
|
||||||
# Example:
|
|
||||||
# class BlogSerializer < ActiveModel::Serializer
|
|
||||||
# class AuthorSerialier < ActiveModel::Serializer
|
|
||||||
# ...
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# belongs_to :author
|
|
||||||
# ...
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# The belongs_to relationship would be rendered with
|
|
||||||
# BlogSerializer::AuthorSerialier
|
|
||||||
BY_PARENT_SERIALIZER = lambda do |resource_class, serializer_class, _namespace|
|
|
||||||
return if serializer_class == ActiveModel::Serializer
|
|
||||||
|
|
||||||
serializer_name = serializer_from(resource_class)
|
|
||||||
"#{serializer_class}::#{serializer_name}"
|
|
||||||
end
|
|
||||||
|
|
||||||
DEFAULT = [
|
|
||||||
BY_PARENT_SERIALIZER,
|
|
||||||
BY_NAMESPACE,
|
|
||||||
BY_RESOURCE_NAMESPACE,
|
|
||||||
BY_RESOURCE
|
|
||||||
].freeze
|
|
||||||
|
|
||||||
module_function
|
|
||||||
|
|
||||||
def namespace_for(klass)
|
|
||||||
klass.name.deconstantize
|
|
||||||
end
|
|
||||||
|
|
||||||
def resource_class_name(klass)
|
|
||||||
klass.name.demodulize
|
|
||||||
end
|
|
||||||
|
|
||||||
def serializer_from_resource_name(name)
|
|
||||||
"#{name}Serializer"
|
|
||||||
end
|
|
||||||
|
|
||||||
def serializer_from(klass)
|
|
||||||
name = resource_class_name(klass)
|
|
||||||
serializer_from_resource_name(name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,129 +0,0 @@
|
|||||||
# ActiveModelSerializers::Model is a convenient superclass for making your models
|
|
||||||
# from Plain-Old Ruby Objects (PORO). It also serves as a reference implementation
|
|
||||||
# that satisfies ActiveModel::Serializer::Lint::Tests.
|
|
||||||
module ActiveModelSerializers
|
|
||||||
class Model
|
|
||||||
include ActiveModel::Serializers::JSON
|
|
||||||
include ActiveModel::Model
|
|
||||||
|
|
||||||
# Declare names of attributes to be included in +attributes+ hash.
|
|
||||||
# Is only available as a class-method since the ActiveModel::Serialization mixin in Rails
|
|
||||||
# uses an +attribute_names+ local variable, which may conflict if we were to add instance methods here.
|
|
||||||
#
|
|
||||||
# @overload attribute_names
|
|
||||||
# @return [Array<Symbol>]
|
|
||||||
class_attribute :attribute_names, instance_writer: false, instance_reader: false
|
|
||||||
# Initialize +attribute_names+ for all subclasses. The array is usually
|
|
||||||
# mutated in the +attributes+ method, but can be set directly, as well.
|
|
||||||
self.attribute_names = []
|
|
||||||
|
|
||||||
# Easily declare instance attributes with setters and getters for each.
|
|
||||||
#
|
|
||||||
# To initialize an instance, all attributes must have setters.
|
|
||||||
# However, the hash returned by +attributes+ instance method will ALWAYS
|
|
||||||
# be the value of the initial attributes, regardless of what accessors are defined.
|
|
||||||
# The only way to change the change the attributes after initialization is
|
|
||||||
# to mutate the +attributes+ directly.
|
|
||||||
# Accessor methods do NOT mutate the attributes. (This is a bug).
|
|
||||||
#
|
|
||||||
# @note For now, the Model only supports the notion of 'attributes'.
|
|
||||||
# In the tests, there is a special Model that also supports 'associations'. This is
|
|
||||||
# important so that we can add accessors for values that should not appear in the
|
|
||||||
# attributes hash when modeling associations. It is not yet clear if it
|
|
||||||
# makes sense for a PORO to have associations outside of the tests.
|
|
||||||
#
|
|
||||||
# @overload attributes(names)
|
|
||||||
# @param names [Array<String, Symbol>]
|
|
||||||
# @param name [String, Symbol]
|
|
||||||
def self.attributes(*names)
|
|
||||||
self.attribute_names |= names.map(&:to_sym)
|
|
||||||
# Silence redefinition of methods warnings
|
|
||||||
ActiveModelSerializers.silence_warnings do
|
|
||||||
attr_accessor(*names)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Opt-in to breaking change
|
|
||||||
def self.derive_attributes_from_names_and_fix_accessors
|
|
||||||
unless included_modules.include?(DeriveAttributesFromNamesAndFixAccessors)
|
|
||||||
prepend(DeriveAttributesFromNamesAndFixAccessors)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module DeriveAttributesFromNamesAndFixAccessors
|
|
||||||
def self.included(base)
|
|
||||||
# NOTE that +id+ will always be in +attributes+.
|
|
||||||
base.attributes :id
|
|
||||||
end
|
|
||||||
|
|
||||||
# Override the +attributes+ method so that the hash is derived from +attribute_names+.
|
|
||||||
#
|
|
||||||
# The fields in +attribute_names+ determines the returned hash.
|
|
||||||
# +attributes+ are returned frozen to prevent any expectations that mutation affects
|
|
||||||
# the actual values in the model.
|
|
||||||
def attributes
|
|
||||||
self.class.attribute_names.each_with_object({}) do |attribute_name, result|
|
|
||||||
result[attribute_name] = public_send(attribute_name).freeze
|
|
||||||
end.with_indifferent_access.freeze
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Support for validation and other ActiveModel::Errors
|
|
||||||
# @return [ActiveModel::Errors]
|
|
||||||
attr_reader :errors
|
|
||||||
|
|
||||||
# (see #updated_at)
|
|
||||||
attr_writer :updated_at
|
|
||||||
|
|
||||||
# The only way to change the attributes of an instance is to directly mutate the attributes.
|
|
||||||
# @example
|
|
||||||
#
|
|
||||||
# model.attributes[:foo] = :bar
|
|
||||||
# @return [Hash]
|
|
||||||
attr_reader :attributes
|
|
||||||
|
|
||||||
# @param attributes [Hash]
|
|
||||||
def initialize(attributes = {})
|
|
||||||
attributes ||= {} # protect against nil
|
|
||||||
@attributes = attributes.symbolize_keys.with_indifferent_access
|
|
||||||
@errors = ActiveModel::Errors.new(self)
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
# Defaults to the downcased model name.
|
|
||||||
# This probably isn't a good default, since it's not a unique instance identifier,
|
|
||||||
# but that's what is currently implemented \_('-')_/.
|
|
||||||
#
|
|
||||||
# @note Though +id+ is defined, it will only show up
|
|
||||||
# in +attributes+ when it is passed in to the initializer or added to +attributes+,
|
|
||||||
# such as <tt>attributes[:id] = 5</tt>.
|
|
||||||
# @return [String, Numeric, Symbol]
|
|
||||||
def id
|
|
||||||
attributes.fetch(:id) do
|
|
||||||
defined?(@id) ? @id : self.class.model_name.name && self.class.model_name.name.downcase
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# When not set, defaults to the time the file was modified.
|
|
||||||
#
|
|
||||||
# @note Though +updated_at+ and +updated_at=+ are defined, it will only show up
|
|
||||||
# in +attributes+ when it is passed in to the initializer or added to +attributes+,
|
|
||||||
# such as <tt>attributes[:updated_at] = Time.current</tt>.
|
|
||||||
# @return [String, Numeric, Time]
|
|
||||||
def updated_at
|
|
||||||
attributes.fetch(:updated_at) do
|
|
||||||
defined?(@updated_at) ? @updated_at : File.mtime(__FILE__)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# To customize model behavior, this method must be redefined. However,
|
|
||||||
# there are other ways of setting the +cache_key+ a serializer uses.
|
|
||||||
# @return [String]
|
|
||||||
def cache_key
|
|
||||||
ActiveSupport::Cache.expand_cache_key([
|
|
||||||
self.class.model_name.name.downcase,
|
|
||||||
"#{id}-#{updated_at.strftime('%Y%m%d%H%M%S%9N')}"
|
|
||||||
].compact)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
require 'rails/railtie'
|
|
||||||
require 'action_controller'
|
|
||||||
require 'action_controller/railtie'
|
|
||||||
require 'action_controller/serialization'
|
|
||||||
|
|
||||||
module ActiveModelSerializers
|
|
||||||
class Railtie < Rails::Railtie
|
|
||||||
config.to_prepare do
|
|
||||||
ActiveModel::Serializer.serializers_cache.clear
|
|
||||||
end
|
|
||||||
|
|
||||||
initializer 'active_model_serializers.action_controller' do
|
|
||||||
ActiveSupport.on_load(:action_controller) do
|
|
||||||
include(::ActionController::Serialization)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
initializer 'active_model_serializers.prepare_serialization_context' do
|
|
||||||
SerializationContext.url_helpers = Rails.application.routes.url_helpers
|
|
||||||
SerializationContext.default_url_options = Rails.application.routes.default_url_options
|
|
||||||
end
|
|
||||||
|
|
||||||
# This hook is run after the action_controller railtie has set the configuration
|
|
||||||
# based on the *environment* configuration and before any config/initializers are run
|
|
||||||
# and also before eager_loading (if enabled).
|
|
||||||
initializer 'active_model_serializers.set_configs', after: 'action_controller.set_configs' do
|
|
||||||
ActiveModelSerializers.logger = Rails.configuration.action_controller.logger
|
|
||||||
ActiveModelSerializers.config.perform_caching = Rails.configuration.action_controller.perform_caching
|
|
||||||
# We want this hook to run after the config has been set, even if ActionController has already loaded.
|
|
||||||
ActiveSupport.on_load(:action_controller) do
|
|
||||||
ActiveModelSerializers.config.cache_store = ActionController::Base.cache_store
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# :nocov:
|
|
||||||
generators do |app|
|
|
||||||
Rails::Generators.configure!(app.config.generators)
|
|
||||||
Rails::Generators.hidden_namespaces.uniq!
|
|
||||||
require 'generators/rails/resource_override'
|
|
||||||
end
|
|
||||||
# :nocov:
|
|
||||||
|
|
||||||
if Rails.env.test?
|
|
||||||
ActionController::TestCase.send(:include, ActiveModelSerializers::Test::Schema)
|
|
||||||
ActionController::TestCase.send(:include, ActiveModelSerializers::Test::Serializer)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
# Based on discussion in https://github.com/rails/rails/pull/23712#issuecomment-184977238,
|
|
||||||
# the JSON API media type will have its own format/renderer.
|
|
||||||
#
|
|
||||||
# > We recommend the media type be registered on its own as jsonapi
|
|
||||||
# when a jsonapi Renderer and deserializer (Http::Parameters::DEFAULT_PARSERS) are added.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
#
|
|
||||||
# ActiveSupport.on_load(:action_controller) do
|
|
||||||
# require 'active_model_serializers/register_jsonapi_renderer'
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# And then in controllers, use `render jsonapi: model` rather than `render json: model, adapter: :json_api`.
|
|
||||||
#
|
|
||||||
# For example, in a controller action, we can:
|
|
||||||
# respond_to do |format|
|
|
||||||
# format.jsonapi { render jsonapi: model }
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# or
|
|
||||||
#
|
|
||||||
# render jsonapi: model
|
|
||||||
#
|
|
||||||
# No wrapper format needed as it does not apply (i.e. no `wrap_parameters format: [jsonapi]`)
|
|
||||||
module ActiveModelSerializers
|
|
||||||
module Jsonapi
|
|
||||||
MEDIA_TYPE = 'application/vnd.api+json'.freeze
|
|
||||||
HEADERS = {
|
|
||||||
response: { 'CONTENT_TYPE'.freeze => MEDIA_TYPE },
|
|
||||||
request: { 'ACCEPT'.freeze => MEDIA_TYPE }
|
|
||||||
}.freeze
|
|
||||||
|
|
||||||
def self.install
|
|
||||||
# actionpack/lib/action_dispatch/http/mime_types.rb
|
|
||||||
Mime::Type.register MEDIA_TYPE, :jsonapi
|
|
||||||
|
|
||||||
if Rails::VERSION::MAJOR >= 5
|
|
||||||
ActionDispatch::Request.parameter_parsers[:jsonapi] = parser
|
|
||||||
else
|
|
||||||
ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime[:jsonapi]] = parser
|
|
||||||
end
|
|
||||||
|
|
||||||
# ref https://github.com/rails/rails/pull/21496
|
|
||||||
ActionController::Renderers.add :jsonapi do |json, options|
|
|
||||||
json = serialize_jsonapi(json, options).to_json(options) unless json.is_a?(String)
|
|
||||||
self.content_type ||= Mime[:jsonapi]
|
|
||||||
self.response_body = json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Proposal: should actually deserialize the JSON API params
|
|
||||||
# to the hash format expected by `ActiveModel::Serializers::JSON`
|
|
||||||
# actionpack/lib/action_dispatch/http/parameters.rb
|
|
||||||
def self.parser
|
|
||||||
lambda do |body|
|
|
||||||
data = JSON.parse(body)
|
|
||||||
data = { _json: data } unless data.is_a?(Hash)
|
|
||||||
data.with_indifferent_access
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module ControllerSupport
|
|
||||||
def serialize_jsonapi(json, options)
|
|
||||||
options[:adapter] = :json_api
|
|
||||||
options.fetch(:serialization_context) do
|
|
||||||
options[:serialization_context] = ActiveModelSerializers::SerializationContext.new(request)
|
|
||||||
end
|
|
||||||
get_serializer(json, options)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
ActiveModelSerializers::Jsonapi.install
|
|
||||||
|
|
||||||
ActiveSupport.on_load(:action_controller) do
|
|
||||||
include ActiveModelSerializers::Jsonapi::ControllerSupport
|
|
||||||
end
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user