json_api_server 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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