jsonapionify 0.0.1.pre → 0.9.0

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +35 -0
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +0 -2
  5. data/Guardfile +1 -1
  6. data/README.md +13 -8
  7. data/Rakefile +10 -0
  8. data/config.ru +3 -3
  9. data/index.html +1 -0
  10. data/jsonapionify.gemspec +13 -8
  11. data/lib/jsonapionify/api/action.rb +60 -50
  12. data/lib/jsonapionify/api/attribute.rb +13 -2
  13. data/lib/jsonapionify/api/base/app_builder.rb +17 -2
  14. data/lib/jsonapionify/api/base/class_methods.rb +33 -17
  15. data/lib/jsonapionify/api/base/delegation.rb +4 -1
  16. data/lib/jsonapionify/api/base/doc_helper.rb +13 -4
  17. data/lib/jsonapionify/api/base/resource_definitions.rb +13 -2
  18. data/lib/jsonapionify/api/base.rb +22 -6
  19. data/lib/jsonapionify/api/context_delegate.rb +2 -2
  20. data/lib/jsonapionify/api/errors.rb +7 -2
  21. data/lib/jsonapionify/api/errors_object.rb +1 -1
  22. data/lib/jsonapionify/api/header_options.rb +6 -5
  23. data/lib/jsonapionify/api/param_options.rb +49 -7
  24. data/lib/jsonapionify/api/relationship/many.rb +0 -5
  25. data/lib/jsonapionify/api/relationship/one.rb +10 -9
  26. data/lib/jsonapionify/api/relationship.rb +17 -5
  27. data/lib/jsonapionify/api/resource/builders.rb +39 -10
  28. data/lib/jsonapionify/api/resource/class_methods.rb +17 -6
  29. data/lib/jsonapionify/api/resource/defaults/actions.rb +0 -1
  30. data/lib/jsonapionify/api/resource/defaults/errors.rb +11 -11
  31. data/lib/jsonapionify/api/resource/defaults/options.rb +53 -0
  32. data/lib/jsonapionify/api/resource/defaults/params.rb +9 -0
  33. data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +17 -11
  34. data/lib/jsonapionify/api/resource/definitions/actions.rb +51 -45
  35. data/lib/jsonapionify/api/resource/definitions/attributes.rb +2 -2
  36. data/lib/jsonapionify/api/resource/definitions/helpers.rb +18 -0
  37. data/lib/jsonapionify/api/resource/definitions/pagination.rb +183 -53
  38. data/lib/jsonapionify/api/resource/definitions/params.rb +43 -12
  39. data/lib/jsonapionify/api/resource/definitions/request_headers.rb +1 -67
  40. data/lib/jsonapionify/api/resource/definitions/scopes.rb +2 -13
  41. data/lib/jsonapionify/api/resource/definitions/sorting.rb +71 -58
  42. data/lib/jsonapionify/api/resource/error_handling.rb +2 -2
  43. data/lib/jsonapionify/api/resource/includer.rb +6 -0
  44. data/lib/jsonapionify/api/resource.rb +14 -3
  45. data/lib/jsonapionify/api/response.rb +2 -2
  46. data/lib/jsonapionify/api/server/mock_response.rb +2 -2
  47. data/lib/jsonapionify/api/server/request.rb +11 -7
  48. data/lib/jsonapionify/api/server.rb +1 -1
  49. data/lib/jsonapionify/api/sort_field.rb +59 -0
  50. data/lib/jsonapionify/api/sort_field_set.rb +36 -0
  51. data/lib/jsonapionify/callbacks.rb +3 -3
  52. data/lib/jsonapionify/continuation.rb +1 -0
  53. data/lib/jsonapionify/deep_sort_collection.rb +22 -0
  54. data/lib/jsonapionify/documentation/template.erb +196 -77
  55. data/lib/jsonapionify/documentation.rb +9 -9
  56. data/lib/jsonapionify/indented_string.rb +1 -0
  57. data/lib/jsonapionify/inherited_attributes.rb +4 -3
  58. data/lib/jsonapionify/structure/collections/base.rb +2 -1
  59. data/lib/jsonapionify/structure/helpers/errors.rb +1 -1
  60. data/lib/jsonapionify/structure/helpers/object_defaults.rb +2 -1
  61. data/lib/jsonapionify/structure/helpers/validations.rb +2 -1
  62. data/lib/jsonapionify/structure/objects/base.rb +4 -3
  63. data/lib/jsonapionify/structure/objects/top_level.rb +1 -1
  64. data/lib/jsonapionify/types/boolean_type.rb +2 -2
  65. data/lib/jsonapionify/types/date_string_type.rb +1 -1
  66. data/lib/jsonapionify/types/time_string_type.rb +1 -1
  67. data/lib/jsonapionify/version.rb +1 -1
  68. data/lib/jsonapionify.rb +16 -2
  69. metadata +69 -10
  70. data/fixtures/documentation.json +0 -364
  71. data/lib/jsonapionify/api/resource/http.rb +0 -11
  72. data/lib/jsonapionify/enumerable_observer.rb +0 -91
  73. data/lib/jsonapionify/unstrict_proc.rb +0 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c9e665303dde3d55816598fa79a9e6df58fc8281
