json_api_server 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.dockerignore +7 -0
  3. data/.gitignore +14 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +35 -0
  6. data/.travis.yml +5 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Dockerfile +9 -0
  9. data/Gemfile +10 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +432 -0
  12. data/Rakefile +6 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +8 -0
  15. data/config/locales/en.yml +32 -0
  16. data/docker-compose.yml +10 -0
  17. data/json_api_server.gemspec +50 -0
  18. data/lib/json_api_server.rb +76 -0
  19. data/lib/json_api_server/api_version.rb +8 -0
  20. data/lib/json_api_server/attributes_builder.rb +135 -0
  21. data/lib/json_api_server/base_serializer.rb +169 -0
  22. data/lib/json_api_server/builder.rb +201 -0
  23. data/lib/json_api_server/cast.rb +89 -0
  24. data/lib/json_api_server/configuration.rb +99 -0
  25. data/lib/json_api_server/controller/error_handling.rb +164 -0
  26. data/lib/json_api_server/engine.rb +5 -0
  27. data/lib/json_api_server/error.rb +64 -0
  28. data/lib/json_api_server/errors.rb +50 -0
  29. data/lib/json_api_server/exceptions.rb +6 -0
  30. data/lib/json_api_server/fields.rb +76 -0
  31. data/lib/json_api_server/filter.rb +255 -0
  32. data/lib/json_api_server/filter_builders.rb +135 -0
  33. data/lib/json_api_server/filter_config.rb +71 -0
  34. data/lib/json_api_server/filter_parser.rb +88 -0
  35. data/lib/json_api_server/include.rb +158 -0
  36. data/lib/json_api_server/meta_builder.rb +39 -0
  37. data/lib/json_api_server/mime_types.rb +21 -0
  38. data/lib/json_api_server/pagination.rb +189 -0
  39. data/lib/json_api_server/paginator.rb +134 -0
  40. data/lib/json_api_server/relationships_builder.rb +215 -0
  41. data/lib/json_api_server/resource_serializer.rb +245 -0
  42. data/lib/json_api_server/resources_serializer.rb +131 -0
  43. data/lib/json_api_server/serializer.rb +34 -0
  44. data/lib/json_api_server/sort.rb +156 -0
  45. data/lib/json_api_server/sort_configs.rb +63 -0
  46. data/lib/json_api_server/validation_errors.rb +51 -0
  47. data/lib/json_api_server/version.rb +3 -0
  48. metadata +259 -0
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'json_api_server'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,32 @@
1
+ en:
2
+ json_api_server:
3
+ variables:
4
+ defaults:
5
+ name: 'resource'
6
+ render_400:
7
+ title: 'Bad Request'
8
+ detail: "You've made an invalid request."
9
+ inclusion: "Inclusion param '%{param}' is not supported."
10
+ filter: "Filter param '%{param}' is not supported."
11
+ sort: "Sort param '%{param}' is not supported."
12
+ render_401:
13
+ title: 'Unauthorized'
14
+ detail: 'Authentication failed.'
15
+ render_403:
16
+ title: 'Forbidden'
17
+ detail: 'Unauthorized to access this resource or perform this action.'
18
+ render_404:
19
+ title: 'Not Found'
20
+ detail: "This %{name} does not exist."
21
+ render_409:
22
+ title: 'Conflict'
23
+ detail: "This %{name} already exists."
24
+ render_500:
25
+ title: 'Internal Server Error'
26
+ detail: 'The server encountered an unexpected error.'
27
+ render_503:
28
+ title: 'Service Unavailable'
29
+ detail: 'The service is unavailable. It may be under maintenance or preparing for maintenance.'
30
+ render_unknown_format:
31
+ title: 'Unknown Format'
32
+ detail: "Format %{name} is not supported for this endpoint."
@@ -0,0 +1,10 @@
1
+ # docker-compose run --rm gem
2
+ version: '2'
3
+ services:
4
+ gem:
5
+ build:
6
+ context: .
7
+ dockerfile: Dockerfile
8
+ volumes:
9
+ - .:/home/gems/mygem
10
+ entrypoint: /bin/bash
@@ -0,0 +1,50 @@
1
+ # coding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'json_api_server/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'json_api_server'
9
+ spec.version = JsonApiServer::VERSION
10
+ spec.authors = ['ed.mare']
11
+
12
+ spec.summary = 'Implements JSON API 1.0 - data, errors, meta, pagination, sort, ' \
13
+ 'filters, sparse fieldsets and inclusion of related resources.'
14
+ spec.description = 'For building JSON APIs.'
15
+ spec.homepage = 'https://github.com/ed-mare/json_api_server'
16
+ spec.license = 'MIT'
17
+
18
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
19
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
20
+ if spec.respond_to?(:metadata)
21
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
22
+ else
23
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
24
+ 'public gem pushes.'
25
+ end
26
+
27
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
28
+ f.match(%r{^(test|spec|features)/})
29
+ end
30
+ spec.bindir = 'exe'
31
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ['lib']
33
+ spec.required_ruby_version = '>= 2.1'
34
+
35
+ spec.rdoc_options += ['--main', 'README.md', '--exclude', 'spec', '--exclude', 'bin', '--exclude', 'Dockerfile',
36
+ '--exclude', 'Gemfile', '--exclude', 'Gemfile.lock', '--exclude', 'Rakefile']
37
+
38
+ spec.add_dependency 'oj', '~> 3.0'
39
+ spec.add_dependency 'railties', '>= 5.0'
40
+ spec.add_dependency 'activesupport', '>= 5.0'
41
+ spec.add_dependency 'will_paginate', '~> 3.1.0' # for Rails 3+, Sinatra, and Merb
42
+
43
+ spec.add_development_dependency 'bundler', '>= 1.13'
44
+ spec.add_development_dependency 'sqlite3'
45
+ spec.add_development_dependency 'rake', '>= 10.0'
46
+ spec.add_development_dependency 'rspec', '>= 3.0'
47
+ spec.add_development_dependency 'rails', '>= 4.1'
48
+ spec.add_development_dependency 'rspec-rails', '>= 3.5'
49
+ spec.add_development_dependency 'rubocop', '>= 0.47'
50
+ end
@@ -0,0 +1,76 @@
1
+ require 'oj'
2
+ require 'will_paginate'
3
+ require 'logger'
4
+ require 'active_support/core_ext/module/delegation'
5
+ require 'active_support/core_ext/object/blank'
6
+ require 'active_support/core_ext/object/try'
7
+ require 'active_support/core_ext/hash'
8
+ require 'active_support/inflector'
9
+
10
+ require 'json_api_server/version'
11
+ require 'json_api_server/api_version'
12
+ require 'json_api_server/exceptions'
13
+ require 'json_api_server/filter_builders'
14
+ require 'json_api_server/configuration'
15
+ require 'json_api_server/meta_builder'
16
+ require 'json_api_server/serializer'
17
+ require 'json_api_server/base_serializer'
18
+ require 'json_api_server/resource_serializer'
19
+ require 'json_api_server/resources_serializer'
20
+ require 'json_api_server/attributes_builder'
21
+ require 'json_api_server/relationships_builder'
22
+ require 'json_api_server/error'
23
+ require 'json_api_server/errors'
24
+ require 'json_api_server/validation_errors'
25
+ require 'json_api_server/paginator'
26
+ require 'json_api_server/pagination'
27
+ require 'json_api_server/sort_configs'
28
+ require 'json_api_server/sort'
29
+ require 'json_api_server/fields'
30
+ require 'json_api_server/cast'
31
+ require 'json_api_server/filter_config'
32
+ require 'json_api_server/filter_parser'
33
+ require 'json_api_server/filter'
34
+ require 'json_api_server/include'
35
+ require 'json_api_server/builder'
36
+
37
+ if defined?(Rails)
38
+ require 'json_api_server/engine'
39
+ require 'json_api_server/mime_types'
40
+ require 'json_api_server/controller/error_handling'
41
+
42
+ # https://github.com/ohler55/oj/blob/master/pages/Rails.md
43
+ # gem 'oj_mimic_json' # we need this for Rails 4.1.x
44
+ Oj.optimize_rails if Oj.respond_to?(:optimize_rails) # rails 5 but also rails 4?
45
+ end
46
+
47
+ module JsonApiServer # :nodoc:
48
+ class << self
49
+ attr_writer :configuration
50
+
51
+ def configuration
52
+ @configuration ||= JsonApiServer::Configuration.new
53
+ end
54
+
55
+ def configure
56
+ yield configuration
57
+ end
58
+
59
+ def errors(errors)
60
+ JsonApiServer::Errors.new(errors)
61
+ end
62
+
63
+ def validation_errors(model)
64
+ JsonApiServer::ValidationErrors.new(model)
65
+ end
66
+
67
+ def paginator(current_page, total_pages, per_page, base_url, params = {})
68
+ JsonApiServer::Paginator.new(current_page, total_pages, per_page, base_url, params)
69
+ end
70
+
71
+ # Convenience method to JsonApiServer.configuration.logger.
72
+ def logger
73
+ JsonApiServer.configuration.logger
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,8 @@
1
+ module JsonApiServer # :nodoc:
2
+ # JSON API version number. Currently 1.0.
3
+ module ApiVersion
4
+ def jsonapi
5
+ { 'version' => '1.0' }
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,135 @@
1
+ module JsonApiServer # :nodoc:
2
+ # Related to sparse fieldsets. http://jsonapi.org/format/#fetching-sparse-fieldsets
3
+ # "A client MAY request that an endpoint return only specific fields in the response on a per-type
4
+ # basis by including a fields[TYPE] parameter."
5
+ #
6
+ # Use this class to build the <tt>attributes</tt> section in JSON API
7
+ # serializers. It will only add attributes defined in #fields (sparse fieldset).
8
+ # If #fields is nil (no requested sparse fieldset), it will add all attributes.
9
+ #
10
+ # ==== Examples
11
+ #
12
+ # This:
13
+ # /articles?include=author&fields[articles]=title,body,phone&fields[people]=name
14
+ #
15
+ # converts to:
16
+ # {'articles' => ['title', 'body', 'phone'], 'people' => ['name']}
17
+ #
18
+ # When <tt>fields</tt> is an array, only fields in the array are added:
19
+ #
20
+ # AttributesBuilder.new(['title', 'body', 'phone'])
21
+ # .add('title', @record.title)
22
+ # .add('body', @record.body)
23
+ # .add_if('phone', @record.phone, -> { admin? }) # conditionally adding
24
+ # .add('isbn', @record.isbn) # not in sparse fields array
25
+ # .attributes
26
+ #
27
+ # # when non-admin
28
+ # # => {
29
+ # # 'title' => 'Everyone Poops',
30
+ # # 'body' => 'Taro Gomi'
31
+ # # }
32
+ #
33
+ # # or...
34
+ #
35
+ # # when admin
36
+ # # => {
37
+ # # 'title' => 'Everyone Poops',
38
+ # # 'body' => 'Taro Gomi',
39
+ # # 'phone' => '123-4567',
40
+ # # }
41
+ #
42
+ # When <tt>fields</tt> is nil, all attributes are added.
43
+ #
44
+ # AttributesBuilder.new
45
+ # .add_multi(@record, 'title', 'body')
46
+ # .add_if('phone', @record.phone, -> { admin? }) # conditionally adding
47
+ # .add('isbn', @record.isbn)
48
+ # .attributes
49
+ #
50
+ # # when non-admin
51
+ # # => {
52
+ # # 'title' => 'Everyone Poops',
53
+ # # 'body' => 'Taro Gomi',
54
+ # # 'isbn' => '5555555'
55
+ # # }
56
+ #
57
+ # # or...
58
+ #
59
+ # # when admin
60
+ # # => {
61
+ # # 'title' => 'Everyone Poops',
62
+ # # 'body' => 'Taro Gomi',
63
+ # # 'phone' => '123-4567',
64
+ # # 'isbn' => '5555555'
65
+ # # }
66
+ #
67
+ class AttributesBuilder
68
+ # (Array or nil) fields (sparse fieldset) array passed in initialize.
69
+ attr_reader :fields
70
+
71
+ # * <tt>fields</tt> - Array of fields to display for a type. Defaults to nil. When nil, all fields are permitted.
72
+ def initialize(fields = nil)
73
+ @hash = {}
74
+ @fields = fields
75
+ @fields.map!(&:to_s) if @fields.respond_to?(:map)
76
+ end
77
+
78
+ # Adds attribute if attribute name is in <tt>fields</tt> array.
79
+ #
80
+ # i.e,
81
+ #
82
+ # JsonApiServer::AttributesBuilder.new(fields)
83
+ # .add('name', @object.name)
84
+ # .attributes
85
+ def add(name, value)
86
+ @hash[name.to_s] = value if add_attr?(name)
87
+ self
88
+ end
89
+
90
+ # Add multiple attributes.
91
+ #
92
+ # i.e,
93
+ #
94
+ # JsonApiServer::AttributesBuilder.new(fields)
95
+ # .add_multi(@object, 'name', 'email', 'logins')
96
+ # .attributes
97
+ def add_multi(object, *attrs)
98
+ attrs.each { |attr| add(attr, object.send(attr)) }
99
+ self
100
+ end
101
+
102
+ # Adds attribute if attribute name is in <tt>fields</tt> array *and* proc returns true.
103
+ #
104
+ # i.e,
105
+ #
106
+ # JsonApiServer::AttributesBuilder.new(fields)
107
+ # .add_if('email', @object.email, -> { admin? })
108
+ # .attributes
109
+ def add_if(name, value, proc)
110
+ @hash[name] = value if add_attr?(name) && proc.call == true
111
+ self
112
+ end
113
+
114
+ # Returns attributes as a hash.
115
+ #
116
+ # i.e.,
117
+ # {
118
+ # 'title' => 'Everyone Poops',
119
+ # 'body' => 'Taro Gomi',
120
+ # 'phone' => '123-4567',
121
+ # 'isbn' => '5555555'
122
+ # }
123
+ def attributes
124
+ @hash
125
+ end
126
+
127
+ protected
128
+
129
+ # Returns true if @fields is not defined. If @fields is defined,
130
+ # returns true if attribute name is included in the @fields array.
131
+ def add_attr?(name)
132
+ @fields.respond_to?(:include?) ? @fields.include?(name.to_s) : true
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,169 @@
1
+ module JsonApiServer # :nodoc:
2
+ # === Description
3
+ #
4
+ # Base JSON API serializer for this gem. Based on spec document structure:
5
+ # http://jsonapi.org/format/#document-structure. Classes should inherit and override.
6
+ # ResourceSerializer and ResourcesSerializer inherit from this class.
7
+ #
8
+ # Consists of 4 methods (#links, #data, #included, #meta) which create
9
+ # the following document structure. The 4 methods should return data
10
+ # that is serializable to JSON.
11
+ #
12
+ # {
13
+ # ":jsonapi": {
14
+ # ":version": "1.0"
15
+ # },
16
+ # ":links": null,
17
+ # ":data": null,
18
+ # ":included": null,
19
+ # ":meta": null
20
+ # }
21
+ #
22
+ # There is an additional method, #relationship_data, which should be
23
+ # populated with data for "relationships".
24
+ #
25
+ # Example class:
26
+ #
27
+ # class CommentSerializer < JsonApiServer::BaseSerializer
28
+ # def initialize(object, **options)
29
+ # super(options)
30
+ # @object = object
31
+ # end
32
+ #
33
+ # def links
34
+ # {
35
+ # self: File.join(base_url, "/comments/#{@object.id}")
36
+ # }
37
+ # end
38
+ #
39
+ # def data
40
+ # {
41
+ # "type": "comments",
42
+ # "id": "12",
43
+ # "attributes": {
44
+ # "comment": @object.comment,
45
+ # "created_at": @object.created_at,
46
+ # "updated_at": @object.created_at,
47
+ # },
48
+ # "relationships": {
49
+ # "author": {
50
+ # "links": {"self": "http://example.com/people/#{@object.author_id}"},
51
+ # "data": {"id": @object.author_id, "type": "people"}
52
+ # }
53
+ # }
54
+ # }
55
+ # end
56
+ # end
57
+ #
58
+ # Sometimes only part of document is needed, i.e., when embedding one serializer in another.
59
+ # <tt>as_json</tt> takes an optional hash argument which determines which parts of the document to return.
60
+ # These options can also be set in the #as_json_options attribute.
61
+ #
62
+ # serializer.as_json(include: [:data]) # => { data: {...} }
63
+ # serializer.as_json(include: [:links]) # => { links: {...} }
64
+ # serializer.as_json(include: [:data, :links]) # =>
65
+ # # {
66
+ # # links: {...},
67
+ # # data: {...}
68
+ # # }
69
+ # serializer.as_json(include: [:relationship_data]) # =>
70
+ # # {
71
+ # # data: {
72
+ # # # usually links or object_id + type.
73
+ # # }
74
+ # # }
75
+ #
76
+ # <tt>base_url</tt> -- is JsonApiServer::Configuration#base_url exposed as a protected
77
+ # method. For creating links.
78
+ class BaseSerializer
79
+ include JsonApiServer::Serializer
80
+ include JsonApiServer::ApiVersion
81
+
82
+ # Hash. as_json options. Same options can be passed into #as_json.
83
+ # Defaults to nil. When not set, all sections are rendered.
84
+ #
85
+ # Possible options:
86
+ #
87
+ # <tt>:include</tt> (Array) -- Optional. Possible values: :jsonapi, :links, :data,
88
+ # :included, :meta and :relationship_data. :relationship_data is a special case --
89
+ # if present in the array, only relationship data is rendered (data section w/o
90
+ # attributes).
91
+ #
92
+ # i.e,
93
+ #
94
+ # # Set attribute
95
+ # serializer.as_json_options = { include: [:data] }
96
+ # serializer.as_json_options = { include: [:data, :links] }
97
+ # serializer.as_json_options = { include: [:relationship_data] }
98
+ #
99
+ # # Or set when calling #as_json
100
+ # serializer.as_json(include: [:data])
101
+ attr_accessor :as_json_options
102
+
103
+ def initialize(**options)
104
+ @as_json_options = options[:as_json_options]
105
+ end
106
+
107
+ # JSON API *links* section. Subclass implements.
108
+ # Api spec: http://jsonapi.org/format/#document-links
109
+ def links
110
+ nil
111
+ end
112
+
113
+ # JSON API *data* section. Subclass implements.
114
+ # Api spec: http://jsonapi.org/format/#document-structure
115
+ def data
116
+ nil
117
+ end
118
+
119
+ # JSON to render with #as_json_option :relationship_data. Subclass implements.
120
+ # Api spec: http://jsonapi.org/format/#fetching-relationships
121
+ def relationship_data
122
+ nil
123
+ end
124
+
125
+ # JSON API *included* section. Sublclass implements.
126
+ # Api spec: http://jsonapi.org/format/#fetching-includes
127
+ def included
128
+ nil
129
+ end
130
+
131
+ # JSON API *meta* section. Sublclass implements.
132
+ # Api spec: http://jsonapi.org/format/#document-meta
133
+ def meta
134
+ nil
135
+ end
136
+
137
+ # Creates the following document structure by default. See #as_json_options for
138
+ # a description of options. The hash is with indifferent access.
139
+ # {
140
+ # "jsonapi" => {
141
+ # "version" => "1.0"
142
+ # },
143
+ # "links" => null,
144
+ # "data" => null,
145
+ # "included" => null,
146
+ # "meta" => null
147
+ # }
148
+ def as_json(**options)
149
+ opts = (options.any? ? options : as_json_options) || {}
150
+ sections = opts[:include] || %w[jsonapi links data included meta]
151
+ hash = {}
152
+
153
+ if sections.include?(:relationship_data)
154
+ hash['data'] = relationship_data
155
+ else
156
+ sections.each { |s| hash[s] = send(s) if sections.include?(s) }
157
+ end
158
+
159
+ ActiveSupport::HashWithIndifferentAccess.new(hash)
160
+ end
161
+
162
+ protected
163
+
164
+ # Configuration base_url.
165
+ def base_url
166
+ JsonApiServer.configuration.base_url
167
+ end
168
+ end
169
+ end