jsonapionify 0.0.1.pre → 0.9.0

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