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.
- checksums.yaml +4 -4
- data/.editorconfig +35 -0
- data/.ruby-version +1 -1
- data/.travis.yml +0 -2
- data/Guardfile +1 -1
- data/README.md +13 -8
- data/Rakefile +10 -0
- data/config.ru +3 -3
- data/index.html +1 -0
- data/jsonapionify.gemspec +13 -8
- data/lib/jsonapionify/api/action.rb +60 -50
- data/lib/jsonapionify/api/attribute.rb +13 -2
- data/lib/jsonapionify/api/base/app_builder.rb +17 -2
- data/lib/jsonapionify/api/base/class_methods.rb +33 -17
- data/lib/jsonapionify/api/base/delegation.rb +4 -1
- data/lib/jsonapionify/api/base/doc_helper.rb +13 -4
- data/lib/jsonapionify/api/base/resource_definitions.rb +13 -2
- data/lib/jsonapionify/api/base.rb +22 -6
- data/lib/jsonapionify/api/context_delegate.rb +2 -2
- data/lib/jsonapionify/api/errors.rb +7 -2
- data/lib/jsonapionify/api/errors_object.rb +1 -1
- data/lib/jsonapionify/api/header_options.rb +6 -5
- data/lib/jsonapionify/api/param_options.rb +49 -7
- data/lib/jsonapionify/api/relationship/many.rb +0 -5
- data/lib/jsonapionify/api/relationship/one.rb +10 -9
- data/lib/jsonapionify/api/relationship.rb +17 -5
- data/lib/jsonapionify/api/resource/builders.rb +39 -10
- data/lib/jsonapionify/api/resource/class_methods.rb +17 -6
- data/lib/jsonapionify/api/resource/defaults/actions.rb +0 -1
- data/lib/jsonapionify/api/resource/defaults/errors.rb +11 -11
- data/lib/jsonapionify/api/resource/defaults/options.rb +53 -0
- data/lib/jsonapionify/api/resource/defaults/params.rb +9 -0
- data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +17 -11
- data/lib/jsonapionify/api/resource/definitions/actions.rb +51 -45
- data/lib/jsonapionify/api/resource/definitions/attributes.rb +2 -2
- data/lib/jsonapionify/api/resource/definitions/helpers.rb +18 -0
- data/lib/jsonapionify/api/resource/definitions/pagination.rb +183 -53
- data/lib/jsonapionify/api/resource/definitions/params.rb +43 -12
- data/lib/jsonapionify/api/resource/definitions/request_headers.rb +1 -67
- data/lib/jsonapionify/api/resource/definitions/scopes.rb +2 -13
- data/lib/jsonapionify/api/resource/definitions/sorting.rb +71 -58
- data/lib/jsonapionify/api/resource/error_handling.rb +2 -2
- data/lib/jsonapionify/api/resource/includer.rb +6 -0
- data/lib/jsonapionify/api/resource.rb +14 -3
- data/lib/jsonapionify/api/response.rb +2 -2
- data/lib/jsonapionify/api/server/mock_response.rb +2 -2
- data/lib/jsonapionify/api/server/request.rb +11 -7
- data/lib/jsonapionify/api/server.rb +1 -1
- data/lib/jsonapionify/api/sort_field.rb +59 -0
- data/lib/jsonapionify/api/sort_field_set.rb +36 -0
- data/lib/jsonapionify/callbacks.rb +3 -3
- data/lib/jsonapionify/continuation.rb +1 -0
- data/lib/jsonapionify/deep_sort_collection.rb +22 -0
- data/lib/jsonapionify/documentation/template.erb +196 -77
- data/lib/jsonapionify/documentation.rb +9 -9
- data/lib/jsonapionify/indented_string.rb +1 -0
- data/lib/jsonapionify/inherited_attributes.rb +4 -3
- data/lib/jsonapionify/structure/collections/base.rb +2 -1
- data/lib/jsonapionify/structure/helpers/errors.rb +1 -1
- data/lib/jsonapionify/structure/helpers/object_defaults.rb +2 -1
- data/lib/jsonapionify/structure/helpers/validations.rb +2 -1
- data/lib/jsonapionify/structure/objects/base.rb +4 -3
- data/lib/jsonapionify/structure/objects/top_level.rb +1 -1
- data/lib/jsonapionify/types/boolean_type.rb +2 -2
- data/lib/jsonapionify/types/date_string_type.rb +1 -1
- data/lib/jsonapionify/types/time_string_type.rb +1 -1
- data/lib/jsonapionify/version.rb +1 -1
- data/lib/jsonapionify.rb +16 -2
- metadata +69 -10
- data/fixtures/documentation.json +0 -364
- data/lib/jsonapionify/api/resource/http.rb +0 -11
- data/lib/jsonapionify/enumerable_observer.rb +0 -91
- data/lib/jsonapionify/unstrict_proc.rb +0 -28
| @@ -1,3 +1,4 @@ | |
| 1 | 
            +
            require 'possessive'
         | 
| 1 2 | 
             
            require 'active_support/core_ext/array/wrap'
         | 
| 2 3 |  | 
| 3 4 | 
             
            module JSONAPIonify::Api
         | 
| @@ -7,15 +8,10 @@ module JSONAPIonify::Api | |
| 7 8 | 
             
                def self.extended(klass)
         | 
| 8 9 | 
             
                  klass.class_eval do
         | 
| 9 10 | 
             
                    extend JSONAPIonify::InheritedAttributes
         | 
