sinja 1.2.1 → 1.2.2
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/README.md +145 -180
- data/lib/sinatra/jsonapi.rb +1 -1
- data/lib/sinja.rb +91 -100
- data/lib/sinja/errors.rb +24 -8
- data/lib/sinja/relationship_routes/has_many.rb +1 -1
- data/lib/sinja/resource.rb +8 -8
- data/lib/sinja/resource_routes.rb +3 -3
- data/lib/sinja/version.rb +1 -1
- data/sinja.gemspec +16 -0
- metadata +16 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: adc1346572a9c1a9ae1d2b85c70931cd329cdd1e
         | 
| 4 | 
            +
              data.tar.gz: 73db963c1a496a98de718d1ea17ebda739d80ad9
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 55dc7ba3bc974625bb2cf65b943ccf8cf57541226f39eb0d66c7e56320c507cf3f8d76d0fddba5605698677f459958ede489477ebc9baed97d82a50c6a1e76f2
         | 
| 7 | 
            +
              data.tar.gz: f75638978ade62140b1306644e4a81a62b18d1a8ce09b388ffa183c6c4a5859d531ce108b71299e5b82e39b3759e6d3c0b381799dc12be0f845a63784ec2280a
         | 
    
        data/README.md
    CHANGED
    
    | @@ -10,19 +10,23 @@ | |
