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
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 515e9924931d33894090d68b518e2fe0e1509ab4
         | 
| 4 | 
            +
              data.tar.gz: eb62428441716cfc4012d90f3b3af2bb6013e158
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 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. | 
| 1 | 
            +
            2.3.0
         | 
    
        data/.travis.yml
    CHANGED
    
    
    
        data/Guardfile
    CHANGED
    
    
    
        data/README.md
    CHANGED
    
    | @@ -1,10 +1,19 @@ | |
| 1 1 | 
             
            # JSONAPIonify
         | 
| 2 | 
            -
            [](https://badge.fury.io/rb/jsonapionify)
         | 
| 3 3 | 
             
            [](https://travis-ci.org/brandfolder/jsonapionify)
         | 
| 4 4 | 
             
            [](https://codeclimate.com/repos/5672446f137f95309c0067c6/feed)
         | 
| 5 5 | 
             
            [](https://codeclimate.com/repos/5672446f137f95309c0067c6/coverage)
         | 
| 6 6 |  | 
| 7 | 
            -
             | 
| 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 | 
            +
              [](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 | 
            -
             | 
| 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 | 
| 8 | 
            -
            docs | 
| 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 | 
| 8 | 
            -
              spec.version | 
| 9 | 
            -
              spec.authors | 
| 10 | 
            -
              spec.email | 
| 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 | 
| 13 | 
            -
              spec.homepage | 
| 14 | 
            -
              spec.license | 
| 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 | 
            -
               | 
| 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. | 
| 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( | 
| 58 | 
            -
                   | 
| 59 | 
            -
             | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 63 | 
            -
                    )
         | 
| 64 | 
            -
                   | 
| 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 | 
| 141 | 
            -
                   | 
| 142 | 
            -
                  cache_options       = {}
         | 
| 144 | 
            +
                  action        = dup
         | 
| 145 | 
            +
                  cache_options = {}
         | 
| 143 146 | 
             
                  resource.new.instance_eval do
         | 
| 144 147 | 
             
                    # Bootstrap the Action
         | 
| 145 | 
            -
                     | 
| 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 | 
            -
             | 
| 165 | 
            -
             | 
| 166 | 
            -
             | 
| 167 | 
            -
             | 
| 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 | 
            -
             | 
| 171 | 
            -
             | 
| 172 | 
            -
             | 
| 175 | 
            +
                    define_singleton_method :action_name do
         | 
| 176 | 
            +
                      action.name
         | 
| 177 | 
            +
                    end
         | 
| 173 178 |  | 
| 174 | 
            -
             | 
| 175 | 
            -
             | 
| 176 | 
            -
             | 
| 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 | 
            -
                       | 
| 182 | 
            -
             | 
| 183 | 
            -
                         | 
| 184 | 
            -
             | 
| 185 | 
            -
                         | 
| 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  | 
| 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  | 
| 211 | 
            +
                    rescue Errors::RequestError
         | 
| 202 212 | 
             
                      error_response
         | 
| 203 | 
            -
                    rescue  | 
| 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 | 
| 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 | 
| 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 | 
| 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 | 
            -
                   | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
                     | 
| 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  | 
| 31 | 
            -
                   | 
| 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  | 
| 35 | 
            -
                   | 
| 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. | 
| 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 | 
            -
                     | 
| 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 | 
            -
                               : | 
| 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  | 
| 12 | 
            -
                   | 
| 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 | 
            -
                   | 
| 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  | 
| 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 | 
            -
                   | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 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
         |