| 11 | 
            +
                    include JSONAPIonify::Callbacks
         | 
| 12 | 
            +
                    define_callbacks :request, :list, :create, :read, :update, :delete,
         | 
| 13 | 
            +
                                     :show, :add, :remove, :replace, :exception
         | 
| 10 14 | 
             
                    inherited_array_attribute :action_definitions
         | 
| 11 | 
            -
                    inherited_hash_attribute :callbacks
         | 
| 12 | 
            -
             | 
| 13 | 
            -
                    def self.inherited(subclass)
         | 
| 14 | 
            -
                      super
         | 
| 15 | 
            -
                      callbacks.each do |action_name, klass|
         | 
| 16 | 
            -
                        subclass.callbacks[action_name] = Class.new klass
         | 
| 17 | 
            -
                      end
         | 
| 18 | 
            -
                    end
         | 
| 19 15 | 
             
                  end
         | 
| 20 16 | 
             
                end
         | 
| 21 17 |  | 
| @@ -27,7 +23,6 @@ module JSONAPIonify::Api | |
| 27 23 | 
             
                        context.response_collection,
         | 
| 28 24 | 
             
                        fields: context.fields
         | 
| 29 25 | 
             
                      )
         | 
| 30 | 
            -
                      context.meta[:total_count]     = context.collection.count
         | 
| 31 26 | 
             
                      context.response_object.to_json
         | 
| 32 27 | 
             
                    end
         | 
| 33 28 | 
             
                  end
         | 
| @@ -42,6 +37,7 @@ module JSONAPIonify::Api | |
| 42 37 | 
             
                  define_action(:create, 'POST', **options, &block).tap do |action|
         | 
| 43 38 | 
             
                    action.response status: 201 do |context|
         | 
| 44 39 | 
             
                      context.response_object[:data] = build_resource(context.request, context.instance, fields: context.fields)
         | 
| 40 | 
            +
                      response_headers['Location']   = build_url(context.request, context.instance)
         | 
| 45 41 | 
             
                      context.response_object.to_json
         | 
| 46 42 | 
             
                    end
         | 
| 47 43 | 
             
                  end
         | 
| @@ -74,9 +70,19 @@ module JSONAPIonify::Api | |
| 74 70 | 
             
                def process(request)
         | 
| 75 71 | 
             
                  path_actions = self.path_actions(request)
         | 
| 76 72 | 
             
                  if request.options? && path_actions.present?
         | 
| 77 | 
            -
                     | 
| 78 | 
            -
             | 
| 79 | 
            -
                       | 
| 73 | 
            +
                    allow    = [*path_actions.map(&:request_method), 'OPTIONS']
         | 
| 74 | 
            +
                    requests = allow.each_with_object({}) do |method, h|
         | 
| 75 | 
            +
                      h[method] = options_for_method(method)
         | 
| 76 | 
            +
                    end
         | 
| 77 | 
            +
                    Action.dummy do
         | 
| 78 | 
            +
                      response_headers['Allow'] = allow.join(', ')
         | 
| 79 | 
            +
                    end.response(status: 200, accept: 'application/vnd.api+json') do
         | 
| 80 | 
            +
                      JSONAPIonify.new_object(
         | 
| 81 | 
            +
                        meta: {
         | 
| 82 | 
            +
                          type: self.class.type,
         | 
| 83 | 
            +
                          requests: requests
         | 
| 84 | 
            +
                        }
         | 
| 85 | 
            +
                      ).to_json
         | 
| 80 86 | 
             
                    end.call(self, request)
         | 
| 81 87 | 
             
                  elsif (action = find_supported_action(request))
         | 
| 82 88 | 
             
                    action.call(self, request)
         | 
| @@ -87,39 +93,28 @@ module JSONAPIonify::Api | |
| 87 93 | 
             
                  end
         | 
| 88 94 | 
             
                end
         | 
| 89 95 |  | 
| 96 | 
            +
                def options_for_method(method)
         | 
| 97 | 
            +
                  case method
         | 
| 98 | 
            +
                  when 'GET'
         | 
| 99 | 
            +
                    { attributes: attributes.select(&:read).map(&:options_json) }
         | 
| 100 | 
            +
                  when 'POST', 'PUT', 'PATCH'
         | 
| 101 | 
            +
                    { attributes: attributes.select(&:write).map(&:options_json) }
         | 
| 102 | 
            +
                  else
         | 
| 103 | 
            +
                    {}
         | 
| 104 | 
            +
                  end
         | 
| 105 | 
            +
                end
         | 
| 106 | 
            +
             | 
| 90 107 | 
             
                def before(action_name = nil, &block)
         | 
| 91 108 | 
             
                  if action_name == :index
         | 
| 92 109 | 
             
                    warn 'the `index` action will soon be deprecated, use `list` instead!'
         | 
| 93 110 | 
             
                    action_name = :list
         | 
| 94 111 | 
             
                  end
         | 
| 95 | 
            -
                  return  | 
| 96 | 
            -
                   | 
| 97 | 
            -
                end
         | 
| 98 | 
            -
             | 
| 99 | 
            -
                def base_callbacks
         | 
| 100 | 
            -
                  resource       = self
         | 
| 101 | 
            -
                  callbacks['*'] ||= Class.new do
         | 
| 102 | 
            -
                    def self.context(*)
         | 
| 103 | 
            -
                    end
         | 
| 104 | 
            -
             | 
| 105 | 
            -
                    include Resource::ErrorHandling
         | 
| 106 | 
            -
             | 