4
- data.tar.gz: fd43d532d153eb7c3cc7061eaed2259c23887d97
3
+ metadata.gz: 515e9924931d33894090d68b518e2fe0e1509ab4
4
+ data.tar.gz: eb62428441716cfc4012d90f3b3af2bb6013e158
5
5
  SHA512:
6
- metadata.gz: 1b5a3455e4617b3600f888a70f775d04cc100773714021c69f3ef3a6ae9b7e91278ff4df78f6d4ba84f864daa199cfdec39c1e3389a3b8d10d3ebe74633fb2c0
7
- data.tar.gz: 2fc676ba38dc4c5cd89b3efbdb5ec114565439b2e50b063fa2c106d6c998d6067d4cdc9581551344a23b79af397f433113280c2dc378fc8f7f1028eb14d1a194
6
+ metadata.gz: eea64da6a6862c3a985fd63c8a2458fdf7c162777ca0f5116e8d20f81982fe1aafbc3023c02c4cac89b1003fe85e70e219476f53c2ebe999c8fe096c7c1ed59a
7
+ data.tar.gz: c84cd475c778481ad492c7ed220b3195315e23ff8f7a1223721ce74004792273f06340b1f3738132bfc3dcd69ad715784cdd4141f43a3d65f2f68fef87eb9a8a
data/.editorconfig ADDED
@@ -0,0 +1,35 @@
1
+ root = true
2
+
3
+ [*]
4
+ insert_final_newline = true
5
+ trim_trailing_whitespace = true
6
+ charset = utf-8
7
+
8
+ # Ruby Files
9
+ [{*.rb, Gemfile*, config.ru, Rakefile, *.rake}]
10
+ indent_style = space
11
+ indent_size = 2
12
+
13
+ # SQL
14
+ [*.sql]
15
+ indent_style = space
16
+ indent_size = 4
17
+
18
+ # Makefiles
19
+ [{Makefile, *.mk}]
20
+ indent_style = tab
21
+ indent_size = 1
22
+
23
+ # ERB
24
+ [*.erb]
25
+ indent_style = space
26
+ indent_size = 2
27
+
28
+ # Yaml
29
+ [*.yml]
30
+ indent_style = space
31
+ indent_size = 2
32
+
33
+ # Markdown uses trailing whitespace for linebreaks.
34
+ [{*.markdown, *.md}]
35
+ trim_trailing_whitespace = false
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.2.3
1
+ 2.3.0
data/.travis.yml CHANGED
@@ -1,7 +1,5 @@
1
1
  language: ruby
2
2
  sudo: false
3
- rvm:
4
- - 2.2.3
5
3
  before_install: gem install bundler -v 1.10.6
6
4
  script: bundle exec rspec
7
5
  cache: bundler
data/Guardfile CHANGED
@@ -1,6 +1,6 @@
1
1
  guard :rspec, cmd: "bundle exec rspec" do
2
2
  require "guard/rspec/dsl"
3
- dsl = Guard::RSpec::Dsl.new(self)
3
+ dsl = Guard::RSpec::Dsl.new(self)
4
4
 