| 10 10 | 
             
            [](https://badge.fury.io/rb/sinja)
         | 
| 11 11 | 
             
            [](https://gemnasium.com/github.com/mwpastore/sinja)
         | 
| 12 12 | 
             
            [](https://travis-ci.org/mwpastore/sinja)
         | 
| 13 | 
            -
             | 
| 13 | 
            +
            [][7]
         | 
| 14 14 | 
             
            [](https://gitter.im/sinja-rb/Lobby)
         | 
| 15 | 
            -
            [](https://ember-community-slackin.herokuapp.com/?channel=-ember-data)
         | 
| 16 15 |  | 
| 17 16 | 
             
            Sinja is a [Sinatra][1] [extension][10] for quickly building [RESTful][11],
         | 
| 18 | 
            -
            [{json:api}][2]- | 
| 19 | 
            -
            [JSONAPI::Serializers][3] gem  | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 17 | 
            +
            [{json:api}][2]-compliant web services, leveraging the excellent
         | 
| 18 | 
            +
            [JSONAPI::Serializers][3] gem for payload serialization. It enhances Sinatra's
         | 
| 19 | 
            +
            DSL to enable resource-, relationship-, and role-centric API development, and
         | 
| 20 | 
            +
            it configures Sinatra with the proper settings, MIME-types, filters,
         | 
| 21 | 
            +
            conditions, and error-handling.
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            There are [many][31] parsing (deserializing) and rendering (serializing) and
         | 
| 24 | 
            +
            so-called "JSON API" libraries available for Ruby, but relatively few that
         | 
| 25 | 
            +
            attempt to correctly implement the entire {json:api} specification, including
         | 
| 26 | 
            +
            routing, request header and query parameter checking, and relationship
         | 
| 27 | 
            +
            side-loading. Sinja lets you focus on the business logic of your applications
         | 
| 28 | 
            +
            without worrying about the specification, and without pulling in a heavy
         | 
| 29 | 
            +
            framework like [Rails][16]. It's lightweight and ORM-agnostic!
         | 
| 26 30 |  | 
| 27 31 | 
             
            <!-- START doctoc generated TOC please keep comment here to allow auto update -->
         | 
| 28 32 | 
             
            <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
         | 
| @@ -30,14 +34,7 @@ the {json:api} specification is). | |
| 30 34 |  | 
| 31 35 | 
             
            - [Synopsis](#synopsis)
         | 
| 32 36 | 
             
            - [Installation](#installation)
         | 
| 33 | 
            -
            - [ | 
| 34 | 
            -
              - [Ol' Blue Eyes is Back](#ol-blue-eyes-is-back)
         | 
| 35 | 
            -
              - [Public APIs](#public-apis)
         | 
| 36 | 
            -
                - [Commonly Used](#commonly-used)
         | 
| 37 | 
            -
                - [Less-Commonly Used](#less-commonly-used)
         | 
| 38 | 
            -
              - [Performance](#performance)
         | 
| 39 | 
            -
              - [Extensions](#extensions)
         | 
| 40 | 
            -
              - [Comparison with JSONAPI::Resources](#comparison-with-jsonapiresources)
         | 
| 37 | 
            +
            - [Ol' Blue Eyes is Back](#ol-blue-eyes-is-back)
         | 
| 41 38 | 
             
            - [Basic Usage](#basic-usage)
         | 
| 42 39 | 
             
              - [Configuration](#configuration)
         | 
| 43 40 | 
             
                - [Sinatra](#sinatra)
         | 
| @@ -45,24 +42,8 @@ the {json:api} specification is). | |
| 45 42 | 
             
              - [Resource Locators](#resource-locators)
         | 
| 46 43 | 
             
              - [Action Helpers](#action-helpers)
         | 
| 47 44 | 
             
                - [`resource`](#resource)
         | 
| 48 | 
            -
                  - [`index {..}` => Array](#index---array)
         | 
| 49 | 
            -
                  - [`show {|id| ..}` => Object](#show-id---object)
         | 
| 50 | 
            -
                  - [`show {..}` => Object](#show---object)
         | 
| 51 | 
            -
                  - [`show_many {|ids| ..}` => Array](#show_many-ids---array)
         | 
| 52 | 
            -
                  - [`create {|attr, id| ..}` => id, Object?](#create-attr-id---id-object)
         | 
| 53 | 
            -
                  - [`create {|attr| ..}` => id, Object](#create-attr---id-object)
         | 
| 54 | 
            -
                  - [`update {|attr| ..}` => Object?](#update-attr---object)
         | 
| 55 | 
            -
                  - [`destroy {..}`](#destroy-)
         | 
| 56 45 | 
             
                - [`has_one`](#has_one)
         | 
| 57 | 
            -
                  - [`pluck {..}` => Object](#pluck---object)
         | 
| 58 | 
            -
                  - [`prune {..}` => TrueClass?](#prune---trueclass)
         | 
| 59 | 
            -
                  - [`graft {|rio| ..}` => TrueClass?](#graft-rio---trueclass)
         | 
| 60 46 | 
             
                - [`has_many`](#has_many)
         | 
| 61 | 
            -
                  - [`fetch {..}` => Array](#fetch---array)
         | 
| 62 | 
            -
                  - [`clear {..}` => TrueClass?](#clear---trueclass)
         | 
| 63 | 
            -
                  - [`replace {|rios| ..}` => TrueClass?](#replace-rios---trueclass)
         | 
| 64 | 
            -
                  - [`merge {|rios| ..}` => TrueClass?](#merge-rios---trueclass)
         | 
| 65 | 
            -
                  - [`subtract {|rios| ..}` => TrueClass?](#subtract-rios---trueclass)
         | 
| 66 47 | 
             
            - [Advanced Usage](#advanced-usage)
         | 
| 67 48 | 
             
              - [Action Helper Hooks & Utilities](#action-helper-hooks--utilities)
         | 
| 68 49 | 
             
              - [Authorization](#authorization)
         | 
| @@ -82,15 +63,19 @@ the {json:api} specification is). | |
| 82 63 | 
             
              - [Side-Unloading Related Resources](#side-unloading-related-resources)
         | 
| 83 64 | 
             
              - [Side-Loading Relationships](#side-loading-relationships)
         | 
| 84 65 | 
             
                - [Avoiding Null Foreign Keys](#avoiding-null-foreign-keys)
         | 
| 85 | 
            -
                  - [Many-to-One](#many-to-one)
         | 
| 86 | 
            -
                  - [One-to-Many](#one-to-many)
         | 
| 87 | 
            -
                  - [Many-to-Many](#many-to-many)
         | 
| 88 66 | 
             
              - [Coalesced Find Requests](#coalesced-find-requests)
         | 
| 89 67 | 
             
              - [Patchless Clients](#patchless-clients)
         | 
| 68 | 
            +
            - [Extensions](#extensions)
         | 
| 69 | 
            +
              - [Sequel](#sequel)
         | 
| 90 70 | 
             
            - [Application Concerns](#application-concerns)
         | 
| 71 | 
            +
              - [Performance](#performance)
         | 
| 72 | 
            +
              - [Public APIs](#public-apis)
         | 
| 73 | 
            +
                - [Commonly Used](#commonly-used)
         | 
| 74 | 
            +
                - [Less-Commonly Used](#less-commonly-used)
         | 
| 91 75 | 
             
              - [Sinja or Sinatra::JSONAPI](#sinja-or-sinatrajsonapi)
         | 
| 92 76 | 
             
              - [Code Organization](#code-organization)
         | 
| 93 77 | 
             
              - [Testing](#testing)
         | 
| 78 | 
            +
            - [Comparison with JSONAPI::Resources](#comparison-with-jsonapiresources)
         | 
| 94 79 | 
             
            - [Development](#development)
         | 
| 95 80 | 
             
            - [Contributing](#contributing)
         | 
| 96 81 | 
             
            - [License](#license)
         | 
| @@ -100,7 +85,6 @@ the {json:api} specification is). | |
| 100 85 | 
             
            ## Synopsis
         | 
| 101 86 |  | 
| 102 87 | 
             
            ```ruby
         | 
| 103 | 
            -
            require 'sinatra'
         | 
| 104 88 | 
             
            require 'sinatra/jsonapi'
         | 
| 105 89 |  | 
| 106 90 | 
             
            resource :posts do
         | 
| @@ -171,36 +155,12 @@ Or install it yourself as: | |
| 171 155 | 
             
            $ gem install sinja
         | 
| 172 156 | 
             
            ```
         | 
| 173 157 |  | 
| 174 | 
            -
            ##  | 
| 175 | 
            -
             | 
| 176 | 
            -
            * ORM-agnostic
         | 
| 177 | 
            -
            * Simple role-based authorization
         | 
| 178 | 
            -
            * To-one and to-many relationships and related resources
         | 
| 179 | 
            -
            * Side-loaded relationships on resource creation and update
         | 
| 180 | 
            -
            * Error-handling
         | 
| 181 | 
            -
              * Conflicts (constraint violations)
         | 
| 182 | 
            -
              * Missing records
         | 
| 183 | 
            -
              * Validation failures
         | 
| 184 | 
            -
            * Filtering, sorting, and paging collections
         | 
| 185 | 
            -
            * Plus all the features of [JSONAPI::Serializers][3]!
         | 
| 186 | 
            -
             | 
| 187 | 
            -
            Its main competitors in the Ruby space are [ActiveModelSerializers][12] (AMS)
         | 
| 188 | 
            -
            with the JsonApi adapter, [JSONAPI::Resources][8] (JR), and
         | 
| 189 | 
            -
            [jsonapi-utils][26], all of which are designed to work with [Rails][16] and
         | 
| 190 | 
            -
            [ActiveRecord][17]/[ActiveModel][18] (although they may work with [Sequel][13]
         | 
| 191 | 
            -
            via [sequel-rails][14] and Sequel's [`:active_model` plugin][15]). Otherwise,
         | 
| 192 | 
            -
            you might use something like Sinatra, [Roda][20], or [Grape][19] with a
         | 
| 193 | 
            -
            (de)serialization library, your own routes, and a ton of boilerplate. The goal
         | 
| 194 | 
            -
            of this extension is to provide most or all of the boilerplate for a Sintara
         | 
| 195 | 
            -
            application and automate the drawing of routes based on the resource
         | 
| 196 | 
            -
            definitions.
         | 
| 197 | 
            -
             | 
| 198 | 
            -
            ### Ol' Blue Eyes is Back
         | 
| 158 | 
            +
            ## Ol' Blue Eyes is Back
         | 
| 199 159 |  | 
| 200 160 | 
             
            The "power" so to speak of implementing this functionality as a Sinatra
         | 
| 201 161 | 
             
            extension is that all of Sinatra's usual features are available within your
         | 
| 202 | 
            -
            resource definitions.  | 
| 203 | 
            -
             | 
| 162 | 
            +
            resource definitions. Action helper blocks get compiled into Sinatra helpers,
         | 
| 163 | 
            +
            and the `resource`, `has_one`, and `has_many` keywords build
         | 
| 204 164 | 
             
            [Sinatra::Namespace][21] blocks. You can manage caching directives, set
         | 
| 205 165 | 
             
            headers, and even `halt` (or `not_found`, although such cases are usually
         | 
| 206 166 | 
             
            handled transparently by returning `nil` values or empty collections from
         | 
| @@ -295,107 +255,11 @@ class App < Sinatra::Base | |
| 295 255 | 
             
            end
         | 
| 296 256 | 
             
            ```
         | 
| 297 257 |  | 
| 298 | 
            -
            ### Public APIs
         | 
| 299 | 
            -
             | 
| 300 | 
            -
            Sinja makes a few APIs public to help you work around edge cases in your
         | 
| 301 | 
            -
            application.
         | 
| 302 | 
            -
             | 
| 303 | 
            -
            #### Commonly Used
         | 
| 304 | 
            -
             | 
| 305 | 
            -
            **can?**
         | 
| 306 | 
            -
            : Takes the symbol of an action helper and returns true if the current user has
         | 
| 307 | 
            -
              access to call that action helper for the current resource using the `role`
         | 
| 308 | 
            -
              helper and role definitions detailed under "Authorization" below.
         | 
| 309 | 
            -
             | 
| 310 | 
            -
            **role?**
         | 
| 311 | 
            -
            : Takes a list of role(s) and returns true if it has members in common with the
         | 
| 312 | 
            -
              current user's role(s).
         | 
| 313 | 
            -
             | 
| 314 | 
            -
            **sideloaded?**
         | 
| 315 | 
            -
            : Returns true if the request was invoked from another action helper.
         | 
| 316 | 
            -
             | 
| 317 | 
            -
            #### Less-Commonly Used
         | 
| 318 | 
            -
             | 
| 319 | 
            -
            These are helpful if you want to add some custom routes to your Sinja
         | 
| 320 | 
            -
            application.
         | 
| 321 | 
            -
             | 
| 322 | 
            -
            **data**
         | 
| 323 | 
            -
            : Returns the `data` key of the deserialized request payload (with symbolized
         | 
| 324 | 
            -
              names).
         | 
| 325 | 
            -
             | 
| 326 | 
            -
            **dedasherize**
         | 
| 327 | 
            -
            : Takes a string or symbol and returns the string or symbol with any and all
         | 
| 328 | 
            -
              dashes transliterated to underscores, and camelCase converted to snake_case.
         | 
| 329 | 
            -
             | 
| 330 | 
            -
            **dedasherize_names**
         | 
| 331 | 
            -
            : Takes a hash and returns the hash with its keys dedasherized (deeply).
         | 
| 332 | 
            -
             | 
| 333 | 
            -
            **serialize_model**
         | 
| 334 | 
            -
            : Takes a model (and optional hash of JSONAPI::Serializers options) and returns
         | 
| 335 | 
            -
              a serialized model.
         | 
| 336 | 
            -
             | 
| 337 | 
            -
            **serialize_model?**
         | 
| 338 | 
            -
            : Takes a model (and optional hash of JSONAPI::Serializers options) and returns
         | 
| 339 | 
            -
              a serialized model if non-`nil`, or the root metadata if present, or a HTTP
         | 
| 340 | 
            -
              status 204.
         | 
| 341 | 
            -
             | 
| 342 | 
            -
            **serialize_models**
         | 
| 343 | 
            -
            : Takes an array of models (and optional hash of JSONAPI::Serializers options)
         | 
| 344 | 
            -
              and returns a serialized collection.
         | 
| 345 | 
            -
             | 
| 346 | 
            -
            **serialize_models?**
         | 
| 347 | 
            -
            : Takes an array of models (and optional hash of JSONAPI::Serializers options)
         | 
| 348 | 
            -
              and returns a serialized collection if non-empty, or the root metadata if
         | 
| 349 | 
            -
              present, or a HTTP status 204.
         | 
| 350 | 
            -
             | 
| 351 | 
            -
            ### Performance
         | 
| 352 | 
            -
             | 
| 353 | 
            -
            Although there is some heavy metaprogramming happening at boot time, the end
         | 
| 354 | 
            -
            result is simply a collection of Sinatra namespaces, routes, filters,
         | 
| 355 | 
            -
            conditions, helpers, etc., and Sinja applications should perform as if you had
         | 
| 356 | 
            -
            written them verbosely. The main caveat is that there are quite a few block
         | 
| 357 | 
            -
            closures, which don't perform as well as normal methods in Ruby. Feedback
         | 
| 358 | 
            -
            welcome.
         | 
| 359 | 
            -
             | 
| 360 | 
            -
            ### Extensions
         | 
| 361 | 
            -
             | 
| 362 | 
            -
            Sinja extensions provide additional helpers, DSL, and configuration, packaging
         | 
| 363 | 
            -
            ORM-specific boilerplate as separate gems. At the moment, the only available
         | 
| 364 | 
            -
            extension is for [Sequel][30], but community contributions are welcome!
         | 
| 365 | 
            -
             | 
| 366 | 
            -
            ### Comparison with JSONAPI::Resources
         | 
| 367 | 
            -
             | 
| 368 | 
            -
            | Feature         | JR                               | Sinja                                             |
         | 
| 369 | 
            -
            | :-------------- | :------------------------------- | :------------------------------------------------ |
         | 
| 370 | 
            -
            | Serializer      | Built-in                         | [JSONAPI::Serializers][3]                         |
         | 
| 371 | 
            -
            | Framework       | Rails                            | Sinatra, but easy to mount within others          |
         | 
| 372 | 
            -
            | Routing         | ActionDispatch::Routing          | Mustermann                                        |
         | 
| 373 | 
            -
            | Caching         | ActiveSupport::Cache             | BYO                                               |
         | 
| 374 | 
            -
            | ORM             | ActiveRecord/ActiveModel         | BYO                                               |
         | 
| 375 | 
            -
            | Authorization   | [Pundit][9]                      | Role-based                                        |
         | 
| 376 | 
            -
            | Immutability    | `immutable` method               | Omit mutator action helpers (e.g. `update`)       |
         | 
| 377 | 
            -
            | Fetchability    | `fetchable_fields` method        | Omit attributes in Serializer                     |
         | 
| 378 | 
            -
            | Creatability    | `creatable_fields` method        | Handle in `create` action helper or Model\*       |
         | 
| 379 | 
            -
            | Updatability    | `updatable_fields` method        | Handle in `update` action helper or Model\*       |
         | 
| 380 | 
            -
            | Sortability     | `sortable_fields` method         | `sort` helper and `:sort_by` option               |
         | 
| 381 | 
            -
            | Default sorting | `default_sort` method            | Set default for `params[:sort]`                   |
         | 
| 382 | 
            -
            | Context         | `context` method                 | Rack middleware (e.g. `env['context']`)           |
         | 
| 383 | 
            -
            | Attributes      | Define in Model and Resource     | Define in Model\* and Serializer                  |
         | 
| 384 | 
            -
            | Formatting      | `:format` attribute keyword      | Define attribute as a method in Serialier         |
         | 
| 385 | 
            -
            | Relationships   | Define in Model and Resource     | Define in Model, Resource, and Serializer         |
         | 
| 386 | 
            -
            | Filters         | `filter(s)` keywords             | `filter` helper and `:filter_by` option           |
         | 
| 387 | 
            -
            | Default filters | `:default` filter keyword        | Set default for `params[:filter]`                 |
         | 
| 388 | 
            -
            | Pagination      | JSONAPI::Paginator               | `page` helper and `page_using` configurable       |
         | 
| 389 | 
            -
            | Meta            | `meta` method                    | Serializer `:meta` option               |
         | 
| 390 | 
            -
            | Primary keys    | `resource_key_type` configurable | Serializer `id` method               |
         | 
| 391 | 
            -
             | 
| 392 | 
            -
            \* - Depending on your ORM.
         | 
| 393 | 
            -
             | 
| 394 258 | 
             
            ## Basic Usage
         | 
| 395 259 |  | 
| 396 260 | 
             
            You'll need a database schema and models (using the engine and ORM of your
         | 
| 397 261 | 
             
            choice) and [serializers][3] to get started. Create a new Sinatra application
         | 
| 398 | 
            -
            (classic or modular) to hold all your {json:api}  | 
| 262 | 
            +
            (classic or modular) to hold all your {json:api} controllers and (if modular)
         | 
| 399 263 | 
             
            register this extension. Instead of defining routes with `get`, `post`, etc. as
         | 
| 400 264 | 
             
            you normally would, define `resource` blocks with action helpers and `has_one`
         | 
| 401 265 | 
             
            and `has_many` relationship blocks (with their own action helpers). Sinja will
         | 
| @@ -417,7 +281,8 @@ these settings. | |
| 417 281 | 
             
            * Registers [Sinatra::Namespace][21] and [Mustermann][25]
         | 
| 418 282 | 
             
            * Disables [Rack::Protection][6] (can be reenabled with `enable :protection` or
         | 
| 419 283 | 
             
              by manually `use`-ing the Rack::Protection middleware)
         | 
| 420 | 
            -
            * Disables static file routes (can be reenabled with `enable :static | 
| 284 | 
            +
            * Disables static file routes (can be reenabled with `enable :static`; be sure
         | 
| 285 | 
            +
              to reenable Rack::Protection::PathTraversal as well)
         | 
| 421 286 | 
             
            * Disables "classy" error pages (in favor of "classy" {json:api} error documents)
         | 
| 422 287 | 
             
            * Adds an `:api_json` MIME-type (`application/vnd.api+json`)
         | 
| 423 288 | 
             
            * Enforces strict checking of the `Accept` and `Content-Type` request headers
         | 
| @@ -440,12 +305,12 @@ their defaults shown): | |
| 440 305 | 
             
            configure_jsonapi do |c|
         | 
| 441 306 | 
             
              #c.conflict_exceptions = [] # see "Conflicts" below
         | 
| 442 307 |  | 
| 308 | 
            +
              #c.not_found_exceptions = [] # see "Missing Records" below
         | 
| 309 | 
            +
             | 
| 443 310 | 
             
              # see "Validations" below
         | 
| 444 311 | 
             
              #c.validation_exceptions = []
         | 
| 445 312 | 
             
              #c.validation_formatter = ->{ [] }
         | 
| 446 313 |  | 
| 447 | 
            -
              #c.not_found_exceptions = [] # see "Missing Records" below
         | 
| 448 | 
            -
             | 
| 449 314 | 
             
              # see "Authorization" below
         | 
| 450 315 | 
             
              #c.default_roles = {}
         | 
| 451 316 | 
             
              #c.default_has_one_roles = {}
         | 
| @@ -481,8 +346,9 @@ Much of Sinja's advanced functionality (e.g. updating and destroying resources, | |
| 481 346 | 
             
            relationship routes) is dependent upon its ability to locate the corresponding
         | 
| 482 347 | 
             
            resource for a request. To enable these features, define an ordinary helper
         | 
| 483 348 | 
             
            method named `find` in your resource definition that takes a single ID argument
         | 
| 484 | 
            -
            and returns the corresponding object. Once defined, a `resource` object will
         | 
| 485 | 
            -
             | 
| 349 | 
            +
            and returns the corresponding object. Once defined, a `resource` object will be
         | 
| 350 | 
            +
            made available in any action helpers that operate on a single (parent)
         | 
| 351 | 
            +
            resource.
         | 
| 486 352 |  | 
| 487 353 | 
             
            ```ruby
         | 
| 488 354 | 
             
            resource :posts do
         | 
| @@ -574,13 +440,6 @@ any given resource block.) | |
| 574 440 | 
             
            Take an array of IDs and return an equally-lengthed array of objects to
         | 
| 575 441 | 
             
            serialize on the response. See "Coalesced Find Requests" below.
         | 
| 576 442 |  | 
| 577 | 
            -
            ##### `create {|attr, id| ..}` => id, Object?
         | 
| 578 | 
            -
             | 
| 579 | 
            -
            With client-generated IDs: Take a hash of (dedasherized) attributes and a
         | 
| 580 | 
            -
            client-generated ID, create a new resource, and return the ID and optionally
         | 
| 581 | 
            -
            the created resource. (Note that only one or the other `create` action helpers
         | 
| 582 | 
            -
            is allowed in any given resource block.)
         | 
| 583 | 
            -
             | 
| 584 443 | 
             
            ##### `create {|attr| ..}` => id, Object
         | 
| 585 444 |  | 
| 586 445 | 
             
            Without client-generated IDs: Take a hash of (dedasherized) attributes, create
         | 
| @@ -588,6 +447,13 @@ a new resource, and return the server-generated ID and the created resource. | |
| 588 447 | 
             
            (Note that only one or the other `create` action helpers is allowed in any
         | 
| 589 448 | 
             
            given resource block.)
         | 
| 590 449 |  | 
| 450 | 
            +
            ##### `create {|attr, id| ..}` => id, Object?
         | 
| 451 | 
            +
             | 
| 452 | 
            +
            With client-generated IDs: Take a hash of (dedasherized) attributes and a
         | 
| 453 | 
            +
            client-generated ID, create a new resource, and return the ID and optionally
         | 
| 454 | 
            +
            the created resource. (Note that only one or the other `create` action helpers
         | 
| 455 | 
            +
            is allowed in any given resource block.)
         | 
| 456 | 
            +
             | 
| 591 457 | 
             
            ##### `update {|attr| ..}` => Object?
         | 
| 592 458 |  | 
| 593 459 | 
             
            Take a hash of (dedasherized) attributes, update `resource`, and optionally
         | 
| @@ -885,8 +751,8 @@ The {json:api} specification states that any unhandled query parameters should | |
| 885 751 | 
             
            cause the request to abort with HTTP status 400. To enforce this requirement,
         | 
| 886 752 | 
             
            Sinja maintains a global "whitelist" of acceptable query parameters as well as
         | 
| 887 753 | 
             
            a per-route whitelist, and interrogates your application to see which features
         | 
| 888 | 
            -
            it supports; for example, a route may allow a `filter` query | 
| 889 | 
            -
            may not have defined a `filter` helper.
         | 
| 754 | 
            +
            it supports; for example, a route may generally allow a `filter` query
         | 
| 755 | 
            +
            parameter, but you may not have defined a `filter` helper.
         | 
| 890 756 |  | 
| 891 757 | 
             
            To let a custom query parameter through to the standard action helpers, add it
         | 
| 892 758 | 
             
            to the `query_params` configurable with a `nil` value:
         | 
| @@ -1235,7 +1101,6 @@ end | |
| 1235 1101 | 
             
            The following matrix outlines which combinations of action helpers and
         | 
| 1236 1102 | 
             
            `:sideload_on` options enable which behaviors:
         | 
| 1237 1103 |  | 
| 1238 | 
            -
            <small>
         | 
| 1239 1104 | 
             
            <table>
         | 
| 1240 1105 | 
             
            <thead>
         | 
| 1241 1106 | 
             
            <tr>
         | 
| @@ -1274,7 +1139,6 @@ The following matrix outlines which combinations of action helpers and | |
| 1274 1139 | 
             
            </tr>
         | 
| 1275 1140 | 
             
            </tbody>
         | 
| 1276 1141 | 
             
            </table>
         | 
| 1277 | 
            -
            </small>
         | 
| 1278 1142 |  | 
| 1279 1143 | 
             
            #### Avoiding Null Foreign Keys
         | 
| 1280 1144 |  | 
| @@ -1428,14 +1292,86 @@ class MyApp < Sinatra::Base | |
| 1428 1292 | 
             
            end
         | 
| 1429 1293 | 
             
            ```
         | 
| 1430 1294 |  | 
| 1295 | 
            +
            ## Extensions
         | 
| 1296 | 
            +
             | 
| 1297 | 
            +
            Sinja extensions provide additional helpers, DSL, and ORM-specific boilerplate
         | 
| 1298 | 
            +
            as separate gems. Community contributions welcome!
         | 
| 1299 | 
            +
             | 
| 1300 | 
            +
            ### Sequel
         | 
| 1301 | 
            +
             | 
| 1302 | 
            +
            Please see [Sinja::Sequel][30] for more information.
         | 
| 1303 | 
            +
             | 
| 1431 1304 | 
             
            ## Application Concerns
         | 
| 1432 1305 |  | 
| 1306 | 
            +
            ### Performance
         | 
| 1307 | 
            +
             | 
| 1308 | 
            +
            Although there is some heavy metaprogramming happening at boot time, the end
         | 
| 1309 | 
            +
            result is simply a collection of Sinatra namespaces, routes, filters,
         | 
| 1310 | 
            +
            conditions, helpers, etc., and Sinja applications should perform as if you had
         | 
| 1311 | 
            +
            written them verbosely. The main caveat is that there are quite a few block
         | 
| 1312 | 
            +
            closures, which don't perform as well as normal methods in Ruby. Feedback
         | 
| 1313 | 
            +
            welcome.
         | 
| 1314 | 
            +
             | 
| 1315 | 
            +
            ### Public APIs
         | 
| 1316 | 
            +
             | 
| 1317 | 
            +
            Sinja makes a few APIs public to help you work around edge cases in your
         | 
| 1318 | 
            +
            application.
         | 
| 1319 | 
            +
             | 
| 1320 | 
            +
            #### Commonly Used
         | 
| 1321 | 
            +
             | 
| 1322 | 
            +
            **can?**
         | 
| 1323 | 
            +
            : Takes the symbol of an action helper and returns true if the current user has
         | 
| 1324 | 
            +
              access to call that action helper for the current resource using the `role`
         | 
| 1325 | 
            +
              helper and role definitions detailed under "Authorization" below.
         | 
| 1326 | 
            +
             | 
| 1327 | 
            +
            **role?**
         | 
| 1328 | 
            +
            : Takes a list of role(s) and returns true if it has members in common with the
         | 
| 1329 | 
            +
              current user's role(s).
         | 
| 1330 | 
            +
             | 
| 1331 | 
            +
            **sideloaded?**
         | 
| 1332 | 
            +
            : Returns true if the request was invoked from another action helper.
         | 
| 1333 | 
            +
             | 
| 1334 | 
            +
            #### Less-Commonly Used
         | 
| 1335 | 
            +
             | 
| 1336 | 
            +
            These are helpful if you want to add some custom routes to your Sinja
         | 
| 1337 | 
            +
            application.
         | 
| 1338 | 
            +
             | 
| 1339 | 
            +
            **data**
         | 
| 1340 | 
            +
            : Returns the `data` key of the deserialized request payload (with symbolized
         | 
| 1341 | 
            +
              names).
         | 
| 1342 | 
            +
             | 
| 1343 | 
            +
            **dedasherize**
         | 
| 1344 | 
            +
            : Takes a string or symbol and returns the string or symbol with any and all
         | 
| 1345 | 
            +
              dashes transliterated to underscores, and camelCase converted to snake_case.
         | 
| 1346 | 
            +
             | 
| 1347 | 
            +
            **dedasherize_names**
         | 
| 1348 | 
            +
            : Takes a hash and returns the hash with its keys dedasherized (deeply).
         | 
| 1349 | 
            +
             | 
| 1350 | 
            +
            **serialize_model**
         | 
| 1351 | 
            +
            : Takes a model (and optional hash of JSONAPI::Serializers options) and returns
         | 
| 1352 | 
            +
              a serialized model.
         | 
| 1353 | 
            +
             | 
| 1354 | 
            +
            **serialize_model?**
         | 
| 1355 | 
            +
            : Takes a model (and optional hash of JSONAPI::Serializers options) and returns
         | 
| 1356 | 
            +
              a serialized model if non-`nil`, or the root metadata if present, or a HTTP
         | 
| 1357 | 
            +
              status 204.
         | 
| 1358 | 
            +
             | 
| 1359 | 
            +
            **serialize_models**
         | 
| 1360 | 
            +
            : Takes an array of models (and optional hash of JSONAPI::Serializers options)
         | 
| 1361 | 
            +
              and returns a serialized collection.
         | 
| 1362 | 
            +
             | 
| 1363 | 
            +
            **serialize_models?**
         | 
| 1364 | 
            +
            : Takes an array of models (and optional hash of JSONAPI::Serializers options)
         | 
| 1365 | 
            +
              and returns a serialized collection if non-empty, or the root metadata if
         | 
| 1366 | 
            +
              present, or a HTTP status 204.
         | 
| 1367 | 
            +
             | 
| 1433 1368 | 
             
            ### Sinja or Sinatra::JSONAPI
         | 
| 1434 1369 |  | 
| 1435 1370 | 
             
            Everything is dual-namespaced under both Sinatra::JSONAPI and Sinja, and Sinja
         | 
| 1436 1371 | 
             
            requires Sinatra::Base, so this:
         | 
| 1437 1372 |  | 
| 1438 1373 | 
             
            ```ruby
         | 
| 1374 | 
            +
            require 'sinatra/base'
         | 
| 1439 1375 | 
             
            require 'sinatra/jsonapi'
         | 
| 1440 1376 |  | 
| 1441 1377 | 
             
            class App < Sinatra::Base
         | 
| @@ -1527,6 +1463,34 @@ applications will behave according to the {json:api} spec (as long as you | |
| 1527 1463 | 
             
            follow the usage documented in this README) and focus on testing your business
         | 
| 1528 1464 | 
             
            logic.
         | 
| 1529 1465 |  | 
| 1466 | 
            +
            ## Comparison with JSONAPI::Resources
         | 
| 1467 | 
            +
             | 
| 1468 | 
            +
            | Feature         | JR                               | Sinja                                             |
         | 
| 1469 | 
            +
            | :-------------- | :------------------------------- | :------------------------------------------------ |
         | 
| 1470 | 
            +
            | Serializer      | Built-in                         | [JSONAPI::Serializers][3]                         |
         | 
| 1471 | 
            +
            | Framework       | Rails                            | Sinatra, but easy to mount within others          |
         | 
| 1472 | 
            +
            | Routing         | ActionDispatch::Routing          | Mustermann                                        |
         | 
| 1473 | 
            +
            | Caching         | ActiveSupport::Cache             | BYO                                               |
         | 
| 1474 | 
            +
            | ORM             | ActiveRecord/ActiveModel         | BYO                                               |
         | 
| 1475 | 
            +
            | Authorization   | [Pundit][9]                      | Role-based                                        |
         | 
| 1476 | 
            +
            | Immutability    | `immutable` method               | Omit mutator action helpers (e.g. `update`)       |
         | 
| 1477 | 
            +
            | Fetchability    | `fetchable_fields` method        | Omit attributes in Serializer                     |
         | 
| 1478 | 
            +
            | Creatability    | `creatable_fields` method        | Handle in `create` action helper or Model\*       |
         | 
| 1479 | 
            +
            | Updatability    | `updatable_fields` method        | Handle in `update` action helper or Model\*       |
         | 
| 1480 | 
            +
            | Sortability     | `sortable_fields` method         | `sort` helper and `:sort_by` option               |
         | 
| 1481 | 
            +
            | Default sorting | `default_sort` method            | Set default for `params[:sort]`                   |
         | 
| 1482 | 
            +
            | Context         | `context` method                 | Rack middleware (e.g. `env['context']`)           |
         | 
| 1483 | 
            +
            | Attributes      | Define in Model and Resource     | Define in Model\* and Serializer                  |
         | 
| 1484 | 
            +
            | Formatting      | `:format` attribute keyword      | Define attribute as a method in Serialier         |
         | 
| 1485 | 
            +
            | Relationships   | Define in Model and Resource     | Define in Model, Resource, and Serializer         |
         | 
| 1486 | 
            +
            | Filters         | `filter(s)` keywords             | `filter` helper and `:filter_by` option           |
         | 
| 1487 | 
            +
            | Default filters | `:default` filter keyword        | Set default for `params[:filter]`                 |
         | 
| 1488 | 
            +
            | Pagination      | JSONAPI::Paginator               | `page` helper and `page_using` configurable       |
         | 
| 1489 | 
            +
            | Meta            | `meta` method                    | Serializer `:meta` option               |
         | 
| 1490 | 
            +
            | Primary keys    | `resource_key_type` configurable | Serializer `id` method               |
         | 
| 1491 | 
            +
             | 
| 1492 | 
            +
            \* – Depending on your ORM.
         | 
| 1493 | 
            +
             | 
| 1530 1494 | 
             
            ## Development
         | 
| 1531 1495 |  | 
| 1532 1496 | 
             
            After checking out the repo, run `bin/setup` to install dependencies. Then, run
         | 
| @@ -1555,7 +1519,7 @@ License](http://opensource.org/licenses/MIT). | |
| 1555 1519 | 
             
            [4]: http://www.rubydoc.info/github/rack/rack/master/Rack/URLMap
         | 
| 1556 1520 | 
             
            [5]: http://rodauth.jeremyevans.net
         | 
| 1557 1521 | 
             
            [6]: https://github.com/sinatra/sinatra/tree/master/rack-protection
         | 
| 1558 | 
            -
            [7]: http://jsonapi.org/format/
         | 
| 1522 | 
            +
            [7]: http://jsonapi.org/format/1.0/
         | 
| 1559 1523 | 
             
            [8]: https://github.com/cerebris/jsonapi-resources
         | 
| 1560 1524 | 
             
            [9]: https://github.com/cerebris/jsonapi-resources#authorization
         | 
| 1561 1525 | 
             
            [10]: http://www.sinatrarb.com/extensions-wild.html
         | 
| @@ -1574,8 +1538,9 @@ License](http://opensource.org/licenses/MIT). | |
| 1574 1538 | 
             
            [23]: http://jsonapi.org/recommendations/#patchless-clients
         | 
| 1575 1539 | 
             
            [24]: http://www.rubydoc.info/github/rack/rack/Rack/MethodOverride
         | 
| 1576 1540 | 
             
            [25]: http://www.sinatrarb.com/mustermann/
         | 
| 1577 | 
            -
            [26]: https://github. | 
| 1541 | 
            +
            [26]: https://jsonapi-suite.github.io/jsonapi_suite/
         | 
| 1578 1542 | 
             
            [27]: https://github.com/coryodaniel/munson
         | 
| 1579 1543 | 
             
            [28]: https://github.com/chingor13/json_api_client
         | 
| 1580 1544 | 
             
            [29]: https://github.com/brynary/rack-test
         | 
| 1581 1545 | 
             
            [30]: https://github.com/mwpastore/sinja-sequel
         | 
| 1546 | 
            +
            [31]: http://jsonapi.org/implementations/#server-libraries-ruby
         | 
    
        data/lib/sinatra/jsonapi.rb
    CHANGED
    
    
    
        data/lib/sinja.rb
    CHANGED
    
    | @@ -14,71 +14,11 @@ require 'sinja/version' | |
| 14 14 |  | 
| 15 15 | 
             
            module Sinja
         | 
| 16 16 | 
             
              MIME_TYPE = 'application/vnd.api+json'
         | 
| 17 | 
            -
              ERROR_CODES =  | 
| 18 | 
            -
                 | 
| 19 | 
            -
                 | 
| 20 | 
            -
                 | 
| 21 | 
            -
                 | 
| 22 | 
            -
                NotAcceptableError,
         | 
| 23 | 
            -
                ConflictError,
         | 
| 24 | 
            -
                UnsupportedTypeError
         | 
| 25 | 
            -
              ].map! { |c| [c.new.http_status, c] }.to_h.tap do |h|
         | 
| 26 | 
            -
                h[422] = UnprocessibleEntityError
         | 
| 27 | 
            -
              end.freeze
         | 
| 28 | 
            -
             | 
| 29 | 
            -
              def resource(resource_name, konst=nil, &block)
         | 
| 30 | 
            -
                abort "Must supply proc constant or block for `resource'" \
         | 
| 31 | 
            -
                  unless block = (konst if konst.is_a?(Proc)) || block
         | 
| 32 | 
            -
             | 
| 33 | 
            -
                resource_name = resource_name.to_s
         | 
| 34 | 
            -
                  .pluralize
         | 
| 35 | 
            -
                  .dasherize
         | 
| 36 | 
            -
                  .to_sym
         | 
| 37 | 
            -
             | 
| 38 | 
            -
                # trigger default procs
         | 
| 39 | 
            -
                config = _sinja.resource_config[resource_name]
         | 
| 40 | 
            -
             | 
| 41 | 
            -
                namespace "/#{resource_name}" do
         | 
| 42 | 
            -
                  define_singleton_method(:_resource_config) { config }
         | 
| 43 | 
            -
                  define_singleton_method(:resource_config) { config[:resource] }
         | 
| 44 | 
            -
             | 
| 45 | 
            -
                  helpers do
         | 
| 46 | 
            -
                    define_method(:sanity_check!) do |*args|
         | 
| 47 | 
            -
                      super(resource_name, *args)
         | 
| 48 | 
            -
                    end
         | 
| 49 | 
            -
                  end
         | 
| 50 | 
            -
             | 
| 51 | 
            -
                  before %r{/(?<id>[^/]+)(?:/.+)?} do |id|
         | 
| 52 | 
            -
                    self.resource =
         | 
| 53 | 
            -
                      if env.key?('sinja.resource')
         | 
| 54 | 
            -
                        env['sinja.resource']
         | 
| 55 | 
            -
                      elsif respond_to?(:find)
         | 
| 56 | 
            -
                        find(id)
         | 
| 57 | 
            -
                      end
         | 
| 58 | 
            -
             | 
| 59 | 
            -
                    raise NotFoundError, "Resource '#{id}' not found" unless resource
         | 
| 60 | 
            -
                  end
         | 
| 61 | 
            -
             | 
| 62 | 
            -
                  register Resource
         | 
| 63 | 
            -
             | 
| 64 | 
            -
                  instance_eval(&block)
         | 
| 65 | 
            -
                end
         | 
| 66 | 
            -
              end
         | 
| 67 | 
            -
             | 
| 68 | 
            -
              alias_method :resources, :resource
         | 
| 69 | 
            -
             | 
| 70 | 
            -
              def sinja
         | 
| 71 | 
            -
                if block_given?
         | 
| 72 | 
            -
                  yield _sinja
         | 
| 73 | 
            -
                else
         | 
| 74 | 
            -
                  _sinja
         | 
| 75 | 
            -
                end
         | 
| 76 | 
            -
              end
         | 
| 77 | 
            -
             | 
| 78 | 
            -
              alias_method :configure_jsonapi, :sinja
         | 
| 79 | 
            -
              def freeze_jsonapi
         | 
| 80 | 
            -
                _sinja.freeze
         | 
| 81 | 
            -
              end
         | 
| 17 | 
            +
              ERROR_CODES = ObjectSpace.each_object(Class).to_a
         | 
| 18 | 
            +
                .keep_if { |klass| klass < HttpError }
         | 
| 19 | 
            +
                .map! { |c| [(c.const_get(:HTTP_STATUS) rescue nil), c] }
         | 
| 20 | 
            +
                .delete_if { |a| a.first.nil? }
         | 
| 21 | 
            +
                .to_h.freeze
         | 
| 82 22 |  | 
| 83 23 | 
             
              def self.registered(app)
         | 
| 84 24 | 
             
                app.register Mustermann if Sinatra::VERSION[/^\d+/].to_i < 2
         | 
| @@ -101,20 +41,23 @@ module Sinja | |
| 101 41 | 
             
                  end
         | 
| 102 42 | 
             
                end
         | 
| 103 43 |  | 
| 104 | 
            -
                app.set : | 
| 44 | 
            +
                app.set :qcaptures do |*index|
         | 
| 105 45 | 
             
                  condition do
         | 
| 106 46 | 
             
                    @qcaptures ||= []
         | 
| 47 | 
            +
             | 
| 107 48 | 
             
                    index.to_h.all? do |key, subkeys|
         | 
| 108 | 
            -
                       | 
| 109 | 
            -
             | 
| 110 | 
            -
             | 
| 111 | 
            -
             | 
| 112 | 
            -
             | 
| 113 | 
            -
             | 
| 114 | 
            -
                         | 
| 115 | 
            -
             | 
| 116 | 
            -
             | 
| 117 | 
            -
                         | 
| 49 | 
            +
                      key = key.to_s
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                      Hash === params[key] && params[key].any? && [*subkeys].all? do |subkey|
         | 
| 52 | 
            +
                        subkey = subkey.to_s
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                        # TODO: What if deleting one is successful, but not another?
         | 
| 55 | 
            +
                        # We'll need to restore the hash to its original state.
         | 
| 56 | 
            +
                        @qcaptures << params[key].delete(subkey) if params[key].key?(subkey)
         | 
| 57 | 
            +
                      end.tap do |ok|
         | 
| 58 | 
            +
                        # If us deleting key(s) causes the hash to become empty, delete it.
         | 
| 59 | 
            +
                        params.delete(key) if ok && params[key].empty?
         | 
| 60 | 
            +
                      end
         | 
| 118 61 | 
             
                    end
         | 
| 119 62 | 
             
                  end
         | 
| 120 63 | 
             
                end
         | 
| @@ -132,7 +75,7 @@ module Sinja | |
| 132 75 | 
             
                      # Ignore interal Sinatra query parameters (e.g. :captures) and any
         | 
| 133 76 | 
             
                      # "known" query parameter set to `nil' in the configurable.
         | 
| 134 77 | 
             
                      next if !env['rack.request.query_hash'].key?(key.to_s) ||
         | 
| 135 | 
            -
                        settings._sinja.query_params.fetch(key, : | 
| 78 | 
            +
                        settings._sinja.query_params.fetch(key, :__NOT_FOUND__).nil?
         | 
| 136 79 |  | 
| 137 80 | 
             
                      raise BadRequestError, "`#{key}' query parameter not allowed" \
         | 
| 138 81 | 
             
                        unless allow_params.include?(key)
         | 
| @@ -148,13 +91,13 @@ module Sinja | |
| 148 91 |  | 
| 149 92 | 
             
                    return true if env['sinja.normalized'] == params.object_id
         | 
| 150 93 |  | 
| 151 | 
            -
                    settings._sinja.query_params.each do |key,  | 
| 152 | 
            -
                      next if  | 
| 94 | 
            +
                    settings._sinja.query_params.each do |key, default_value|
         | 
| 95 | 
            +
                      next if default_value.nil?
         | 
| 153 96 |  | 
| 154 97 | 
             
                      if respond_to?("normalize_#{key}_params")
         | 
| 155 | 
            -
                        params[key.to_s] = send("normalize_#{key}_params")
         | 
| 98 | 
            +
                        params[key.to_s] = send("normalize_#{key}_params", default_value)
         | 
| 156 99 | 
             
                      else
         | 
| 157 | 
            -
                        params[key.to_s] ||=  | 
| 100 | 
            +
                        params[key.to_s] ||= default_value
         | 
| 158 101 | 
             
                      end
         | 
| 159 102 | 
             
                    end
         | 
| 160 103 |  | 
| @@ -186,6 +129,8 @@ module Sinja | |
| 186 129 |  | 
| 187 130 | 
             
                  if method_defined?(:bad_request?)
         | 
| 188 131 | 
             
                    # This screws up our error-handling logic in Sinatra 2.0, so monkeypatch it.
         | 
| 132 | 
            +
                    # https://github.com/sinatra/sinatra/issues/1211
         | 
| 133 | 
            +
                    # https://github.com/sinatra/sinatra/pull/1212
         | 
| 189 134 | 
             
                    def bad_request?
         | 
| 190 135 | 
             
                      false
         | 
| 191 136 | 
             
                    end
         | 
| @@ -209,8 +154,8 @@ module Sinja | |
| 209 154 | 
             
                    end
         | 
| 210 155 | 
             
                  end
         | 
| 211 156 |  | 
| 212 | 
            -
                  def normalize_filter_params
         | 
| 213 | 
            -
                    return  | 
| 157 | 
            +
                  def normalize_filter_params(default_value)
         | 
| 158 | 
            +
                    return default_value unless params[:filter]&.any?
         | 
| 214 159 |  | 
| 215 160 | 
             
                    raise BadRequestError, "Unsupported `filter' query parameter(s)" \
         | 
| 216 161 | 
             
                      unless respond_to?(:filter)
         | 
| @@ -230,7 +175,7 @@ module Sinja | |
| 230 175 | 
             
                    raise BadRequestError, "Invalid `filter' query parameter(s)"
         | 
| 231 176 | 
             
                  end
         | 
| 232 177 |  | 
| 233 | 
            -
                  def normalize_sort_params
         | 
| 178 | 
            +
                  def normalize_sort_params(_default_value)
         | 
| 234 179 | 
             
                    return {} unless params[:sort]&.any?
         | 
| 235 180 |  | 
| 236 181 | 
             
                    raise BadRequestError, "Unsupported `sort' query parameter(s)" \
         | 
| @@ -252,8 +197,8 @@ module Sinja | |
| 252 197 | 
             
                    raise BadRequestError, "Invalid `sort' query parameter(s)"
         | 
| 253 198 | 
             
                  end
         | 
| 254 199 |  | 
| 255 | 
            -
                  def normalize_page_params
         | 
| 256 | 
            -
                    return  | 
| 200 | 
            +
                  def normalize_page_params(default_value)
         | 
| 201 | 
            +
                    return default_value unless params[:page]&.any?
         | 
| 257 202 |  | 
| 258 203 | 
             
                    raise BadRequestError, "Unsupported `page' query parameter(s)" \
         | 
| 259 204 | 
             
                      unless respond_to?(:page)
         | 
| @@ -267,25 +212,17 @@ module Sinja | |
| 267 212 | 
             
                    return if params[:page].empty?
         | 
| 268 213 |  | 
| 269 214 | 
             
                    return params[:page] \
         | 
| 270 | 
            -
                      if params[:page].keys | 
| 215 | 
            +
                      if (params[:page].keys - settings._sinja.page_using.keys).empty?
         | 
| 271 216 |  | 
| 272 217 | 
             
                    raise BadRequestError, "Invalid `page' query parameter(s)"
         | 
| 273 218 | 
             
                  end
         | 
| 274 219 |  | 
| 275 220 | 
             
                  def filter_sort_page?(action)
         | 
| 276 | 
            -
                    return enum_for(__callee__, action) | 
| 221 | 
            +
                    return enum_for(__callee__, action) unless block_given?
         | 
| 277 222 |  | 
| 278 | 
            -
                    if filter = filter_by?(action)
         | 
| 279 | 
            -
             | 
| 280 | 
            -
                    end
         | 
| 281 | 
            -
             | 
| 282 | 
            -
                    if sort = sort_by?(action)
         | 
| 283 | 
            -
                      yield :sort, sort
         | 
| 284 | 
            -
                    end
         | 
| 285 | 
            -
             | 
| 286 | 
            -
                    if page = page_using?
         | 
| 287 | 
            -
                      yield :page, page
         | 
| 288 | 
            -
                    end
         | 
| 223 | 
            +
                    if filter = filter_by?(action) then yield :filter, filter end
         | 
| 224 | 
            +
                    if sort = sort_by?(action) then yield :sort, sort end
         | 
| 225 | 
            +
                    if page = page_using? then yield :page, page end
         | 
| 289 226 | 
             
                  end
         | 
| 290 227 |  | 
| 291 228 | 
             
                  def filter_sort_page(collection, opts)
         | 
| @@ -347,7 +284,7 @@ module Sinja | |
| 347 284 | 
             
                end
         | 
| 348 285 |  | 
| 349 286 | 
             
                app.after do
         | 
| 350 | 
            -
                  body serialize_response_body if response. | 
| 287 | 
            +
                  body serialize_response_body if response.successful?
         | 
| 351 288 | 
             
                end
         | 
| 352 289 |  | 
| 353 290 | 
             
                app.not_found do
         | 
| @@ -376,6 +313,60 @@ module Sinja | |
| 376 313 | 
             
                end
         | 
| 377 314 | 
             
              end
         | 
| 378 315 |  | 
| 316 | 
            +
              def resource(resource_name, konst=nil, &block)
         | 
| 317 | 
            +
                abort "Must supply proc constant or block for `resource'" \
         | 
| 318 | 
            +
                  unless block = (konst if konst.is_a?(Proc)) || block
         | 
| 319 | 
            +
             | 
| 320 | 
            +
                resource_name = resource_name.to_s
         | 
| 321 | 
            +
                  .pluralize
         | 
| 322 | 
            +
                  .dasherize
         | 
| 323 | 
            +
                  .to_sym
         | 
| 324 | 
            +
             | 
| 325 | 
            +
                # trigger default procs
         | 
| 326 | 
            +
                config = _sinja.resource_config[resource_name]
         | 
| 327 | 
            +
             | 
| 328 | 
            +
                namespace "/#{resource_name}" do
         | 
| 329 | 
            +
                  define_singleton_method(:_resource_config) { config }
         | 
| 330 | 
            +
                  define_singleton_method(:resource_config) { config[:resource] }
         | 
| 331 | 
            +
             | 
| 332 | 
            +
                  helpers do
         | 
| 333 | 
            +
                    define_method(:sanity_check!) do |*args|
         | 
| 334 | 
            +
                      super(resource_name, *args)
         | 
| 335 | 
            +
                    end
         | 
| 336 | 
            +
                  end
         | 
| 337 | 
            +
             | 
| 338 | 
            +
                  before %r{/(?<id>[^/]+)(?:/.+)?} do |id|
         | 
| 339 | 
            +
                    self.resource =
         | 
| 340 | 
            +
                      if env.key?('sinja.resource')
         | 
| 341 | 
            +
                        env['sinja.resource']
         | 
| 342 | 
            +
                      elsif respond_to?(:find)
         | 
| 343 | 
            +
                        find(id)
         | 
| 344 | 
            +
                      end
         | 
| 345 | 
            +
             | 
| 346 | 
            +
                    raise NotFoundError, "Resource '#{id}' not found" unless resource
         | 
| 347 | 
            +
                  end
         | 
| 348 | 
            +
             | 
| 349 | 
            +
                  register Resource
         | 
| 350 | 
            +
             | 
| 351 | 
            +
                  instance_eval(&block)
         | 
| 352 | 
            +
                end
         | 
| 353 | 
            +
              end
         | 
| 354 | 
            +
             | 
| 355 | 
            +
              alias_method :resources, :resource
         | 
| 356 | 
            +
             | 
| 357 | 
            +
              def sinja
         | 
| 358 | 
            +
                if block_given?
         | 
| 359 | 
            +
                  yield _sinja
         | 
| 360 | 
            +
                else
         | 
| 361 | 
            +
                  _sinja
         | 
| 362 | 
            +
                end
         | 
| 363 | 
            +
              end
         | 
| 364 | 
            +
             | 
| 365 | 
            +
              alias_method :configure_jsonapi, :sinja
         | 
| 366 | 
            +
              def freeze_jsonapi
         | 
| 367 | 
            +
                _sinja.freeze
         | 
| 368 | 
            +
              end
         | 
| 369 | 
            +
             | 
| 379 370 | 
             
              def self.extended(base)
         | 
| 380 371 | 
             
                def base.route(*, **opts)
         | 
| 381 372 | 
             
                  opts[:qparams] ||= []
         | 
    
        data/lib/sinja/errors.rb
    CHANGED
    
    | @@ -27,34 +27,50 @@ module Sinja | |
| 27 27 | 
             
              end
         | 
| 28 28 |  | 
| 29 29 | 
             
              class BadRequestError < HttpError
         | 
| 30 | 
            -
                 | 
| 30 | 
            +
                HTTP_STATUS = 400
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def initialize(*args) super(HTTP_STATUS, *args) end
         | 
| 31 33 | 
             
              end
         | 
| 32 34 |  | 
| 33 35 | 
             
              class ForbiddenError < HttpError
         | 
| 34 | 
            -
                 | 
| 36 | 
            +
                HTTP_STATUS = 403
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                def initialize(*args) super(HTTP_STATUS, *args) end
         | 
| 35 39 | 
             
              end
         | 
| 36 40 |  | 
| 37 41 | 
             
              class NotFoundError < HttpError
         | 
| 38 | 
            -
                 | 
| 42 | 
            +
                HTTP_STATUS = 404
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                def initialize(*args) super(HTTP_STATUS, *args) end
         | 
| 39 45 | 
             
              end
         | 
| 40 46 |  | 
| 41 47 | 
             
              class MethodNotAllowedError < HttpError
         | 
| 42 | 
            -
                 | 
| 48 | 
            +
                HTTP_STATUS = 405
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                def initialize(*args) super(HTTP_STATUS, *args) end
         | 
| 43 51 | 
             
              end
         | 
| 44 52 |  | 
| 45 53 | 
             
              class NotAcceptableError < HttpError
         | 
| 46 | 
            -
                 | 
| 54 | 
            +
                HTTP_STATUS = 406
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def initialize(*args) super(HTTP_STATUS, *args) end
         | 
| 47 57 | 
             
              end
         | 
| 48 58 |  | 
| 49 59 | 
             
              class ConflictError < HttpError
         | 
| 50 | 
            -
                 | 
| 60 | 
            +
                HTTP_STATUS = 409
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                def initialize(*args) super(HTTP_STATUS, *args) end
         | 
| 51 63 | 
             
              end
         | 
| 52 64 |  | 
| 53 65 | 
             
              class UnsupportedTypeError < HttpError
         | 
| 54 | 
            -
                 | 
| 66 | 
            +
                HTTP_STATUS = 415
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                def initialize(*args) super(HTTP_STATUS, *args) end
         | 
| 55 69 | 
             
              end
         | 
| 56 70 |  | 
| 57 71 | 
             
              class UnprocessibleEntityError < HttpError
         | 
| 72 | 
            +
                HTTP_STATUS = 422
         | 
| 73 | 
            +
             | 
| 58 74 | 
             
                attr_reader :tuples
         | 
| 59 75 |  | 
| 60 76 | 
             
                def initialize(tuples=[])
         | 
| @@ -63,7 +79,7 @@ module Sinja | |
| 63 79 | 
             
                  fail 'Tuples not properly formatted' \
         | 
| 64 80 | 
             
                    unless @tuples.any? && @tuples.all? { |t| Array === t && t.length == 2 }
         | 
| 65 81 |  | 
| 66 | 
            -
                  super( | 
| 82 | 
            +
                  super(HTTP_STATUS)
         | 
| 67 83 | 
             
                end
         | 
| 68 84 | 
             
              end
         | 
| 69 85 | 
             
            end
         | 
| @@ -26,7 +26,7 @@ module Sinja | |
| 26 26 | 
             
                    app.get '', :qparams=>%i[include fields filter sort page], :actions=>:fetch do
         | 
| 27 27 | 
             
                      fsp_opts = filter_sort_page?(:fetch)
         | 
| 28 28 | 
             
                      collection, opts = fetch
         | 
| 29 | 
            -
                      collection, pagination = filter_sort_page(collection, fsp_opts)
         | 
| 29 | 
            +
                      collection, pagination = filter_sort_page(collection, fsp_opts.to_h)
         | 
| 30 30 | 
             
                      serialize_models(collection, opts, pagination)
         | 
| 31 31 | 
             
                    end
         | 
| 32 32 |  | 
    
        data/lib/sinja/resource.rb
    CHANGED
    
    | @@ -13,6 +13,14 @@ require 'sinja/resource_routes' | |
| 13 13 |  | 
| 14 14 | 
             
            module Sinja
         | 
| 15 15 | 
             
              module Resource
         | 
| 16 | 
            +
                def self.registered(app)
         | 
| 17 | 
            +
                  app.helpers Helpers::Relationships do
         | 
| 18 | 
            +
                    attr_accessor :resource
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  app.register ResourceRoutes
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 16 24 | 
             
                def def_action_helper(context, action, allow_opts=[])
         | 
| 17 25 | 
             
                  abort "Action helper names can't overlap with Sinatra DSL" \
         | 
| 18 26 | 
             
                    if Sinatra::Base.respond_to?(action)
         | 
| @@ -72,14 +80,6 @@ module Sinja | |
| 72 80 | 
             
                  end
         | 
| 73 81 | 
             
                end
         | 
| 74 82 |  | 
| 75 | 
            -
                def self.registered(app)
         | 
| 76 | 
            -
                  app.helpers Helpers::Relationships do
         | 
| 77 | 
            -
                    attr_accessor :resource
         | 
| 78 | 
            -
                  end
         | 
| 79 | 
            -
             | 
| 80 | 
            -
                  app.register ResourceRoutes
         | 
| 81 | 
            -
                end
         | 
| 82 | 
            -
             | 
| 83 83 | 
             
                %i[has_one has_many].each do |rel_type|
         | 
| 84 84 | 
             
                  define_method(rel_type) do |rel, &block|
         | 
| 85 85 | 
             
                    rel = rel.to_s
         | 
| @@ -9,11 +9,11 @@ module Sinja | |
| 9 9 | 
             
                  app.def_action_helper(app, :update, :roles)
         | 
| 10 10 | 
             
                  app.def_action_helper(app, :destroy, :roles)
         | 
| 11 11 |  | 
| 12 | 
            -
                  app.head '', : | 
| 12 | 
            +
                  app.head '', :qcaptures=>{ :filter=>:id } do
         | 
| 13 13 | 
             
                    allow :get=>:show
         | 
| 14 14 | 
             
                  end
         | 
| 15 15 |  | 
| 16 | 
            -
                  app.get '', : | 
| 16 | 
            +
                  app.get '', :qcaptures=>{ :filter=>:id }, :qparams=>%i[include fields], :actions=>:show do
         | 
| 17 17 | 
             
                    ids = @qcaptures.first # TODO: Get this as a block parameter?
         | 
| 18 18 | 
             
                    ids = ids.split(',') if String === ids
         | 
| 19 19 | 
             
                    ids = [*ids].tap(&:uniq!)
         | 
| @@ -45,7 +45,7 @@ module Sinja | |
| 45 45 | 
             
                  app.get '', :qparams=>%i[include fields filter sort page], :actions=>:index do
         | 
| 46 46 | 
             
                    fsp_opts = filter_sort_page?(:index)
         | 
| 47 47 | 
             
                    collection, opts = index
         | 
| 48 | 
            -
                    collection, pagination = filter_sort_page(collection, fsp_opts)
         | 
| 48 | 
            +
                    collection, pagination = filter_sort_page(collection, fsp_opts.to_h)
         | 
| 49 49 | 
             
                    serialize_models(collection, opts, pagination)
         | 
| 50 50 | 
             
                  end
         | 
| 51 51 |  | 
    
        data/lib/sinja/version.rb
    CHANGED
    
    
    
        data/sinja.gemspec
    CHANGED
    
    | @@ -10,6 +10,22 @@ Gem::Specification.new do |spec| | |
| 10 10 | 
             
              spec.email         = ['mike@oobak.org']
         | 
| 11 11 |  | 
| 12 12 | 
             
              spec.summary       = 'RESTful, {json:api}-compliant web services in Sinatra'
         | 
| 13 | 
            +
              spec.description   = <<~EOF
         | 
| 14 | 
            +
                Sinja is a Sinatra extension for quickly building RESTful,
         | 
| 15 | 
            +
                {json:api}-compliant web services, leveraging the excellent
         | 
| 16 | 
            +
                JSONAPI::Serializers gem for payload serialization. It enhances Sinatra's
         | 
| 17 | 
            +
                DSL to enable resource-, relationship-, and role-centric API development,
         | 
| 18 | 
            +
                and it configures Sinatra with the proper settings, MIME-types, filters,
         | 
| 19 | 
            +
                conditions, and error-handling.
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                There are many parsing (deserializing) and rendering (serializing) and
         | 
| 22 | 
            +
                so-called "JSON API" libraries available for Ruby, but relatively few that
         | 
| 23 | 
            +
                attempt to correctly implement the entire {json:api} specification,
         | 
| 24 | 
            +
                including routing, request header and query parameter checking, and
         | 
| 25 | 
            +
                relationship side-loading. Sinja lets you focus on the business logic of
         | 
| 26 | 
            +
                your applications without worrying about the specification, and without
         | 
| 27 | 
            +
                pulling in a heavy framework like Rails. It's lightweight and ORM-agnostic!
         | 
| 28 | 
            +
              EOF
         | 
| 13 29 | 
             
              spec.homepage      = 'https://github.com/mwpastore/sinja'
         | 
| 14 30 | 
             
              spec.license       = 'MIT'
         | 
| 15 31 |  | 
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: sinja
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 1.2. | 
| 4 | 
            +
              version: 1.2.2
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Mike Pastore
         | 
| @@ -208,7 +208,21 @@ dependencies: | |
| 208 208 | 
             
                - - "~>"
         | 
| 209 209 | 
             
                  - !ruby/object:Gem::Version
         | 
| 210 210 | 
             
                    version: '1.3'
         | 
| 211 | 
            -
            description: 
         | 
| 211 | 
            +
            description: |
         | 
| 212 | 
            +
              Sinja is a Sinatra extension for quickly building RESTful,
         | 
| 213 | 
            +
              {json:api}-compliant web services, leveraging the excellent
         | 
| 214 | 
            +
              JSONAPI::Serializers gem for payload serialization. It enhances Sinatra's
         | 
| 215 | 
            +
              DSL to enable resource-, relationship-, and role-centric API development,
         | 
| 216 | 
            +
              and it configures Sinatra with the proper settings, MIME-types, filters,
         | 
| 217 | 
            +
              conditions, and error-handling.
         | 
| 218 | 
            +
             | 
| 219 | 
            +
              There are many parsing (deserializing) and rendering (serializing) and
         | 
| 220 | 
            +
              so-called "JSON API" libraries available for Ruby, but relatively few that
         | 
| 221 | 
            +
              attempt to correctly implement the entire {json:api} specification,
         | 
| 222 | 
            +
              including routing, request header and query parameter checking, and
         | 
| 223 | 
            +
              relationship side-loading. Sinja lets you focus on the business logic of
         | 
| 224 | 
            +
              your applications without worrying about the specification, and without
         | 
| 225 | 
            +
              pulling in a heavy framework like Rails. It's lightweight and ORM-agnostic!
         | 
| 212 226 | 
             
            email:
         | 
| 213 227 | 
             
            - mike@oobak.org
         | 
| 214 228 | 
             
            executables: []
         |