| 107 | 
            -
                    define_singleton_method(:error_definitions) do
         | 
| 108 | 
            -
                      resource.error_definitions
         | 
| 109 | 
            -
                    end
         | 
| 110 | 
            -
             | 
| 111 | 
            -
                    include JSONAPIonify::Callbacks
         | 
| 112 | 
            -
                    define_callbacks :request
         | 
| 113 | 
            -
                  end
         | 
| 114 | 
            -
                end
         | 
| 115 | 
            -
             | 
| 116 | 
            -
                def callbacks_for(action_name)
         | 
| 117 | 
            -
                  resource               = self
         | 
| 118 | 
            -
                  callbacks[action_name] ||= Class.new(base_callbacks)
         | 
| 112 | 
            +
                  return before_request &block if action_name == nil
         | 
| 113 | 
            +
                  send("before_#{action_name}", &block)
         | 
| 119 114 | 
             
                end
         | 
| 120 115 |  | 
| 121 | 
            -
                def define_action(*args, **options, &block)
         | 
| 122 | 
            -
                  Action.new(*args, **options, &block).tap do |new_action|
         | 
| 116 | 
            +
                def define_action(name, *args, **options, &block)
         | 
| 117 | 
            +
                  Action.new(name, *args, **options, &block).tap do |new_action|
         | 
| 123 118 | 
             
                    action_definitions.delete new_action
         | 
| 124 119 | 
             
                    action_definitions << new_action
         | 
| 125 120 | 
             
                  end
         | 
| @@ -133,14 +128,11 @@ module JSONAPIonify::Api | |
| 133 128 |  | 
| 134 129 | 
             
                def no_action_response(request)
         | 
| 135 130 | 
             
                  if request_method_actions(request).present?
         | 
| 136 | 
            -
                    Action. | 
| 137 | 
            -
                  elsif  | 
| 138 | 
            -
                    Action. | 
| 139 | 
            -
                      headers['Allow'] = path_actions.map(&:request_method).join(', ')
         | 
| 140 | 
            -
                      error_now :method_not_allowed
         | 
| 141 | 
            -
                    end
         | 
| 131 | 
            +
                    Action.error :unsupported_media_type
         | 
| 132 | 
            +
                  elsif self.path_actions(request).present?
         | 
| 133 | 
            +
                    Action.error :forbidden
         | 
| 142 134 | 
             
                  else
         | 
| 143 | 
            -
                    Action. | 
| 135 | 
            +
                    Action.error :not_found
         | 
| 144 136 | 
             
                  end
         | 
| 145 137 | 
             
                end
         | 
| 146 138 |  | 
| @@ -173,12 +165,26 @@ module JSONAPIonify::Api | |
| 173 165 | 
             
                end
         | 
| 174 166 |  | 
| 175 167 | 
             
                def actions
         | 
| 168 | 
            +
                  return if action_definitions.blank?
         | 
| 176 169 | 
             
                  action_definitions.select do |action|
         | 
| 177 170 | 
             
                    action.only_associated == false ||
         | 
| 178 171 | 
             
                      (respond_to?(:rel) && action.only_associated == true)
         | 
| 179 172 | 
             
                  end
         | 
| 180 173 | 
             
                end
         | 
| 181 174 |  | 
| 175 | 
            +
                def documented_actions
         | 
| 176 | 
            +
                  api.eager_load
         | 
| 177 | 
            +
                  relationships = descendants.select { |descendant| descendant.respond_to? :rel }
         | 
| 178 | 
            +
                  rels          = relationships.each_with_object([]) do |rel, ary|
         | 
| 179 | 
            +
                    rel.actions.each do |action|
         | 
| 180 | 
            +
                      ary << [action, "#{rel.rel.owner.type}/:id", [rel, rel.rel.name, false, "#{action.name} #{rel.rel.owner.type.singularize.possessive} #{rel.rel.name}"]]
         | 
| 181 | 
            +
                    end
         | 
| 182 | 
            +
                  end
         | 
| 183 | 
            +
                  actions.map do |action|
         | 
| 184 | 
            +
                    [action, '', [self, type, true, "#{action.name} #{type}"]]
         | 
| 185 | 
            +
                  end + rels
         | 
| 186 | 
            +
                end
         | 
| 187 | 
            +
             | 
| 182 188 | 
             
                private
         | 
| 183 189 |  | 
| 184 190 | 
             
                def base_path
         | 
| @@ -6,7 +6,7 @@ module JSONAPIonify::Api | |
| 6 6 | 
             
                    extend JSONAPIonify::InheritedAttributes
         | 
| 7 7 | 
             
                    extend JSONAPIonify::Types
         | 
| 8 8 | 
             
                    inherited_array_attribute :attributes
         | 
| 9 | 
            -
                    delegate :attributes, to: :class
         | 
| 9 | 
            +
                    delegate :id_attribute, :attributes, to: :class
         | 
| 10 10 |  | 
| 11 11 | 
             
                    context(:fields, readonly: true) do |context|
         | 
| 12 12 | 
             
                      should_error = false
         | 
| @@ -20,7 +20,7 @@ module JSONAPIonify::Api | |
| 20 20 | 
             
                            attribute ? field_list << attribute.name : error(:field_not_permitted, type, field) && (should_error = true)
         | 
| 21 21 | 
             
                          end
         | 
| 22 22 | 
             
                      end
         | 
| 23 | 
            -
                      raise  | 
| 23 | 
            +
                      raise Errors::RequestError if should_error
         | 