5
5
  # RSpec files
6
6
  rspec = dsl.rspec
data/README.md CHANGED
@@ -1,10 +1,19 @@
1
1
  # JSONAPIonify
2
- [![Version](http://allthebadges.io/brandfolder/jsonapionify/badge_fury.png)](http://allthebadges.io/brandfolder/jsonapionify/badge_fury)
2
+ [![Gem Version](https://badge.fury.io/rb/jsonapionify.svg)](https://badge.fury.io/rb/jsonapionify)
3
3
  [![Build Status](https://travis-ci.org/brandfolder/jsonapionify.svg?branch=master)](https://travis-ci.org/brandfolder/jsonapionify)
4
4
  [![Code Climate](https://codeclimate.com/repos/5672446f137f95309c0067c6/badges/a369f0a182ce111c8fcd/gpa.svg)](https://codeclimate.com/repos/5672446f137f95309c0067c6/feed)
5
5
  [![Test Coverage](https://codeclimate.com/repos/5672446f137f95309c0067c6/badges/a369f0a182ce111c8fcd/coverage.svg)](https://codeclimate.com/repos/5672446f137f95309c0067c6/coverage)
6
6
 
7
- TODO: Write Description
7
+ JSONAPIonify is a framework for building JSONApi 1.0 compliant
8
+ APIs. It can run as a standalone rack app or as part of a larger framework such
9
+ as rails. In addition, it auto-generates beautiful documentation.
10
+
11
+ Live Example:
12
+
13
+ * [Resource](https://api.brandfolder.com/v2/slug/brandfolder)
14
+ * [Documentation](https://api.brandfolder.com/v2/docs) (screenshot below)
15
+
16
+ [![Documentation Example](https://api.url2png.com/v6/P3CAE278FC306AA/50ef2ba09c77f6fb25dd7f179de2a704/png/?thumbnail_max_width=500&url=https%3A%2F%2Fapi.brandfolder.com%2Fv2%2Fdocs)](https://api.brandfolder.com/v2/docs)
8
17
 
9
18
  ## Installation
10
19
 
@@ -18,13 +27,10 @@ And then execute:
18
27
 
19
28
  $ bundle
20
29
 
21
- Or install it yourself as:
22
-
23
- $ gem install jsonapionify
24
-
25
30
  ## Usage
26
31
 
27
- TODO: Write usage instructions here
32
+ Refer to the [wiki](https://github.com/brandfolder/jsonapionify/wiki) for detailed
33
+ information on how to use the framework.
28
34
 
29
35
  ## Development
30
36
 
@@ -40,4 +46,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/brandf
40
46
  ## License
41
47
 
42
48
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
43
-
data/Rakefile CHANGED
@@ -17,6 +17,16 @@ task :missing_specs do
17
17
  end
18
18
  end
19
19
 
20
+ desc 'Remove empty specs'
21
+ task :prune_specs do
22
+ empty_specs = Dir.glob("./spec/**/*_spec.rb").select do |f|
23
+ File.read(f).empty?
24
+ end
25
+ empty_specs.each do |f|
26
+ FileUtils.rm f
27
+ end
28
+ end
29
+
20
30
  STATS_DIRECTORIES = [
21
31
  %w(Structure lib/jsonapionify/structure),
22
32
  %w(Server lib/jsonapionify/api),
data/config.ru CHANGED
@@ -4,12 +4,12 @@ require 'jsonapionify'
4
4
  require 'navigable_hash'
5
5
  require 'json'
6
6
 
7
- api = NavigableHash.new JSON.load File.read 'fixtures/documentation.json'
8
- docs = JSONAPIonify::Documentation.new(api)
7
+ api = NavigableHash.new JSON.load File.read 'fixtures/documentation.json'
8
+ docs = JSONAPIonify::Documentation.new(api)
9
9
  result = docs.result
10
10
 
11
11
  run ->(_) {
12
12
  response = Rack::Response.new
13
13
  response.write result
14
14
  response.finish
15
- }
15
+ }
data/index.html ADDED
@@ -0,0 +1 @@
1
+ My Page
data/jsonapionify.gemspec CHANGED
@@ -4,21 +4,22 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'jsonapionify/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "jsonapionify"
8
- spec.version = JSONAPIonify::VERSION
9
- spec.authors = ["Jason Waldrip"]
10
- spec.email = ["jason@waldrip.net"]
7
+ spec.name = "jsonapionify"
8
+ spec.version = JSONAPIonify::VERSION
9
+ spec.authors = ["Jason Waldrip"]
10
+ spec.email = ["jason@waldrip.net"]
11
11
 
12
- spec.summary = %q{Ruby object structure conforming to the JSON API spec.}
13
- spec.homepage = "https://github.com/brandfolder/jsonapionify"
14
- spec.license = "MIT"
12
+ spec.summary = %q{Ruby object structure conforming to the JSON API spec.}
13
+ spec.homepage = "https://github.com/brandfolder/jsonapionify"
14
+ spec.license = "MIT"
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(vendor|spec)/}) }
17
17
  spec.bindir = "exe"
18
18
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.required_ruby_version = '~> 2.2'
21
+ version_file = File.expand_path '../.ruby-version', __FILE__
22
+ spec.required_ruby_version = "~> #{File.read(version_file).strip.sub(/\-p[0-9]+$/, '')}"
22
23
 
23
24
  spec.add_dependency "activesupport", "~> 4.2"
24
25
  spec.add_dependency "faraday", "~> 0.9"
@@ -27,6 +28,9 @@ Gem::Specification.new do |spec|
27
28
  spec.add_dependency "oj"
28
29
  spec.add_dependency "rack-test"
29
30
  spec.add_dependency 'faker'
31
+ spec.add_dependency 'possessive'
32
+ spec.add_dependency 'unstrict_proc'
33
+ spec.add_dependency 'enumerable_observer'
30
34
 
31
35
  spec.add_development_dependency 'pry'
32
36
  spec.add_development_dependency 'rocco'
@@ -47,4 +51,5 @@ Gem::Specification.new do |spec|
47
51
  spec.add_development_dependency 'ruby-prof'
48
52
  spec.add_development_dependency 'bcrypt'
49
53
  spec.add_development_dependency 'thin'
54
+ spec.add_development_dependency 'memory_model'
50
55
  end
@@ -3,10 +3,16 @@ module JSONAPIonify::Api
3
3
  attr_reader :name, :request_block, :content_type, :responses, :prepend,
4
4
  :path, :request_method, :only_associated
5
5
 
6
- def self.stub(&block)
6
+ def self.dummy(&block)
7
7
  new(nil, nil, &block)
8
8
  end
9
9
 
10
+ def self.error(name, &block)
11
+ dummy do
12
+ error_now name, &block
13
+ end
14
+ end
15
+
10
16
  def initialize(name, request_method, path = nil, require_body = nil, example_type = :resource, content_type: nil, prepend: nil, only_associated: false, &block)
11
17
  @request_method = request_method
12
18
  @require_body = require_body.nil? ? %w{POST PUT PATCH}.include?(@request_method) : require_body
@@ -54,17 +60,16 @@ module JSONAPIonify::Api
54
60
  request.path_info.match(path_regex(base, name, include_path))
55
61
  end
56
62
 
57
- def documentation_object(resource, base, name, include_path)
58
- @documentation_object ||= begin
59
- url = build_path(base, name, include_path)
60
- OpenStruct.new(
61
- name: self.name.to_s,
62
- sample_requests: example_requests(resource, url)
63
- )
64
- end
63
+ def documentation_object(base, resource, name, include_path, label)
64
+ url = build_path(base, name.to_s, include_path)
65
+ path = URI.parse(url).path
66
+ OpenStruct.new(
67
+ id: [request_method, path].join('-').parameterize,
68
+ label: label,
69
+ sample_requests: example_requests(resource, url)
70
+ )
65
71
  end
66
72
 
67
-
68
73
  def example_requests(resource, url)
69
74
  responses.map do |response|
70
75
  opts = {}
@@ -76,7 +81,7 @@ module JSONAPIonify::Api
76
81
  { 'data' => resource.build_resource(request, resource.example_instance, relationships: false, links: false).as_json }.to_json
77
82
  when :resource_identifier
78
83
  { 'data' => resource.build_resource_identifier(resource.example_instance).as_json }.to_json
79
- end if @content_type == 'application/vnd.api+json'
84
+ end if @content_type == 'application/vnd.api+json' && !%w{GET DELETE}.include?(request_method)
80
85
  request = Server::Request.env_for(url, request_method, opts)
81
86
  response = Server::MockResponse.new(*sample_request(resource, request))
82
87
 
@@ -88,8 +93,7 @@ module JSONAPIonify::Api
88
93
  end
89
94
 
90
95
  def supports_content_type?(request)
91
- @content_type == request.content_type ||
92
- (request.content_type.nil? && !request.has_body?)
96
+ @content_type == request.content_type || !request.has_body?
93
97
  end
94
98
 
95
99
  def supports_request_method?(request)
@@ -137,57 +141,63 @@ module JSONAPIonify::Api
137
141
  end
138
142
 
139
143
  def call(resource, request)
140
- action = dup
141
- cache_hit_exception = Class.new StandardError
142
- cache_options = {}
144
+ action = dup
145
+ cache_options = {}
143
146
  resource.new.instance_eval do
144
147
  # Bootstrap the Action
145
- callbacks = resource.callbacks_for(action.name).new
146
- context = ContextDelegate.new(request, self, self.class.context_definitions)
147
-
148
- define_singleton_method :action_name do
149
- action.name
150
- end
148
+ context = ContextDelegate.new(request, self, self.class.context_definitions)
151
149
 
150
+ # Define Singletons
152
151
  define_singleton_method :cache do |key, **options|
152
+ raise Errors::DoubleCacheError, "Cache was already called for this action" if @called
153
+ @called = true
153
154
  cache_options.merge! options
154
- cache_options[:key] = [*{
155
- api: [self.class.api.name, self.class.api.resource_signature].join('@'),
156
- resource: self.class.type,
157
- content_type: request.content_type || '*',
158
- accept: request.accept.join(','),
159
- params: context.params.to_param
160
- }.map { |kv| kv.join(':') }, key].join('|')
161
- raise cache_hit_exception, cache_options[:key] if self.class.cache_store.exist?(cache_options[:key])
162
- end if request.get?
163
155
 
164
- # Define Shared Singletons
165
- [self, callbacks].each do |target|
166
- target.define_singleton_method :errors do
167
- context.errors
156
+ # Build the cache key, and obscure it.
157
+ context.meta[:cache_key] =
158
+ cache_options[:key] =
159
+ Base64.urlsafe_encode64(
160
+ {
161
+ dsl: JSONAPIonify.digest,
162
+ api: self.class.api.signature,
163
+ path: request.path,
164
+ accept: request.accept,
165
+ params: context.params,
166
+ key: key,
167
+ }.to_json
168
+ )
169
+ # If the cache exists, then fail to cache miss
170
+ if self.class.cache_store.exist?(cache_options[:key])
171
+ raise Errors::CacheHit, cache_options[:key]
168
172
  end
173
+ end if request.get?
169
174
 
170
- define_singleton_method :response_headers do
171
- context.response_headers
172
- end
175
+ define_singleton_method :action_name do
176
+ action.name
177
+ end
173
178
 
174
- target.define_singleton_method :error_exception do
175
- context.error_exception
176
- end
179
+ define_singleton_method :errors do
180
+ context.errors
181
+ end
182
+
183
+ define_singleton_method :response_headers do
184
+ context.response_headers
177
185
  end
178
186
 
179
187
  begin
180
188
  # Run Callbacks
181
- case callbacks.run_callbacks(:request, context) { errors.present? }
182
- when true # Boolean true means errors
183
- raise error_exception
184
- when nil # nil means no result, callback failed
185
- error_now :internal_server_error
189
+ [:request, action.name].each do |callback|
190
+ case run_callbacks(callback, context) { errors.present? }
191
+ when true # Boolean true means errors
192
+ raise Errors::RequestError
193
+ when nil # nil means no result, callback failed
194
+ error_now :internal_server_error
195
+ end if action.name
186
196
  end
187
197
 
188
198
  # Start the request
189
199
  instance_exec(context, &action.request_block)
190
- fail error_exception if errors.present?
200
+ fail Errors::RequestError if errors.present?
191
201
  response_definition =
192
202
  action.responses.find { |response| response.accept? request } ||
193
203
  error_now(:not_acceptable)
@@ -196,11 +206,11 @@ module JSONAPIonify::Api
196
206
  cache_options[:key],
197
207
  [status, headers, body.body],
198
208
  **cache_options.except(:key)
199
- ) if request.get?
209
+ ) if request.get? && cache_options.present?
200
210
  end
201
- rescue error_exception
211
+ rescue Errors::RequestError
202
212
  error_response
203
- rescue cache_hit_exception
213
+ rescue Errors::CacheHit
204
214
  self.class.cache_store.read cache_options[:key]
205
215
  rescue Exception => exception
206
216
  rescued_response exception
@@ -1,8 +1,12 @@
1
+ require 'unstrict_proc'
2
+
1
3
  module JSONAPIonify::Api
2
4
  class Attribute
5
+ using UnstrictProc
3
6
  attr_reader :name, :type, :description, :read, :write, :required
4
7
 
5
8
  def initialize(name, type, description, read: true, write: true, required: false, example: nil)
9
+ raise ArgumentError, 'required attributes must be writable' if required && !write
6
10
  unless type.is_a? JSONAPIonify::Types::BaseType
7
11
  raise TypeError, "#{type} is not a valid JSON type"
8
12
  end
@@ -20,6 +24,13 @@ module JSONAPIonify::Api
20
24
  self.name == other.name
21
25
  end
22
26
 
27
+ def options_json
28
+ {
29
+ name: name,
30
+ required: required
31
+ }
32
+ end
33
+
23
34
  def required?
24
35
  !!@required
25
36
  end
@@ -36,10 +47,10 @@ module JSONAPIonify::Api
36
47
  !!@write
37
48
  end
38
49
 
39
- def example
50
+ def example(*args)
40
51
  case @example
41
52
  when Proc
42
- type.dump @example.call
53
+ type.dump @example.unstrict.call(*args)
43
54
  when nil
44
55
  type.dump type.sample(name)
45
56
  else
@@ -1,6 +1,17 @@
1
1
  module JSONAPIonify::Api
2
2
  module Base::AppBuilder
3
3
 
4
+ def self.extended(klass)
5
+ klass.class_eval do
6
+ extend JSONAPIonify::InheritedAttributes
7
+ inherited_array_attribute :middleware
8
+ end
9
+ end
10
+
11
+ def use(*args, &block)
12
+ middleware << [args, block]
13
+ end
14
+
4
15
  def call(env)
5
16
  app.call(env)
6
17
  end
@@ -15,16 +26,20 @@ module JSONAPIonify::Api
15
26
  use Base::Reloader unless ENV['RACK_ENV'] == 'production'
16
27
  map "/docs" do
17
28
  run ->(env) {
18
- request = JSONAPIonify::Api::Server::Request.new env
29
+ request = JSONAPIonify::Api::Server::Request.new env
19
30
  if request.path_info.present?
20
31
  return [301, { 'location' => request.path.chomp(request.path_info) }, []]
21
32
  end
22
- response = Rack::Response.new
33
+ response = Rack::Response.new
23
34
  response.write api.documentation_output(request)
24
35
  response.finish
25
36
  }
26
37
  end
27
38
  map "/" do
39
+ use Rack::MethodOverride
40
+ api.middleware.each do |args, block|
41
+ use *args, &block
42
+ end
28
43
  run JSONAPIonify::Api::Server.new(api)
29
44
  end
30
45
  end
@@ -4,45 +4,57 @@ module JSONAPIonify::Api
4
4
  module Base::ClassMethods
5
5
 
6
6
  def self.extended(klass)
7
- klass.class_attribute :load_path
7
+ klass.class_attribute :load_path, :load_file
8
8
  end
9
9
 
10
10
  def resource_files
11
- Dir.glob File.join(load_path, '**/*.rb')
11
+ files = Dir.glob(File.join(load_path, '**/*.rb'))
12
+ files.concat(superclass < JSONAPIonify::Api::Base ? superclass.resource_files : []).sort
12
13
  end
13
14
 
14
15
  def resource_signature
15
- Digest::SHA2.hexdigest resource_files.map { |file| File.read file }.join
16
+ Digest::SHA2.hexdigest [*resource_files, load_file].map { |file| File.read file }.join
17
+ end
18
+
19
+ def signature
20
+ [name, resource_signature].join('@')
16
21
  end
17
22
 
18
23
  def load_resources
19
- return unless load_path
20
- if @last_signature != resource_signature
21
- @documentation_output = nil
22
- @last_signature = resource_signature
23
- $".delete_if { |s| s.start_with? load_path }
24
- resource_files.each do |file|
25
- require file
26
- end
24
+ return unless load_path || resources_loaded?
25
+ @documentation_output = nil
26
+ @last_signature = resource_signature
27
+ $".delete_if { |s| s.start_with? load_path }
28
+ resource_files.each do |file|
29
+ require file
27
30
  end
28
31
  end
29
32
 
30
- def resource_class
31
- const_get(:ResourceBase, false)
33
+ def resources_loaded?
34
+ @last_signature == resource_signature
35
+ end
36
+
37
+ def http_error(action, request)
38
+ Action.error(action).call(resource_class, request)
32
39
  end
33
40
 
34
- def documentation_order(resources_in_order)
35
- @documentation_order = resources_in_order
41
+ def root_url(request)
42
+ URI.parse(request.root_url).tap do |uri|
43
+ sticky_params = sticky_params(request.params)
44
+ uri.query = sticky_params.to_param if sticky_params.present?
45
+ end.to_s
36
46
  end
37
47
 
38
48
  def process_index(request)
39
49
  headers = ContextDelegate.new(request, resource_class.new, resource_class.context_definitions).response_headers
40
50
  obj = JSONAPIonify.new_object
41
51
  obj[:meta] = { resources: {} }
42
- obj[:links] = { self: request.root_url }
52
+ obj[:links] = { self: request.url }
43
53
  obj[:meta][:documentation] = File.join(request.root_url, 'docs')
44
54
  obj[:meta][:resources] = resources.each_with_object({}) do |resource, hash|
45
- hash[resource.type] = resource.get_url(request.root_url)
55
+ if resource.actions.any? { |action| action.name == :list }
56
+ hash[resource.type] = resource.get_url(root_url(request))
57
+ end
46
58
  end
47
59
  Rack::Response.new.tap do |response|
48
60
  response.status = 200
@@ -66,6 +78,10 @@ module JSONAPIonify::Api
66
78
  @cache_store = store
67
79
  end
68
80
 
81
+ def eager_load
82
+ resources.each(&:eager_load)
83
+ end
84
+
69
85
  def cache_store
70
86
  @cache_store ||= JSONAPIonify.cache_store
71
87
  end
@@ -5,7 +5,10 @@ module JSONAPIonify::Api
5
5
  klass.class_eval do
6
6
  class << self
7
7
  delegate :context, :response_header, :helper, :rescue_from, :error,
8
- :pagination, :before, :param, :request_header, :sorting,
8
+ :enable_pagination, :before, :param, :request_header,
9
+ :define_pagination_strategy, :define_sorting_strategy,
10
+ :sticky_params, :authentication, :on_exception,
11
+ :example_id_generator,
9
12
  to: :resource_class
10
13
  end
11
14
  end
@@ -1,5 +1,6 @@
1
1
  module JSONAPIonify::Api
2
2
  module Base::DocHelper
3
+ Link = Struct.new(:title, :href)
3
4
 
4
5
  def self.extended(klass)
5
6
  klass.class_eval do
@@ -8,8 +9,12 @@ module JSONAPIonify::Api
8
9
  end
9
10
  end
10
11
 
11
- def link(href)
12
- links << href
12
+ def documentation_order(resources_in_order)
13
+ @documentation_order = resources_in_order
14
+ end
15
+
16
+ def link(title, href)
17
+ links << Link.new(title, href)
13
18
  end
14
19
 
15
20
  def title(title)
@@ -21,12 +26,16 @@ module JSONAPIonify::Api
21
26
  end
22
27
 
23
28
  def documentation_output(request)
24
- @documentation_output ||= JSONAPIonify::Documentation.new(documentation_object(request)).result
29
+ #cache_store.fetch(resource_signature) do
30
+ JSONAPIonify::Documentation.new(documentation_object(request)).result
31
+ #end
25
32
  end
26
33
 
27
34
  def resources_in_order
28
35
  indexes = @documentation_order || []
29
- resources.sort_by { |resource| indexes.index(resource.name) || indexes.length }
36
+ resources.sort_by(&:name).sort_by do |resource|
37
+ indexes.map(&:to_s).index(resource.type) || indexes.length
38
+ end
30
39
  end
31
40
 
32
41
  def documentation_object(request)
@@ -11,12 +11,16 @@ module JSONAPIonify::Api
11
11
  def resource(type)
12
12
  raise ArgumentError, 'type required' if type.nil?
13
13
  type = type.to_sym
14
- const_name = type.to_s.camelcase
14
+ const_name = type.to_s.camelcase + 'Resource'
15
15
  return const_get(const_name, false) if const_defined?(const_name, false)
16
16
  raise Errors::ResourceNotFound, "Resource not defined: #{type}" unless resource_defined?(type)
17
17
  klass = Class.new(resource_class, &resource_definitions[type]).set_type(type)
18
18
  param(:fields, type)
19
19
  const_set const_name, klass
20
+ rescue Errors::ResourceNotFound => e
21
+ raise e if resources_loaded?
22
+ load_resources
23
+ retry
20
24
  end
21
25
 
22
26
  def resource_defined?(name)
@@ -30,10 +34,17 @@ module JSONAPIonify::Api
30
34
  end
31
35
 
32
36
  def define_resource(name, &block)
33
- const_name = name.to_s.camelcase
37
+ const_name = name.to_s.camelcase + 'Resource'
34
38
  remove_const(const_name) if const_defined? const_name
35
39
  resource_definitions[name.to_sym] = block
36
40
  end
37
41
 
42
+ def extend_resource(name, &block)
43
+ old = resource_definitions[name.to_sym]
44
+ resource_definitions[name.to_sym] = proc do
45
+ [old, block].each { |b| class_eval(&b) }
46
+ end
47
+ end
48
+
38
49
  end
39
50
  end
@@ -13,12 +13,28 @@ module JSONAPIonify::Api
13
13
 
14
14
  def self.inherited(subclass)
15
15
  super(subclass)
16
- file = caller.reject { |f| f.start_with? JSONAPIonify.path }[0].split(/\:\d/)[0]
17
- dir = File.expand_path File.dirname(file)
18
- basename = File.basename(file, File.extname(file))
19
- self.load_path = File.join(dir, basename)
20
- subclass.const_set(:ResourceBase, Class.new(Resource).set_api(subclass))
21
- load_resources
16
+ subclass.instance_exec(self) do |superclass|
17
+ const_set(:ResourceBase, Class.new(superclass.resource_class))
18
+ resource_class.set_api(self)
19
+
20
+ file = caller.reject { |f| f.start_with? JSONAPIonify.path }[0].split(/\:\d/)[0]
21
+ dir = File.expand_path File.dirname(file)
22
+ basename = File.basename(file, File.extname(file))
23
+ self.load_path = File.join(dir, basename)
24
+ self.load_file = file
25
+
26
+ @title = superclass.instance_variable_get(:@title)
27
+ @description = superclass.instance_variable_get(:@description)
28
+ @documentation_order = superclass.instance_variable_get(:@documentation_order)
29
+ end
30
+ end
31
+
32
+ def self.resource_class
33
+ if const_defined?(:ResourceBase, false)
34
+ const_get(:ResourceBase, false)
35
+ else
36
+ const_set(:ResourceBase, Class.new(Resource))
37
+ end
22
38
  end
23
39
 
24
40
  end