| 24 24 | 
             
                      fields
         | 
| 25 25 | 
             
                    end
         | 
| 26 26 | 
             
                  end
         | 
| @@ -5,5 +5,23 @@ module JSONAPIonify::Api | |
| 5 5 | 
             
                  define_method(name, &block)
         | 
| 6 6 | 
             
                end
         | 
| 7 7 |  | 
| 8 | 
            +
                def authentication(&block)
         | 
| 9 | 
            +
                  context :authentication, readonly: true do |context|
         | 
| 10 | 
            +
                    OpenStruct.new.tap do |authentication_object|
         | 
| 11 | 
            +
                      if instance_exec(context.request, authentication_object, &block) == false
         | 
| 12 | 
            +
                        error_now :forbidden
         | 
| 13 | 
            +
                      end
         | 
| 14 | 
            +
                    end
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  before do |context|
         | 
| 18 | 
            +
                    context.authentication
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def on_exception(&block)
         | 
| 23 | 
            +
                  before_exception &block
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 8 26 | 
             
              end
         | 
| 9 27 | 
             
            end
         | 
| @@ -1,79 +1,209 @@ | |
| 1 1 | 
             
            module JSONAPIonify::Api
         | 
| 2 2 | 
             
              module Resource::Definitions::Pagination
         | 
| 3 | 
            -
             | 
| 4 3 | 
             
                class PaginationLinksDelegate
         | 
| 5 4 |  | 
| 6 | 
            -
                  def initialize( | 
| 7 | 
            -
                    @ | 
| 8 | 
            -
                    @ | 
| 5 | 
            +
                  def initialize(url, params, links)
         | 
| 6 | 
            +
                    @url    = url
         | 
| 7 | 
            +
                    @params = params
         | 
| 8 | 
            +
                    @links  = links
         | 
| 9 9 | 
             
                  end
         | 
| 10 10 |  | 
| 11 11 | 
             
                  %i{first last next prev}.each do |method|
         | 
| 12 12 | 
             
                    define_method method do |**options|
         | 
| 13 | 
            -
                      @links[method] = URI.parse(@ | 
| 13 | 
            +
                      @links[method] = URI.parse(@url).tap do |uri|
         | 
| 14 14 | 
             
                        page_params = { page: options }.deep_stringify_keys
         | 
| 15 | 
            -
                        uri.query   = @ | 
| 15 | 
            +
                        uri.query   = @params.deep_merge(page_params).to_param
         | 
| 16 16 | 
             
                      end.to_s
         | 
| 17 17 | 
             
                    end
         | 
| 18 18 | 
             
                  end
         | 
| 19 19 |  | 
| 20 20 | 
             
                end
         | 
| 21 21 |  | 
| 22 | 
            -
                 | 
| 23 | 
            -
                   | 
| 24 | 
            -
                     | 
| 25 | 
            -
             | 
| 26 | 
            -
                     | 
| 27 | 
            -
             | 
| 28 | 
            -
                     | 
| 29 | 
            -
             | 
| 30 | 
            -
                     | 
| 31 | 
            -
             | 
| 32 | 
            -
                     | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 55 | 
            -
             | 
| 22 | 
            +
                def self.extended(klass)
         | 
| 23 | 
            +
                  klass.class_eval do
         | 
| 24 | 
            +
                    include InstanceMethods
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    inherited_hash_attribute :pagination_strategies
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    define_pagination_strategy 'Object' do |collection|
         | 
| 29 | 
            +
                      collection
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                    define_pagination_strategy 'Enumerable' do |collection, params, links, per, context|
         | 
| 33 | 
            +
                      size = Integer(params['first'] || params['last'] || per)
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                      slice =
         | 
| 36 | 
            +
                        if (params['before'] && params['first']) || (params['after'] && params['last'])
         | 
| 37 | 
            +
                          error :forbidden do
         | 
| 38 | 
            +
                            message 'Illegal combination of parameters'
         | 
| 39 | 
            +
                          end
         | 
| 40 | 
            +
                        elsif (after = params['after'])
         | 
| 41 | 
            +
                          key_values = parse_and_validate_cursor(:after, after, context)
         | 
| 42 | 
            +
                          array_select_past_cursor(
         | 
| 43 | 
            +
                            collection,
         | 
| 44 | 
            +
                            context.sort_params,
         | 
| 45 | 
            +
                            key_values
         | 
| 46 | 
            +
                          ).first(size)
         | 
| 47 | 
            +
                        elsif (before = params['before'])
         | 
| 48 | 
            +
                          key_values = parse_and_validate_cursor(:before, before, context)
         | 
| 49 | 
            +
                          array_select_past_cursor(
         | 
| 50 | 
            +
                            collection,
         | 
| 51 | 
            +
                            context.sort_params.reverse,
         | 
| 52 | 
            +
                            key_values
         | 
| 53 | 
            +
                          ).last(size)
         | 
| 54 | 
            +
                        elsif params['last']
         | 
| 55 | 
            +
                          collection.last(size)
         | 
| 56 | 
            +
                        else
         | 
| 57 | 
            +
                          collection.first(size)
         | 
| 58 | 
            +
                        end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                      links.first first: size
         | 
| 61 | 
            +
                      links.last last: size
         | 
| 62 | 
            +
                      links.prev before: build_cursor_from_instance(context.request, slice.first), last: size unless slice.first == collection.first
         | 
| 63 | 
            +
                      links.next after: build_cursor_from_instance(context.request, slice.last), first: size unless slice.last == collection.last
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                      slice
         | 
| 66 | 
            +
                    end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                    define_pagination_strategy 'ActiveRecord::Relation' do |collection, params, links, per, context|
         | 
| 69 | 
            +
                      size = Integer(params['first'] || params['last'] || per)
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                      slice =
         | 
| 72 | 
            +
                        if (params['before'] && params['first']) || (params['after'] && params['last'])
         | 
| 73 | 
            +
                          error :forbidden do
         | 
| 74 | 
            +
                            message 'Illegal combination of parameters'
         | 
| 75 | 
            +
                          end
         | 
| 76 | 
            +
                        elsif (after = params['after'])
         | 
| 77 | 
            +
                          key_values = parse_and_validate_cursor(:after, after, context)
         | 
| 78 | 
            +
                          arel_select_past_cursor(
         | 
| 79 | 
            +
                            collection,
         | 
| 80 | 
            +
                            context.sort_params,
         | 
| 81 | 
            +
                            key_values
         | 
| 82 | 
            +
                          ).limit(size)
         | 
| 83 | 
            +
                        elsif (before = params['before'])
         | 
| 84 | 
            +
                          key_values = parse_and_validate_cursor(:before, before, context)
         | 
| 85 | 
            +
                          ids        = arel_select_past_cursor(
         | 
| 86 | 
            +
                            collection,
         | 
| 87 | 
            +
                            context.sort_params.reverse,
         | 
| 88 | 
            +
                            key_values
         | 
| 89 | 
            +
                          ).reverse_order.limit(size).pluck(:id)
         | 
| 90 | 
            +
                          collection.where(id_attribute => ids)
         | 
| 91 | 
            +
                        elsif params['last']
         | 
| 92 | 
            +
                          ids = collection.reverse_order.limit(size).pluck(id_attribute)
         | 
| 93 | 
            +
                          collection.where(id_attribute => ids).limit(size)
         | 
| 94 | 
            +
                        else
         | 
| 95 | 
            +
                          collection.limit(size)
         | 
| 96 | 
            +
                        end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                      links.first first: size
         | 
| 99 | 
            +
                      links.last last: size
         | 
| 100 | 
            +
                      links.prev before: build_cursor_from_instance(context.request, slice.first), last: size unless slice.first == collection.first
         | 
| 101 | 
            +
                      links.next after: build_cursor_from_instance(context.request, slice.last), first: size unless slice.last == collection.last
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                      slice
         | 
| 104 | 
            +
                    end
         | 
| 56 105 | 
             
                  end
         | 
| 57 | 
            -
                 | 
| 58 | 
            -
             | 
| 59 | 
            -
                 | 
| 106 | 
            +
                end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                def define_pagination_strategy(mod, &block)
         | 
| 109 | 
            +
                  pagination_strategies[mod.to_s] = block
         | 
| 110 | 
            +
                end
         | 
| 60 111 |  | 
| 61 | 
            -
                def  | 
| 62 | 
            -
                   | 
| 63 | 
            -
                   | 
| 112 | 
            +
                def enable_pagination(per: 50)
         | 
| 113 | 
            +
                  param :page, :after, actions: %i{list}
         | 
| 114 | 
            +
                  param :page, :before, actions: %i{list}
         | 
| 115 | 
            +
                  param :page, :first, actions: %i{list}
         | 
| 116 | 
            +
                  param :page, :last, actions: %i{list}
         | 
| 64 117 | 
             
                  context :paginated_collection do |context|
         | 
| 65 | 
            -
                     | 
| 66 | 
            -
             | 
| 67 | 
            -
                       | 
| 118 | 
            +
                    collection = context.sorted_collection
         | 
| 119 | 
            +
                    _, block   = pagination_strategies.to_a.reverse.to_h.find do |mod, _|
         | 
| 120 | 
            +
                      Object.const_defined?(mod, false) && context.collection.class <= Object.const_get(mod, false)
         | 
| 68 121 | 
             
                    end
         | 
| 69 | 
            -
             | 
| 70 | 
            -
             | 
| 122 | 
            +
             | 
| 123 | 
            +
                    links_delegate = PaginationLinksDelegate.new(
         | 
| 124 | 
            +
                      context.request.url,
         | 
| 125 | 
            +
                      self.class.sticky_params(context.params),
         | 
| 126 | 
            +
                      context.links
         | 
| 127 | 
            +
                    )
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                    instance_exec(
         | 
| 130 | 
            +
                      collection,
         | 
| 71 131 | 
             
                      context.request.params['page'] || {},
         | 
| 72 | 
            -
                       | 
| 73 | 
            -
                       | 
| 132 | 
            +
                      links_delegate,
         | 
| 133 | 
            +
                      per,
         | 
| 134 | 
            +
                      context,
         | 
| 135 | 
            +
                      &block
         | 
| 74 136 | 
             
                    )
         | 
| 75 137 | 
             
                  end
         | 
| 76 138 | 
             
                end
         | 
| 77 139 |  | 
| 140 | 
            +
                module InstanceMethods
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                  def array_select_past_cursor(collection, sort_params, key_values)
         | 
| 143 | 
            +
                    sort_params.length.times.map do |i|
         | 
| 144 | 
            +
                      set                             = sort_params[0..i]
         | 
| 145 | 
            +
                      *contains_fields, outside_field = set
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                      # Collect the contains results
         | 
| 148 | 
            +
                      contains_results                = contains_fields.map do |field|
         | 
| 149 | 
            +
                        collection.select do |item|
         | 
| 150 | 
            +
                          value          = item.send(field.name)
         | 
| 151 | 
            +
                          expected_value = key_values[field.name]
         | 
| 152 | 
            +
                          value && value.send(field.contains_operator, expected_value)
         | 
| 153 | 
            +
                        end
         | 
| 154 | 
            +
                      end
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                      # Collect the outside results
         | 
| 157 | 
            +
                      outside_results                 = collection.select do |item|
         | 
| 158 | 
            +
                        value          = item.send(outside_field.name)
         | 
| 159 | 
            +
                        expected_value = key_values[outside_field.name.to_s]
         | 
| 160 | 
            +
                        value && value.send(outside_field.outside_operator, expected_value)
         | 
| 161 | 
            +
                      end
         | 
| 162 | 
            +
             | 
| 163 | 
            +
                      # Finish the query
         | 
| 164 | 
            +
                      [*contains_results, outside_results].reduce(:&)
         | 
| 165 | 
            +
                    end.reduce(:|) || []
         | 
| 166 | 
            +
                  end
         | 
| 167 | 
            +
             | 
| 168 | 
            +
                  def arel_select_past_cursor(collection, sort_params, key_values)
         | 
| 169 | 
            +
                    subselect = sort_params.length.times.map do |i|
         | 
| 170 | 
            +
                      set                             = sort_params[0..i]
         | 
| 171 | 
            +
                      *contains_fields, outside_field = set
         | 
| 172 | 
            +
                      contains_fields.reduce(collection.reorder(nil)) do |relation, field|
         | 
| 173 | 
            +
                        relation.where <<-SQL.strip, value: key_values[field.name.to_s]
         | 
| 174 | 
            +
                          "#{field.name}" #{field.contains_operator} :value
         | 
| 175 | 
            +
                        SQL
         | 
| 176 | 
            +
                      end.where(<<-SQL.strip, key_values[outside_field.name.to_s]).to_sql
         | 
| 177 | 
            +
                        "#{outside_field.name}" #{outside_field.outside_operator} ?
         | 
| 178 | 
            +
                      SQL
         | 
| 179 | 
            +
                    end.join(' UNION ')
         | 
| 180 | 
            +
                    collection.from("(#{subselect}) AS #{collection.table_name}")
         | 
| 181 | 
            +
                  end
         | 
| 182 | 
            +
             | 
| 183 | 
            +
                  def parse_and_validate_cursor(param, cursor, context)
         | 
| 184 | 
            +
                    should_error = false
         | 
| 185 | 
            +
                    options      = JSON.parse(Base64.urlsafe_decode64(cursor))
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                    # Validate Type
         | 
| 188 | 
            +
                    unless options['t'] == self.class.type
         | 
| 189 | 
            +
                      should_error = true
         | 
| 190 | 
            +
                      error(:page_parameter_invalid, :page, param) do
         | 
| 191 | 
            +
                        detail 'The cursor type does not match the resource'
         | 
| 192 | 
            +
                      end
         | 
| 193 | 
            +
                    end
         | 
| 194 | 
            +
             | 
| 195 | 
            +
                    # Validate Sort
         | 
| 196 | 
            +
                    unless options['s'] == context.params['sort']
         | 
| 197 | 
            +
                      should_error = true
         | 
| 198 | 
            +
                      error(:page_parameter_invalid, :page, param) do
         | 
| 199 | 
            +
                        detail 'The cursor sort does not match the request sort'
         | 
| 200 | 
            +
                      end
         | 
| 201 | 
            +
                    end
         | 
| 202 | 
            +
                    raise Errors::RequestError if should_error
         | 
| 203 | 
            +
             | 
| 204 | 
            +
                    options['a']
         | 
| 205 | 
            +
                  end
         | 
| 206 | 
            +
                end
         | 
| 207 | 
            +
             | 
| 78 208 | 
             
              end
         | 
| 79 209 | 
             
            end
         | 
| @@ -6,24 +6,30 @@ module JSONAPIonify::Api | |
| 6 6 | 
             
                    extend JSONAPIonify::InheritedAttributes
         | 
| 7 7 | 
             
                    inherited_hash_attribute :param_definitions
         | 
| 8 8 |  | 
| 9 | 
            -
                    before do |context|
         | 
| 10 | 
            -
                      context.params # pull params so they verify
         | 
| 11 | 
            -
                    end
         | 
| 12 | 
            -
             | 
| 13 9 | 
             
                    context(:params, readonly: true) do |context|
         | 
| 14 | 
            -
                      should_error | 
| 10 | 
            +
                      should_error = false
         | 
| 15 11 |  | 
| 16 | 
            -
                       | 
| 17 | 
            -
                      params          = self.class.param_definitions.select do |_, v|
         | 
| 12 | 
            +
                      params = self.class.param_definitions.select do |_, v|
         | 
| 18 13 | 
             
                        v.actions.blank? || v.actions.include?(action_name)
         | 
| 19 14 | 
             
                      end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                      context.request.params.replace(
         | 
| 17 | 
            +
                        [*params.values.select(&:has_default?).map(&:default), context.request.params].reduce(:deep_merge)
         | 
| 18 | 
            +
                      )
         | 
| 19 | 
            +
             | 
| 20 20 | 
             
                      required_params = params.select do |_, v|
         | 
| 21 21 | 
             
                        v.required
         | 
| 22 22 | 
             
                      end
         | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 23 | 
            +
             | 
| 24 | 
            +
                      # Check for validity
         | 
| 25 | 
            +
                      context.request.params.each do |k, v|
         | 
| 26 | 
            +
                        keypath  = ParamOptions.hash_to_keypaths(k => v)[0]
         | 
| 27 | 
            +
                        reserved = ParamOptions.reserved?(k)
         | 
| 28 | 
            +
                        allowed  = params.keys.include? keypath
         | 
| 29 | 
            +
                        valid    = ParamOptions.valid?(k) || v.is_a?(Hash)
         | 
| 30 | 
            +
                        unless reserved || (allowed && valid)
         | 
| 31 | 
            +
                          should_error = true
         | 
| 32 | 
            +
                          error :parameter_invalid, ParamOptions.keypath_to_string(*keypath)
         | 
| 27 33 | 
             
                        end
         | 
| 28 34 | 
             
                      end
         | 
| 29 35 |  | 
| @@ -32,7 +38,7 @@ module JSONAPIonify::Api | |
| 32 38 | 
             
                        error :parameters_missing, missing_params
         | 
| 33 39 | 
             
                      end
         | 
| 34 40 |  | 
| 35 | 
            -
                      raise  | 
| 41 | 
            +
                      raise Errors::RequestError if should_error
         | 
| 36 42 |  | 
| 37 43 | 
             
                      # Return the params
         | 
| 38 44 | 
             
                      context.request.params
         | 
| @@ -45,5 +51,30 @@ module JSONAPIonify::Api | |
| 45 51 | 
             
                  param_definitions[keypath] = ParamOptions.new(*keypath, **options)
         | 
| 46 52 | 
             
                end
         | 
| 47 53 |  | 
| 54 | 
            +
                def sticky_params(params)
         | 
| 55 | 
            +
                  sticky_param_definitions = param_definitions.values.select(&:sticky)
         | 
| 56 | 
            +
                  ParamOptions.hash_to_keypaths(params).map do |keypath|
         | 
| 57 | 
            +
                    definition = sticky_param_definitions.find do |definition|
         | 
| 58 | 
            +
                      definition.keypath == keypath
         | 
| 59 | 
            +
                    end
         | 
| 60 | 
            +
                    next {} unless definition
         | 
| 61 | 
            +
                    value      = definition.extract_value(params)
         | 
| 62 | 
            +
                    if definition.default_value?(value)
         | 
| 63 | 
            +
                      {}
         | 
| 64 | 
            +
                    else
         | 
| 65 | 
            +
                      definition.with_value(value)
         | 
| 66 | 
            +
                    end
         | 
| 67 | 
            +
                  end.reduce(:deep_merge)
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                  # sticky_param_definitions = param_definitions.values.select(&:sticky)
         | 
| 70 | 
            +
                  # params.each_with_object do |k, v|
         | 
| 71 | 
            +
                  #   definition = sticky_param_definitions.find do |definition|
         | 
| 72 | 
            +
                  #     definition.keypath == ParamOptions.hash_to_keypaths(k => v)[0]
         | 
| 73 | 
            +
                  #   end
         | 
| 74 | 
            +
                  #   binding.pry
         | 
| 75 | 
            +
                  #   definition && !definition.default_value?(v)
         | 
| 76 | 
            +
                  # end
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 48 79 | 
             
              end
         | 
| 49 80 | 
             
            end
         | 
| @@ -6,65 +6,6 @@ module JSONAPIonify::Api | |
| 6 6 | 
             
                    extend JSONAPIonify::InheritedAttributes
         | 
| 7 7 | 
             
                    inherited_hash_attribute :request_header_definitions
         | 
| 8 8 |  | 
| 9 | 
            -
                    # Standard HTTP Headers
         | 
| 10 | 
            -
                    # https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields
         | 
| 11 | 
            -
                    request_header 'accept'
         | 
| 12 | 
            -
                    request_header 'accept-charset'
         | 
| 13 | 
            -
                    request_header 'accept-encoding'
         | 
| 14 | 
            -
                    request_header 'accept-language'
         | 
| 15 | 
            -
                    request_header 'accept-datetime'
         | 
| 16 | 
            -
                    request_header 'authorization'
         | 
| 17 | 
            -
                    request_header 'cache-control'
         | 
| 18 | 
            -
                    request_header 'connection'
         | 
| 19 | 
            -
                    request_header 'cookie'
         | 
| 20 | 
            -
                    request_header 'content-length'
         | 
| 21 | 
            -
                    request_header 'content-md5'
         | 
| 22 | 
            -
                    request_header 'content-type'
         | 
| 23 | 
            -
                    request_header 'date'
         | 
| 24 | 
            -
                    request_header 'expect'
         | 
| 25 | 
            -
                    request_header 'from'
         | 
| 26 | 
            -
                    request_header 'host'
         | 
| 27 | 
            -
                    request_header 'if-match'
         | 
| 28 | 
            -
                    request_header 'if-modified-since'
         | 
| 29 | 
            -
                    request_header 'if-none-match'
         | 
| 30 | 
            -
                    request_header 'if-range'
         | 
| 31 | 
            -
                    request_header 'if-unmodified-since'
         | 
| 32 | 
            -
                    request_header 'max-forwards'
         | 
| 33 | 
            -
                    request_header 'origin'
         | 
| 34 | 
            -
                    request_header 'pragma'
         | 
| 35 | 
            -
                    request_header 'proxy-authorization'
         | 
| 36 | 
            -
                    request_header 'range'
         | 
| 37 | 
            -
                    request_header 'referer'
         | 
| 38 | 
            -
                    request_header 'te'
         | 
| 39 | 
            -
                    request_header 'user-agent'
         | 
| 40 | 
            -
                    request_header 'upgrade'
         | 
| 41 | 
            -
                    request_header 'via'
         | 
| 42 | 
            -
                    request_header 'warning'
         | 
| 43 | 
            -
             | 
| 44 | 
            -
                    # Non-Standard, but widely used HTTP headers
         | 
| 45 | 
            -
                    request_header 'x-requested-with'
         | 
| 46 | 
            -
                    request_header 'dnt'
         | 
| 47 | 
            -
                    request_header 'x-forwarded-for'
         | 
| 48 | 
            -
                    request_header 'x-forwarded-host'
         | 
| 49 | 
            -
                    request_header 'x-forwarded-proto'
         | 
| 50 | 
            -
                    request_header 'front-end-https'
         | 
| 51 | 
            -
                    request_header 'x-att-device-id'
         | 
| 52 | 
            -
                    request_header 'x-wap-profile'
         | 
| 53 | 
            -
                    request_header 'proxy-connection'
         | 
| 54 | 
            -
                    request_header 'x-uidh'
         | 
| 55 | 
            -
                    request_header 'upgrade-insecure-requests'
         | 
| 56 | 
            -
             | 
| 57 | 
            -
                    # Don't allow method overrides
         | 
| 58 | 
            -
                    # request_header 'x-http-method-override'
         | 
| 59 | 
            -
             | 
| 60 | 
            -
                    # Don't allow CSRF tokens, as they should not be used
         | 
| 61 | 
            -
                    # in the api by default
         | 
| 62 | 
            -
                    # request_header 'x-csrf-token'
         | 
| 63 | 
            -
             | 
| 64 | 
            -
                    before do |context|
         | 
| 65 | 
            -
                      context.request_headers # pull request_headers so they verify
         | 
| 66 | 
            -
                    end
         | 
| 67 | 
            -
             | 
| 68 9 | 
             
                    context(:request_headers) do |context|
         | 
| 69 10 | 
             
                      should_error     = false
         | 
| 70 11 |  | 
| @@ -76,19 +17,12 @@ module JSONAPIonify::Api | |
| 76 17 | 
             
                        v.required
         | 
| 77 18 | 
             
                      end
         | 
| 78 19 |  | 
| 79 | 
            -
                      if (invalid_keys = context.request.headers.keys.map(&:downcase) - headers.keys.map(&:downcase)).present?
         | 
| 80 | 
            -
                        should_error = true
         | 
| 81 | 
            -
                        invalid_keys.each do |key|
         | 
| 82 | 
            -
                          error :header_not_permitted, key
         | 
| 83 | 
            -
                        end
         | 
| 84 | 
            -
                      end
         | 
| 85 | 
            -
             | 
| 86 20 | 
             
                      if (missing_keys = required_headers.keys.map(&:downcase) - context.request.headers.keys.map(&:downcase)).present?
         | 
| 87 21 | 
             
                        should_error = true
         | 
| 88 22 | 
             
                        error :headers_missing, missing_keys
         | 
| 89 23 | 
             
                      end
         | 
| 90 24 |  | 
| 91 | 
            -
                      raise  | 
| 25 | 
            +
                      raise Errors::RequestError if should_error
         | 
| 92 26 |  | 
| 93 27 | 
             
                      context.request.headers
         | 
| 94 28 | 
             
                    end
         | 
| @@ -1,17 +1,6 @@ | |
| 1 1 | 
             
            module JSONAPIonify::Api
         | 
| 2 2 | 
             
              module Resource::Definitions::Scopes
         | 
| 3 3 |  | 
| 4 | 
            -
                def self.extended(klass)
         | 
| 5 | 
            -
                  klass.class_eval do
         | 
| 6 | 
            -
                    id :id
         | 
| 7 | 
            -
                    scope { raise NotImplementedError, 'scope not implemented' }
         | 
| 8 | 
            -
                    collection { raise NotImplementedError, 'collection not implemented' }
         | 
| 9 | 
            -
                    instance { raise NotImplementedError, 'instance not implemented' }
         | 
| 10 | 
            -
                    new_instance { raise NotImplementedError, 'new instance not implemented' }
         | 
| 11 | 
            -
                    param :include
         | 
| 12 | 
            -
                  end
         | 
| 13 | 
            -
                end
         | 
| 14 | 
            -
             | 
| 15 4 | 
             
                def scope(&block)
         | 
| 16 5 | 
             
                  define_singleton_method(:current_scope) do
         | 
| 17 6 | 
             
                    Object.new.instance_eval(&block)
         | 
| @@ -25,10 +14,10 @@ module JSONAPIonify::Api | |
| 25 14 |  | 
| 26 15 | 
             
                def instance(&block)
         | 
| 27 16 | 
             
                  define_singleton_method(:find_instance) do |id|
         | 
| 28 | 
            -
                     | 
| 17 | 
            +
                    instance_exec(current_scope, id, OpenStruct.new, &block)
         | 
| 29 18 | 
             
                  end
         | 
| 30 19 | 
             
                  context :instance do |context|
         | 
| 31 | 
            -
                     | 
| 20 | 
            +
                    instance_exec(context.scope, context.id, context, &block)
         | 
| 32 21 | 
             
                  end
         | 
| 33 22 | 
             
                end
         | 
| 34 23 |  |