grape 4.0.1 → 4.1.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/CHANGELOG.md +102 -1
- data/README.md +195 -95
- data/UPGRADING.md +364 -0
- data/grape.gemspec +1 -1
- data/lib/grape/api/instance.rb +57 -30
- data/lib/grape/api.rb +50 -20
- data/lib/grape/content_types.rb +26 -9
- data/lib/grape/cookies.rb +1 -1
- data/lib/grape/declared_params_handler.rb +35 -7
- data/lib/grape/dsl/declared.rb +5 -4
- data/lib/grape/dsl/desc.rb +1 -2
- data/lib/grape/dsl/entity.rb +35 -19
- data/lib/grape/dsl/headers.rb +3 -3
- data/lib/grape/dsl/helpers.rb +5 -8
- data/lib/grape/dsl/inside_route.rb +33 -27
- data/lib/grape/dsl/logger.rb +10 -4
- data/lib/grape/dsl/middleware.rb +4 -9
- data/lib/grape/dsl/mounting.rb +149 -0
- data/lib/grape/dsl/parameters.rb +78 -104
- data/lib/grape/dsl/request_response.rb +35 -22
- data/lib/grape/dsl/routing.rb +17 -191
- data/lib/grape/dsl/settings.rb +1 -1
- data/lib/grape/dsl/version_options.rb +2 -2
- data/lib/grape/dsl/versioning.rb +98 -0
- data/lib/grape/endpoint/state.rb +15 -0
- data/lib/grape/endpoint.rb +229 -137
- data/lib/grape/error_formatter/base.rb +28 -13
- data/lib/grape/error_formatter/json.rb +3 -0
- data/lib/grape/error_formatter/jsonl.rb +15 -0
- data/lib/grape/error_formatter/xml.rb +3 -1
- data/lib/grape/exceptions/error_response.rb +23 -7
- data/lib/grape/exceptions/halt.rb +39 -0
- data/lib/grape/exceptions/validation.rb +9 -9
- data/lib/grape/exceptions/validation_errors.rb +12 -2
- data/lib/grape/formatter/jsonl.rb +34 -0
- data/lib/grape/formatter/markdown.rb +17 -0
- data/lib/grape/formatter/md.rb +11 -0
- data/lib/grape/locale/en.yml +1 -0
- data/lib/grape/middleware/auth/base.rb +16 -1
- data/lib/grape/middleware/auth/strategy_info.rb +2 -2
- data/lib/grape/middleware/base.rb +19 -13
- data/lib/grape/middleware/error.rb +133 -35
- data/lib/grape/middleware/formatter.rb +235 -94
- data/lib/grape/middleware/precomputed_content_types.rb +14 -2
- data/lib/grape/middleware/stack.rb +7 -8
- data/lib/grape/middleware/versioner/base.rb +26 -10
- data/lib/grape/middleware/versioner/header.rb +70 -15
- data/lib/grape/middleware/versioner/param.rb +3 -5
- data/lib/grape/middleware/versioner/path.rb +20 -11
- data/lib/grape/middleware/versioner.rb +1 -1
- data/lib/grape/mountable.rb +2 -4
- data/lib/grape/namespace.rb +2 -3
- data/lib/grape/parser/jsonl.rb +19 -0
- data/lib/grape/request.rb +46 -8
- data/lib/grape/router/base_route.rb +41 -0
- data/lib/grape/router/greedy_route.rb +1 -1
- data/lib/grape/router/mustermann_pattern.rb +1 -1
- data/lib/grape/router/pattern/path.rb +1 -1
- data/lib/grape/router/pattern.rb +11 -5
- data/lib/grape/router/route.rb +42 -11
- data/lib/grape/router/route_buckets.rb +246 -0
- data/lib/grape/router.rb +262 -32
- data/lib/grape/util/endpoint_configuration.rb +7 -1
- data/lib/grape/util/inheritable_setting.rb +80 -161
- data/lib/grape/util/lazy/value.rb +52 -11
- data/lib/grape/util/media_type.rb +38 -15
- data/lib/grape/util/path_normalizer.rb +7 -2
- data/lib/grape/util/registry.rb +9 -2
- data/lib/grape/util/scrub.rb +33 -0
- data/lib/grape/util/translation.rb +6 -6
- data/lib/grape/validations/attributes_iterator.rb +73 -37
- data/lib/grape/validations/coerce_options.rb +1 -1
- data/lib/grape/validations/params_documentation.rb +15 -7
- data/lib/grape/validations/params_scope.rb +176 -43
- data/lib/grape/validations/types/custom_type_coercer.rb +2 -3
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +20 -12
- data/lib/grape/validations/types/multiple_type_coercer.rb +1 -2
- data/lib/grape/validations/types/primitive_coercer.rb +17 -0
- data/lib/grape/validations/types/variant_collection_coercer.rb +24 -10
- data/lib/grape/validations/types.rb +12 -18
- data/lib/grape/validations/validations_spec.rb +53 -22
- data/lib/grape/validations/validators/allow_blank_validator.rb +2 -2
- data/lib/grape/validations/validators/as_validator.rb +2 -3
- data/lib/grape/validations/validators/base.rb +124 -25
- data/lib/grape/validations/validators/coerce_validator.rb +2 -3
- data/lib/grape/validations/validators/default_validator.rb +18 -2
- data/lib/grape/validations/validators/except_values_validator.rb +16 -3
- data/lib/grape/validations/validators/hash_elements_validator.rb +34 -0
- data/lib/grape/validations/validators/length_validator.rb +24 -16
- data/lib/grape/validations/validators/multiple_params_base.rb +0 -4
- data/lib/grape/validations/validators/values_validator.rb +18 -24
- data/lib/grape/version.rb +1 -1
- data/lib/grape.rb +3 -0
- metadata +18 -11
- data/lib/grape/util/lazy/value_array.rb +0 -21
- data/lib/grape/util/lazy/value_enumerable.rb +0 -31
- data/lib/grape/util/lazy/value_hash.rb +0 -21
- data/lib/grape/validations/multiple_attributes_iterator.rb +0 -13
- data/lib/grape/validations/single_attribute_iterator.rb +0 -28
data/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Grape is a REST-like API framework for Ruby. It's designed to run on Rack or com
|
|
|
10
10
|
|
|
11
11
|
## Stable Release
|
|
12
12
|
|
|
13
|
-
You're reading the documentation for the stable release of Grape, 4.0.
|
|
13
|
+
You're reading the documentation for the stable release of Grape, 4.1.0.
|
|
14
14
|
|
|
15
15
|
## Project Resources
|
|
16
16
|
|
|
@@ -27,7 +27,7 @@ The maintainers of Grape are working with Tidelift to deliver commercial support
|
|
|
27
27
|
|
|
28
28
|
## Installation
|
|
29
29
|
|
|
30
|
-
Ruby 3.3 or newer is required.
|
|
30
|
+
Ruby 3.3.1 or newer is required.
|
|
31
31
|
|
|
32
32
|
Grape is available as a gem, to install it run:
|
|
33
33
|
|
|
@@ -124,7 +124,7 @@ Grape's [deprecator](https://api.rubyonrails.org/v7.1.0/classes/ActiveSupport/De
|
|
|
124
124
|
### All
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
By default Grape will compile the routes on the first
|
|
127
|
+
By default Grape will compile the routes on the first request, but it is possible to pre-load routes using the `compile!` method.
|
|
128
128
|
|
|
129
129
|
```ruby
|
|
130
130
|
Twitter::API.compile!
|
|
@@ -351,7 +351,7 @@ end
|
|
|
351
351
|
```ruby
|
|
352
352
|
class BasicAPI < Grape::API
|
|
353
353
|
desc 'Statuses index' do
|
|
354
|
-
params
|
|
354
|
+
params((configuration[:entity] || API::Entities::Status).documentation)
|
|
355
355
|
end
|
|
356
356
|
params do
|
|
357
357
|
requires :all, using: (configuration[:entity] || API::Entities::Status).documentation
|
|
@@ -466,7 +466,7 @@ Using this versioning strategy, clients should pass the desired version in the H
|
|
|
466
466
|
|
|
467
467
|
curl -H Accept:application/vnd.twitter-v1+json http://localhost:9292/statuses/public_timeline
|
|
468
468
|
|
|
469
|
-
By default, the first matching version is used when no `Accept` header is supplied. This behavior is similar to routing in Rails. To circumvent this default behavior, one could use the `:strict` option. When this option is set to `true`, a `406 Not Acceptable` error is returned when no correct `Accept` header is supplied.
|
|
469
|
+
By default, the first matching version is used when no `Accept` header is supplied. This behavior is similar to routing in Rails. To circumvent this default behavior, one could use the `:strict` option. When this option is set to `true`, a `406 Not Acceptable` error is returned when no correct `Accept` header is supplied and the `:cascade` option is set to `false`. Otherwise a `404 Not Found` error is returned by Rack if no other route matches.
|
|
470
470
|
|
|
471
471
|
When an invalid `Accept` header is supplied, a `406 Not Acceptable` error is returned if the `:cascade` option is set to `false`. Otherwise a `404 Not Found` error is returned by Rack if no other route matches.
|
|
472
472
|
|
|
@@ -492,7 +492,7 @@ By default, the first matching version is used when no `Accept-Version` header i
|
|
|
492
492
|
version 'v1', using: :param
|
|
493
493
|
```
|
|
494
494
|
|
|
495
|
-
Using this versioning strategy, clients should pass the desired version as a
|
|
495
|
+
Using this versioning strategy, clients should pass the desired version as a parameter in the URL query string. A version given in the request body is not read.
|
|
496
496
|
|
|
497
497
|
curl http://localhost:9292/statuses/public_timeline?apiver=v1
|
|
498
498
|
|
|
@@ -507,7 +507,7 @@ version 'v1', using: :param, parameter: 'v'
|
|
|
507
507
|
|
|
508
508
|
## Linting
|
|
509
509
|
|
|
510
|
-
You can check whether your API is in conformance with the [Rack's specification](https://github.com/rack/rack/blob/main/SPEC.rdoc) by calling `lint!` at the
|
|
510
|
+
You can check whether your API is in conformance with the [Rack's specification](https://github.com/rack/rack/blob/main/SPEC.rdoc) by calling `lint!` at the top level of the API you serve, or through [configuration](#configuration). Every response the API gives is then checked once with `Rack::Lint`, including the 404 for a path no route matches and the responses of Rack apps mounted with `mount`. A `lint!` inside a namespace, or in an API mounted into another one, has no effect.
|
|
511
511
|
|
|
512
512
|
```ruby
|
|
513
513
|
class Api < Grape::API
|
|
@@ -539,7 +539,7 @@ desc 'Returns your public timeline.' do
|
|
|
539
539
|
params API::Entities::Status.documentation
|
|
540
540
|
success API::Entities::Entity
|
|
541
541
|
failure [[401, 'Unauthorized', 'Entities::Error']]
|
|
542
|
-
|
|
542
|
+
default_response code: 500, message: 'InvalidRequest', model: Entities::Error
|
|
543
543
|
named 'My named route'
|
|
544
544
|
headers XAuthToken: {
|
|
545
545
|
description: 'Validates your identity',
|
|
@@ -578,8 +578,11 @@ end
|
|
|
578
578
|
Use `Grape.configure` to set up global settings at load time.
|
|
579
579
|
Currently the configurable settings are:
|
|
580
580
|
|
|
581
|
-
* `param_builder`: Sets the [Parameter Builder](#parameters), defaults to
|
|
581
|
+
* `param_builder`: Sets the [Parameter Builder](#parameters), defaults to `:hash_with_indifferent_access`.
|
|
582
|
+
* `lint`: Checks every response against Rack's specification, defaults to `false`. See [Linting](#linting).
|
|
583
|
+
* `warn_on_helper_overrides`: Warns when a helper method masks a `Grape::Endpoint` instance method, defaults to `false`.
|
|
582
584
|
* `raise_rendering_errors`: Lets an error response that cannot be rendered propagate out of the middleware stack instead of being answered with a failsafe `500`, defaults to `false`. See [When the error response itself cannot be rendered](#when-the-error-response-itself-cannot-be-rendered).
|
|
585
|
+
* `raise_rescue_handler_errors`: Lets an exception raised inside a `rescue_from` block propagate out of the middleware stack instead of being redispatched, defaults to `false`. See [Re-raising from inside a `rescue_from` block](#re-raising-from-inside-a-rescue_from-block).
|
|
583
586
|
|
|
584
587
|
To change a setting value make sure that at some point during load time the following code runs
|
|
585
588
|
|
|
@@ -620,7 +623,7 @@ get :public_timeline do
|
|
|
620
623
|
end
|
|
621
624
|
```
|
|
622
625
|
|
|
623
|
-
Parameters are automatically populated from the request body on `POST`, `PUT` and `QUERY` for form input, JSON and XML content-types.
|
|
626
|
+
Parameters are automatically populated from the request body on `POST`, `PUT`, `PATCH`, `DELETE` and `QUERY` for form input, JSON and XML content-types.
|
|
624
627
|
|
|
625
628
|
The request:
|
|
626
629
|
|
|
@@ -656,7 +659,7 @@ In the case of conflict between either of:
|
|
|
656
659
|
|
|
657
660
|
* route string parameters
|
|
658
661
|
* `GET`, `POST` and `PUT` parameters
|
|
659
|
-
* the contents of the request body on `POST` and `
|
|
662
|
+
* the contents of the request body on `POST`, `PUT`, `PATCH`, `DELETE` and `QUERY`
|
|
660
663
|
|
|
661
664
|
Route string parameters will have precedence.
|
|
662
665
|
|
|
@@ -674,6 +677,7 @@ class API < Grape::API
|
|
|
674
677
|
get do
|
|
675
678
|
params.color # instead of params[:color]
|
|
676
679
|
end
|
|
680
|
+
end
|
|
677
681
|
```
|
|
678
682
|
|
|
679
683
|
The class can also be overridden on individual parameter blocks using `build_with` as follows.
|
|
@@ -845,7 +849,7 @@ curl -X GET -H "Content-Type: application/json" localhost:9292/parent/foo/bar
|
|
|
845
849
|
"with_parent_namespaces": {
|
|
846
850
|
"parent_name": "foo",
|
|
847
851
|
"child_name": "bar"
|
|
848
|
-
}
|
|
852
|
+
}
|
|
849
853
|
}
|
|
850
854
|
````
|
|
851
855
|
|
|
@@ -950,7 +954,7 @@ curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d
|
|
|
950
954
|
"first_name": "first name",
|
|
951
955
|
"last_name": null,
|
|
952
956
|
"address": {
|
|
953
|
-
"city": "
|
|
957
|
+
"city": "SF",
|
|
954
958
|
"region": null
|
|
955
959
|
}
|
|
956
960
|
}
|
|
@@ -1081,11 +1085,13 @@ curl -X POST -H "Content-Type: application/json" localhost:9292/child -d '{"chil
|
|
|
1081
1085
|
Using `route_param` takes higher precedence over a regular parameter defined with same name:
|
|
1082
1086
|
|
|
1083
1087
|
```ruby
|
|
1088
|
+
format :json
|
|
1089
|
+
|
|
1084
1090
|
params do
|
|
1085
1091
|
requires :foo, type: String
|
|
1086
1092
|
end
|
|
1087
1093
|
route_param :foo do
|
|
1088
|
-
|
|
1094
|
+
post do
|
|
1089
1095
|
{ value: params[:foo] }
|
|
1090
1096
|
end
|
|
1091
1097
|
end
|
|
@@ -1117,7 +1123,7 @@ params do
|
|
|
1117
1123
|
requires :url
|
|
1118
1124
|
end
|
|
1119
1125
|
optional :audio, type: Hash do
|
|
1120
|
-
requires :format, type: Symbol, values: [:mp3, :wav, :aac, :ogg]
|
|
1126
|
+
requires :format, type: Symbol, values: [:mp3, :wav, :aac, :ogg]
|
|
1121
1127
|
end
|
|
1122
1128
|
mutually_exclusive :media, :audio
|
|
1123
1129
|
end
|
|
@@ -1141,7 +1147,7 @@ end
|
|
|
1141
1147
|
Default values are eagerly evaluated. Above `:non_random_number` will evaluate to the same number for each call to the endpoint of this `params` block. To have the default evaluate lazily with each request use a lambda, like `:random_number` above.
|
|
1142
1148
|
|
|
1143
1149
|
Note that default values will be passed through to any validation options specified.
|
|
1144
|
-
The following example
|
|
1150
|
+
The following example raises `Grape::Exceptions::IncompatibleOptionValues` when the API is defined, since the default is not one of the allowed values.
|
|
1145
1151
|
|
|
1146
1152
|
```ruby
|
|
1147
1153
|
params do
|
|
@@ -1185,7 +1191,7 @@ The following are all valid types, supported out of the box by Grape:
|
|
|
1185
1191
|
|
|
1186
1192
|
### Custom Types and Coercions
|
|
1187
1193
|
|
|
1188
|
-
Aside from the default set of supported types listed above, any class can be used as a type as long as an explicit coercion method is supplied. If the type implements a class-level `parse` method, Grape will use it automatically. This method must take one string argument and return an instance of the correct type, or return an instance of `Grape::Types::InvalidValue` which optionally accepts a message to be returned in the response.
|
|
1194
|
+
Aside from the default set of supported types listed above, any class can be used as a type as long as an explicit coercion method is supplied. If the type implements a class-level `parse` method, Grape will use it automatically. This method must take one string argument and return an instance of the correct type, or return an instance of `Grape::Validations::Types::InvalidValue` which optionally accepts a message to be returned in the response.
|
|
1189
1195
|
|
|
1190
1196
|
```ruby
|
|
1191
1197
|
class Color
|
|
@@ -1197,12 +1203,12 @@ class Color
|
|
|
1197
1203
|
def self.parse(value)
|
|
1198
1204
|
return new(value) if %w[blue red green].include?(value)
|
|
1199
1205
|
|
|
1200
|
-
Grape::Types::InvalidValue.new('Unsupported color')
|
|
1206
|
+
Grape::Validations::Types::InvalidValue.new('Unsupported color')
|
|
1201
1207
|
end
|
|
1202
1208
|
end
|
|
1203
1209
|
|
|
1204
1210
|
params do
|
|
1205
|
-
|
|
1211
|
+
optional :color, type: Color, default: Color.new('blue')
|
|
1206
1212
|
requires :more_colors, type: Array[Color] # Collections work
|
|
1207
1213
|
optional :unique_colors, type: Set[Color] # Duplicates discarded
|
|
1208
1214
|
end
|
|
@@ -1284,8 +1290,8 @@ get '/' do
|
|
|
1284
1290
|
params[:json].inspect
|
|
1285
1291
|
end
|
|
1286
1292
|
|
|
1287
|
-
client.get('/', json: '{"int":1}') # => "
|
|
1288
|
-
client.get('/', json: '[{"int":"1"}]') # =>
|
|
1293
|
+
client.get('/', json: '{"int":1}') # => '{"int" => 1}'
|
|
1294
|
+
client.get('/', json: '[{"int":"1"}]') # => '[{"int" => 1}]'
|
|
1289
1295
|
|
|
1290
1296
|
client.get('/', json: '{"int":4}') # => HTTP 400
|
|
1291
1297
|
client.get('/', json: '[{"int":4}]') # => HTTP 400
|
|
@@ -1389,7 +1395,7 @@ end
|
|
|
1389
1395
|
|
|
1390
1396
|
Parameters can be nested using `group` or by calling `requires` or `optional` with a block.
|
|
1391
1397
|
In the [above example](#parameter-validation-and-coercion), this means `params[:media][:url]` is required along with `params[:id]`, and `params[:audio][:format]` is required only if `params[:audio]` is present.
|
|
1392
|
-
With a block, `group`, `requires` and `optional`
|
|
1398
|
+
With a block, `group`, `requires` and `optional` require an additional option `type`, which can be either `Array` or `Hash`; a block without one raises `Grape::Exceptions::MissingGroupType`. Depending on the value, the nested parameters will be treated either as values of a hash or as values of hashes in an array.
|
|
1393
1399
|
|
|
1394
1400
|
```ruby
|
|
1395
1401
|
params do
|
|
@@ -1436,13 +1442,13 @@ You can rename parameters:
|
|
|
1436
1442
|
```ruby
|
|
1437
1443
|
params do
|
|
1438
1444
|
optional :category, as: :type
|
|
1439
|
-
given
|
|
1445
|
+
given category: ->(val) { val == 'foo' } do
|
|
1440
1446
|
requires :description
|
|
1441
1447
|
end
|
|
1442
1448
|
end
|
|
1443
1449
|
```
|
|
1444
1450
|
|
|
1445
|
-
Note:
|
|
1451
|
+
Note: `given` takes the name a param is declared with, not the one `as` renames it to. In the example, it is `category`, not `type`: the renaming applies only to [`declared(params)`](#declared), and naming `type` raises `Grape::Exceptions::UnknownParameter`.
|
|
1446
1452
|
|
|
1447
1453
|
### Group Options
|
|
1448
1454
|
|
|
@@ -1453,9 +1459,9 @@ The example below presents a typical case when parameters share common options.
|
|
|
1453
1459
|
|
|
1454
1460
|
```ruby
|
|
1455
1461
|
params do
|
|
1456
|
-
requires :first_name, type: String, regexp:
|
|
1457
|
-
optional :middle_name, type: String, regexp:
|
|
1458
|
-
requires :last_name, type: String, regexp:
|
|
1462
|
+
requires :first_name, type: String, regexp: /\w+/, desc: 'First name', documentation: { in: 'body' }
|
|
1463
|
+
optional :middle_name, type: String, regexp: /\w+/, desc: 'Middle name', documentation: { in: 'body', x: { nullable: true } }
|
|
1464
|
+
requires :last_name, type: String, regexp: /\w+/, desc: 'Last name', documentation: { in: 'body' }
|
|
1459
1465
|
end
|
|
1460
1466
|
```
|
|
1461
1467
|
|
|
@@ -1463,7 +1469,7 @@ Grape allows you to present the same logic through the `with` method in your par
|
|
|
1463
1469
|
|
|
1464
1470
|
```ruby
|
|
1465
1471
|
params do
|
|
1466
|
-
with(type: String, regexp:
|
|
1472
|
+
with(type: String, regexp: /\w+/, documentation: { in: 'body' }) do
|
|
1467
1473
|
requires :first_name, desc: 'First name'
|
|
1468
1474
|
optional :middle_name, desc: 'Middle name', documentation: { x: { nullable: true } }
|
|
1469
1475
|
requires :last_name, desc: 'Last name'
|
|
@@ -1527,7 +1533,7 @@ Parameters can be restricted to a specific set of values with the `:values` opti
|
|
|
1527
1533
|
```ruby
|
|
1528
1534
|
params do
|
|
1529
1535
|
requires :status, type: Symbol, values: [:not_started, :processing, :done]
|
|
1530
|
-
optional :numbers, type: Array[Integer], default: 1, values: [1, 2, 3, 5, 8]
|
|
1536
|
+
optional :numbers, type: Array[Integer], default: [1], values: [1, 2, 3, 5, 8]
|
|
1531
1537
|
end
|
|
1532
1538
|
```
|
|
1533
1539
|
|
|
@@ -1545,7 +1551,7 @@ Note endless ranges are also supported with ActiveSupport >= 6.0, but they requi
|
|
|
1545
1551
|
|
|
1546
1552
|
```ruby
|
|
1547
1553
|
params do
|
|
1548
|
-
requires :minimum, type: Integer, values: 10..
|
|
1554
|
+
requires :minimum, type: Integer, values: (10..)
|
|
1549
1555
|
optional :maximum, type: Integer, values: ..10
|
|
1550
1556
|
end
|
|
1551
1557
|
```
|
|
@@ -1573,10 +1579,10 @@ Combined with `:type`, ranges (open or closed) cover common numeric bound checks
|
|
|
1573
1579
|
|
|
1574
1580
|
```ruby
|
|
1575
1581
|
params do
|
|
1576
|
-
requires :quantity, type: Integer, values: 1..
|
|
1582
|
+
requires :quantity, type: Integer, values: (1..) # must be a positive integer
|
|
1577
1583
|
requires :discount, type: Float, values: 0.0..100.0 # must be between 0 and 100
|
|
1578
1584
|
requires :rating, type: Integer, values: 5..5 # must be exactly 5
|
|
1579
|
-
requires :numbers, type: [Integer], values: 1..
|
|
1585
|
+
requires :numbers, type: [Integer], values: (1..) # every element must be positive
|
|
1580
1586
|
end
|
|
1581
1587
|
```
|
|
1582
1588
|
|
|
@@ -1620,9 +1626,9 @@ The `except_values` validator behaves similarly to the `values` validator in tha
|
|
|
1620
1626
|
```ruby
|
|
1621
1627
|
params do
|
|
1622
1628
|
requires :browser, except_values: [ 'ie6', 'ie7', 'ie8' ]
|
|
1623
|
-
requires :port, except_values: { value: 0..1024, message: 'is not allowed' }
|
|
1629
|
+
requires :port, type: Integer, except_values: { value: 0..1024, message: 'is not allowed' }
|
|
1624
1630
|
requires :negative, type: Integer, except_values: ..-1
|
|
1625
|
-
requires :hashtag, except_values: -> { Hashtag
|
|
1631
|
+
requires :hashtag, except_values: -> { Hashtag::FORBIDDEN_LIST }
|
|
1626
1632
|
end
|
|
1627
1633
|
```
|
|
1628
1634
|
|
|
@@ -1842,7 +1848,7 @@ end
|
|
|
1842
1848
|
You can also create custom classes that take parameters.
|
|
1843
1849
|
|
|
1844
1850
|
```ruby
|
|
1845
|
-
class
|
|
1851
|
+
class MaxLength < Grape::Validations::Validators::Base
|
|
1846
1852
|
def validate_param!(attr_name, params)
|
|
1847
1853
|
return if params[attr_name].length <= @options
|
|
1848
1854
|
|
|
@@ -1853,10 +1859,12 @@ end
|
|
|
1853
1859
|
|
|
1854
1860
|
```ruby
|
|
1855
1861
|
params do
|
|
1856
|
-
requires :text,
|
|
1862
|
+
requires :text, max_length: 140
|
|
1857
1863
|
end
|
|
1858
1864
|
```
|
|
1859
1865
|
|
|
1866
|
+
A validator is registered under its class name, underscored and without a `Validator` suffix, for every API in the process. Pick a name that no built-in validator uses: a class named `Length` would replace the built-in [`length`](#length) validator everywhere.
|
|
1867
|
+
|
|
1860
1868
|
You can also create custom validation that use request to validate the attribute. For example if you want to have parameters that are available to only admins, you can do the following.
|
|
1861
1869
|
|
|
1862
1870
|
```ruby
|
|
@@ -2113,7 +2121,7 @@ params do
|
|
|
2113
2121
|
requires :name, message: :name_required
|
|
2114
2122
|
end
|
|
2115
2123
|
```
|
|
2116
|
-
```
|
|
2124
|
+
```yaml
|
|
2117
2125
|
# en.yml
|
|
2118
2126
|
|
|
2119
2127
|
en:
|
|
@@ -2128,7 +2136,7 @@ en:
|
|
|
2128
2136
|
|
|
2129
2137
|
You can also override attribute names.
|
|
2130
2138
|
|
|
2131
|
-
```
|
|
2139
|
+
```yaml
|
|
2132
2140
|
# en.yml
|
|
2133
2141
|
|
|
2134
2142
|
en:
|
|
@@ -2148,7 +2156,7 @@ You cannot set a custom message option for Default as it requires interpolation
|
|
|
2148
2156
|
|
|
2149
2157
|
```ruby
|
|
2150
2158
|
params do
|
|
2151
|
-
|
|
2159
|
+
optional :name, values: { value: -> { (1..10).to_a }, message: 'not in range from 1 to 10' }, default: 5
|
|
2152
2160
|
end
|
|
2153
2161
|
```
|
|
2154
2162
|
|
|
@@ -2213,8 +2221,7 @@ curl -H "secret_PassWord: swordfish" ...
|
|
|
2213
2221
|
|
|
2214
2222
|
The header name will have been normalized for you.
|
|
2215
2223
|
|
|
2216
|
-
- In the `
|
|
2217
|
-
- In the `header` helper names will be coerced into a capitalized kebab case as `Secret-PassWord` if using Rack < 3.
|
|
2224
|
+
- In the `headers` helper names will be coerced into a downcased kebab case as `secret-password`, and looked up case-insensitively, so `headers['Secret-Password']` finds it too. On Rack < 3, a standard header such as `User-Agent` keeps its usual capitalization.
|
|
2218
2225
|
- In the `env` collection they appear in all uppercase, in snake case, and prefixed with 'HTTP_' as `HTTP_SECRET_PASSWORD`
|
|
2219
2226
|
|
|
2220
2227
|
The header name will have been normalized per HTTP standards defined in [RFC2616 Section 4.2](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2) regardless of what is being sent by a client.
|
|
@@ -2293,7 +2300,7 @@ get ':id', requirements: { id: /[0-9]*/ } do
|
|
|
2293
2300
|
end
|
|
2294
2301
|
|
|
2295
2302
|
namespace :outer, requirements: { id: /[0-9]*/ } do
|
|
2296
|
-
get :id do
|
|
2303
|
+
get ':id' do
|
|
2297
2304
|
end
|
|
2298
2305
|
|
|
2299
2306
|
get ':id/edit' do
|
|
@@ -2351,7 +2358,7 @@ route_param :name, requirements: /.+/ do
|
|
|
2351
2358
|
end
|
|
2352
2359
|
```
|
|
2353
2360
|
|
|
2354
|
-
Such a regexp claims the extension as well, so `GET /a.b.json` hands the endpoint `"a.b.json"` and format
|
|
2361
|
+
Such a regexp claims the extension as well, so `GET /a.b.json` hands the endpoint `"a.b.json"`. The response format is still read off the end of the path, though, whatever the route captured: with no `format` of its own, the API above answers `GET /a.b.json` in JSON, and `GET /a.b.xml` with a `500`, since the XML formatter cannot render a String. Declaring `format` settles the response format whatever the extension. A narrower constraint leaves the suffix alone: `get ':n', requirements: { n: Float }` matches `/4.2` and still reads `.json` as the format.
|
|
2355
2362
|
|
|
2356
2363
|
### The QUERY Method
|
|
2357
2364
|
|
|
@@ -2528,13 +2535,15 @@ You can set, get and delete your cookies very simply using `cookies` method.
|
|
|
2528
2535
|
```ruby
|
|
2529
2536
|
class API < Grape::API
|
|
2530
2537
|
get 'status_count' do
|
|
2531
|
-
|
|
2532
|
-
cookies[:status_count]
|
|
2538
|
+
# a cookie the client sent back is a String, so convert before counting
|
|
2539
|
+
cookies[:status_count] = cookies[:status_count].to_i + 1
|
|
2533
2540
|
{ status_count: cookies[:status_count] }
|
|
2534
2541
|
end
|
|
2535
2542
|
|
|
2536
2543
|
delete 'status_count' do
|
|
2537
|
-
|
|
2544
|
+
status_count = cookies[:status_count]
|
|
2545
|
+
cookies.delete(:status_count)
|
|
2546
|
+
{ status_count: status_count }
|
|
2538
2547
|
end
|
|
2539
2548
|
end
|
|
2540
2549
|
```
|
|
@@ -2655,7 +2664,7 @@ class API < Grape::API
|
|
|
2655
2664
|
end
|
|
2656
2665
|
```
|
|
2657
2666
|
|
|
2658
|
-
When you add a route for a resource, a route for the `OPTIONS` method will also be added. The response to an OPTIONS request will include an "Allow" header listing the supported methods. If the resource has `before` and `
|
|
2667
|
+
When you add a route for a resource, a route for the `OPTIONS` method will also be added. The response to an OPTIONS request will include an "Allow" header listing the supported methods. If the resource has `before`, `after` and `finally` callbacks they will be executed, but no other callbacks will run.
|
|
2659
2668
|
|
|
2660
2669
|
```ruby
|
|
2661
2670
|
class API < Grape::API
|
|
@@ -2679,12 +2688,12 @@ curl -v -X OPTIONS http://localhost:3000/rt_count
|
|
|
2679
2688
|
> OPTIONS /rt_count HTTP/1.1
|
|
2680
2689
|
>
|
|
2681
2690
|
< HTTP/1.1 204 No Content
|
|
2682
|
-
< Allow: OPTIONS, GET, PUT
|
|
2691
|
+
< Allow: OPTIONS, GET, PUT, HEAD
|
|
2683
2692
|
```
|
|
2684
2693
|
|
|
2685
2694
|
You can disable this behavior with `do_not_route_options!`.
|
|
2686
2695
|
|
|
2687
|
-
If a request for a resource is made with an unsupported HTTP method, an HTTP 405 (Method Not Allowed) response will be returned. If the resource has `before` callbacks they will be executed, but no other callbacks will run.
|
|
2696
|
+
If a request for a resource is made with an unsupported HTTP method, an HTTP 405 (Method Not Allowed) response will be returned. If the resource has `before` and `finally` callbacks they will be executed, but no other callbacks will run.
|
|
2688
2697
|
|
|
2689
2698
|
``` shell
|
|
2690
2699
|
curl -X DELETE -v http://localhost:3000/rt_count/
|
|
@@ -2693,7 +2702,7 @@ curl -X DELETE -v http://localhost:3000/rt_count/
|
|
|
2693
2702
|
> Host: localhost:3000
|
|
2694
2703
|
>
|
|
2695
2704
|
< HTTP/1.1 405 Method Not Allowed
|
|
2696
|
-
< Allow: OPTIONS, GET, PUT
|
|
2705
|
+
< Allow: OPTIONS, GET, PUT, HEAD
|
|
2697
2706
|
```
|
|
2698
2707
|
|
|
2699
2708
|
## Raising Exceptions
|
|
@@ -2722,6 +2731,17 @@ You can set additional headers for the response. They will be merged with header
|
|
|
2722
2731
|
error!('Something went wrong', 500, 'X-Error-Detail' => 'Invalid token.')
|
|
2723
2732
|
```
|
|
2724
2733
|
|
|
2734
|
+
`error!` raises a `Grape::Exceptions::Halt`, which Grape renders as the response without handing it to `rescue_from`. Since it is an exception, a database transaction it leaves is rolled back. It also reaches Grape from a fiber or thread whose result the route waits for, such as an `Async` task.
|
|
2735
|
+
|
|
2736
|
+
```ruby
|
|
2737
|
+
Order.transaction do
|
|
2738
|
+
order = Order.create!(declared(params))
|
|
2739
|
+
error!('Out of stock', 409) unless order.product.in_stock? # the order is rolled back
|
|
2740
|
+
end
|
|
2741
|
+
```
|
|
2742
|
+
|
|
2743
|
+
It is a `StandardError`, so a `rescue => e` around `error!` catches it as well. Rescue `Grape::Exceptions::Halt` first and re-raise it to let it through.
|
|
2744
|
+
|
|
2725
2745
|
You can present documented errors with a Grape entity using the [grape-entity](https://github.com/ruby-grape/grape-entity) gem.
|
|
2726
2746
|
|
|
2727
2747
|
```ruby
|
|
@@ -2733,22 +2753,22 @@ module API
|
|
|
2733
2753
|
end
|
|
2734
2754
|
```
|
|
2735
2755
|
|
|
2736
|
-
The following example specifies the entity to use in the `
|
|
2756
|
+
The following example specifies the entity to use in the `failure` definition.
|
|
2737
2757
|
|
|
2738
2758
|
```ruby
|
|
2739
2759
|
desc 'My Route' do
|
|
2740
|
-
failure [[
|
|
2760
|
+
failure [[401, 'Unauthorized', API::Error]]
|
|
2741
2761
|
end
|
|
2742
|
-
error!({ message: 'Unauthorized' },
|
|
2762
|
+
error!({ message: 'Unauthorized' }, 401)
|
|
2743
2763
|
```
|
|
2744
2764
|
|
|
2745
2765
|
The following example specifies the presented entity explicitly in the error message.
|
|
2746
2766
|
|
|
2747
2767
|
```ruby
|
|
2748
2768
|
desc 'My Route' do
|
|
2749
|
-
failure [[
|
|
2769
|
+
failure [[401, 'Unauthorized']]
|
|
2750
2770
|
end
|
|
2751
|
-
error!({ message: 'Unauthorized', with: API::Error },
|
|
2771
|
+
error!({ message: 'Unauthorized', with: API::Error }, 401)
|
|
2752
2772
|
```
|
|
2753
2773
|
|
|
2754
2774
|
### Default Error HTTP Status Code
|
|
@@ -2771,7 +2791,7 @@ In its simplest form, it can be like:
|
|
|
2771
2791
|
|
|
2772
2792
|
```ruby
|
|
2773
2793
|
route :any, '*path' do
|
|
2774
|
-
error! # or something else
|
|
2794
|
+
error!('Not Found', 404) # or something else
|
|
2775
2795
|
end
|
|
2776
2796
|
```
|
|
2777
2797
|
|
|
@@ -2830,7 +2850,7 @@ class Twitter::API < Grape::API
|
|
|
2830
2850
|
end
|
|
2831
2851
|
```
|
|
2832
2852
|
|
|
2833
|
-
|
|
2853
|
+
A class listed explicitly is rescued whether or not it inherits from `StandardError`; see [Exceptions that should be rescued explicitly](#exceptions-that-should-be-rescued-explicitly).
|
|
2834
2854
|
|
|
2835
2855
|
When several classes could match, the one registered **first** in a scope wins — as with the clauses of a Ruby `rescue`. Register the more specific class before the broader one, or the narrower handler never runs:
|
|
2836
2856
|
|
|
@@ -2869,7 +2889,7 @@ end
|
|
|
2869
2889
|
|
|
2870
2890
|
The error format will match the request format. See "Content-Types" below.
|
|
2871
2891
|
|
|
2872
|
-
Custom error formatters for existing and additional types can be defined with a proc. The formatter receives a `Grape::Exceptions::ErrorResponse`
|
|
2892
|
+
Custom error formatters for existing and additional types can be defined with a proc. The formatter receives a frozen `Grape::Exceptions::ErrorResponse` as `error:` (read its `status`, `message`, `headers`, `backtrace` and `original_exception`), plus three context kwargs — `env:`, `include_backtrace:`, `include_original_exception:`. Pull just the keys you need with `**` to ignore the rest:
|
|
2873
2893
|
|
|
2874
2894
|
```ruby
|
|
2875
2895
|
class Twitter::API < Grape::API
|
|
@@ -2884,7 +2904,7 @@ You can also use a module or class.
|
|
|
2884
2904
|
```ruby
|
|
2885
2905
|
module CustomFormatter
|
|
2886
2906
|
def self.call(error:, **)
|
|
2887
|
-
{ status: error.status, message: error.message, backtrace: error.backtrace }
|
|
2907
|
+
{ status: error.status, message: error.message, backtrace: error.backtrace }.to_json
|
|
2888
2908
|
end
|
|
2889
2909
|
end
|
|
2890
2910
|
|
|
@@ -2951,6 +2971,16 @@ When this handler is registered the framework hands the original exception to yo
|
|
|
2951
2971
|
|
|
2952
2972
|
A second raise inside the redispatched handler is not redispatched again — it goes straight to the framework's generic 500. This bounds the chain at one redispatch and prevents loops.
|
|
2953
2973
|
|
|
2974
|
+
To have an exception raised inside a `rescue_from` block propagate out of the middleware stack instead, so that Rack middleware above Grape can act on it:
|
|
2975
|
+
|
|
2976
|
+
```ruby
|
|
2977
|
+
Grape.configure do |config|
|
|
2978
|
+
config.raise_rescue_handler_errors = true
|
|
2979
|
+
end
|
|
2980
|
+
```
|
|
2981
|
+
|
|
2982
|
+
With this on the exception is re-raised untouched: it is not redispatched, so `rescue_from :internal_grape_exceptions` never sees it and neither `rack.exception` nor `grape.exception` is set. A block that calls `error!` still renders as usual.
|
|
2983
|
+
|
|
2954
2984
|
You can also rescue all exceptions with a code block and handle the Rack response at the lowest level.
|
|
2955
2985
|
|
|
2956
2986
|
```ruby
|
|
@@ -2993,7 +3023,7 @@ rescue_from APIErrors::ParentError do |e|
|
|
|
2993
3023
|
error!({
|
|
2994
3024
|
error: "#{e.class} error",
|
|
2995
3025
|
message: e.message
|
|
2996
|
-
},
|
|
3026
|
+
}, 500)
|
|
2997
3027
|
end
|
|
2998
3028
|
```
|
|
2999
3029
|
|
|
@@ -3003,10 +3033,8 @@ The code below will rescue exceptions of type `RuntimeError` but _not_ its subcl
|
|
|
3003
3033
|
```ruby
|
|
3004
3034
|
rescue_from RuntimeError, rescue_subclasses: false do |e|
|
|
3005
3035
|
error!({
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
errors: e.errors
|
|
3009
|
-
}, e.status)
|
|
3036
|
+
message: e.message
|
|
3037
|
+
}, 500)
|
|
3010
3038
|
end
|
|
3011
3039
|
```
|
|
3012
3040
|
|
|
@@ -3027,7 +3055,7 @@ class Twitter::API < Grape::API
|
|
|
3027
3055
|
end
|
|
3028
3056
|
```
|
|
3029
3057
|
|
|
3030
|
-
The `rescue_from` handler must return a `Rack::Response` object, call `error!`, or raise an exception (either the original exception or another custom one).
|
|
3058
|
+
The `rescue_from` handler must return a `Rack::Response` object, call `error!`, or raise an exception (either the original exception or another custom one). An exception raised in `rescue_from` is dispatched to the handler registered for its class; when there is none, Grape answers it itself, as described in [Re-raising from inside a `rescue_from` block](#re-raising-from-inside-a-rescue_from-block).
|
|
3031
3059
|
|
|
3032
3060
|
Alternately, use the `with` option in `rescue_from` to specify a method or a `proc`.
|
|
3033
3061
|
|
|
@@ -3171,6 +3199,8 @@ class API < Grape::API
|
|
|
3171
3199
|
end
|
|
3172
3200
|
```
|
|
3173
3201
|
|
|
3202
|
+
A logger belongs to the API it is set on, and to the APIs mounted under it that set none of their own. An API that sets none, and is mounted under none that does, logs to a default shared by every such API.
|
|
3203
|
+
|
|
3174
3204
|
For similar to Rails request logging try the [grape_logging](https://github.com/aserafin/grape_logging) or [grape-middleware-logger](https://github.com/ridiculous/grape-middleware-logger) gems.
|
|
3175
3205
|
|
|
3176
3206
|
## API Formats
|
|
@@ -3199,7 +3229,7 @@ If you declare any `content_type` whatsoever, the Grape defaults will be overrid
|
|
|
3199
3229
|
```ruby
|
|
3200
3230
|
class Twitter::API < Grape::API
|
|
3201
3231
|
content_type :xml, 'application/xml'
|
|
3202
|
-
content_type :rss, 'application/xml
|
|
3232
|
+
content_type :rss, 'application/rss+xml'
|
|
3203
3233
|
|
|
3204
3234
|
default_format :xml
|
|
3205
3235
|
end
|
|
@@ -3269,7 +3299,7 @@ end
|
|
|
3269
3299
|
* `GET /hello?format=xml` will respond with an HTTP 406 error code, because the XML format specified by the request parameter is not supported.
|
|
3270
3300
|
* `GET /hello` with an `Accept: application/xml` header will still respond with JSON, since it could not negotiate a recognized content-type from the headers and JSON is the effective default.
|
|
3271
3301
|
|
|
3272
|
-
The formats apply to parsing, too. The following API will only respond to the JSON content-type and will not parse any other input than `application/json`, `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related` and `multipart/mixed`. All other requests will fail with an HTTP
|
|
3302
|
+
The formats apply to parsing, too. The following API will only respond to the JSON content-type and will not parse any other input than `application/json`, `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related` and `multipart/mixed`. All other requests will fail with an HTTP 415 error code.
|
|
3273
3303
|
|
|
3274
3304
|
```ruby
|
|
3275
3305
|
class Twitter::API < Grape::API
|
|
@@ -3277,7 +3307,7 @@ class Twitter::API < Grape::API
|
|
|
3277
3307
|
end
|
|
3278
3308
|
```
|
|
3279
3309
|
|
|
3280
|
-
When the content-type is omitted, Grape will return a
|
|
3310
|
+
When the content-type is omitted, Grape will return a 415 error code unless `default_format` is specified.
|
|
3281
3311
|
The following API will try to parse any data without a content-type using a JSON parser.
|
|
3282
3312
|
|
|
3283
3313
|
```ruby
|
|
@@ -3288,13 +3318,14 @@ end
|
|
|
3288
3318
|
```
|
|
3289
3319
|
|
|
3290
3320
|
If you combine `format` with `rescue_from :all`, errors will be rendered using the same format.
|
|
3291
|
-
|
|
3321
|
+
`default_error_formatter` renders the errors of a format that has no error formatter of its own, such as one you declared yourself; it does not replace the error formatter of a built-in format like `:json`. To change how errors of a built-in format are rendered, register one for it with [`error_formatter`](#exception-handling).
|
|
3292
3322
|
|
|
3293
3323
|
```ruby
|
|
3294
3324
|
class Twitter::API < Grape::API
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3325
|
+
content_type :xls, 'application/vnd.ms-excel'
|
|
3326
|
+
format :xls
|
|
3327
|
+
|
|
3328
|
+
default_error_formatter :json # :xls has no error formatter of its own
|
|
3298
3329
|
end
|
|
3299
3330
|
```
|
|
3300
3331
|
|
|
@@ -3329,6 +3360,31 @@ Built-in formatters are the following.
|
|
|
3329
3360
|
* `:txt`: use object's `to_txt` when available, otherwise `to_s`
|
|
3330
3361
|
* `:serializable_hash`: use object's `serializable_hash` when available, otherwise fallback to `:json`
|
|
3331
3362
|
* `:binary`: data will be returned "as is"
|
|
3363
|
+
* `:jsonl`: one line per element of an Array or Enumerator, otherwise one line, each rendered as `:json` renders it. Not a default content type, see [JSON Lines](#json-lines)
|
|
3364
|
+
* `:md`, `:markdown`: use object's `to_markdown` when available, return a String "as is", otherwise raise `Grape::Exceptions::InvalidFormatter`. Not default content types, see [Serving Markdown](#serving-markdown)
|
|
3365
|
+
|
|
3366
|
+
#### Serving Markdown
|
|
3367
|
+
|
|
3368
|
+
`:md` and `:markdown` are not among the default content types. Declare either one to answer clients that ask for Markdown, such as LLM agents sending `Accept: text/markdown`.
|
|
3369
|
+
|
|
3370
|
+
```ruby
|
|
3371
|
+
class API < Grape::API
|
|
3372
|
+
content_type :json, 'application/json'
|
|
3373
|
+
content_type :md, 'text/markdown; charset=utf-8'
|
|
3374
|
+
default_format :json
|
|
3375
|
+
|
|
3376
|
+
get '/pages/:id' do
|
|
3377
|
+
Page.find(params[:id]) # renders with Page#to_markdown
|
|
3378
|
+
end
|
|
3379
|
+
end
|
|
3380
|
+
```
|
|
3381
|
+
|
|
3382
|
+
* `GET /pages/1.md`, `GET /pages/1.markdown`, `GET /pages/1?format=md`, `?format=markdown` and `GET /pages/1` with an `Accept: text/markdown` header answer `text/markdown`.
|
|
3383
|
+
* `:md` and `:markdown` name the same format: declaring either one answers both extensions, under the name declared. A request for `.markdown` on the API above negotiates `:md`, so a `formatter :md` of your own renders it too.
|
|
3384
|
+
* `to_markdown` is the method Rails' `render markdown:` calls (Rails 8.1), so an object written for a Rails controller renders the same way in Grape.
|
|
3385
|
+
* Grape does not convert anything else to Markdown. When the client negotiated Markdown, a body without `to_markdown` that is not a String answers in the default format instead; when the API pins `format :md`, it answers `500`.
|
|
3386
|
+
* [RFC 7763](https://www.rfc-editor.org/rfc/rfc7763) makes `charset` a required parameter of `text/markdown`, hence `charset=utf-8`.
|
|
3387
|
+
* Errors are rendered as plain text, which is valid Markdown.
|
|
3332
3388
|
|
|
3333
3389
|
#### Serving pre-rendered JSON
|
|
3334
3390
|
|
|
@@ -3369,6 +3425,50 @@ If a body is present in a request to an API, with a Content-Type header value th
|
|
|
3369
3425
|
|
|
3370
3426
|
Response statuses that indicate no content as defined by [Rack](https://github.com/rack) [here](https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L567) will bypass serialization and the body entity - though there should be none - will not be modified.
|
|
3371
3427
|
|
|
3428
|
+
#### JSON Lines
|
|
3429
|
+
|
|
3430
|
+
`:jsonl` serves [JSON Lines](https://jsonlines.org): one JSON value per line, each line ended by a newline. It is not among the default content types, so declare it, as `application/jsonl`, or `application/x-ndjson` if your clients expect that one.
|
|
3431
|
+
|
|
3432
|
+
```ruby
|
|
3433
|
+
class API < Grape::API
|
|
3434
|
+
content_type :jsonl, 'application/jsonl'
|
|
3435
|
+
format :jsonl
|
|
3436
|
+
|
|
3437
|
+
get :users do
|
|
3438
|
+
User.all # {"id":1,"name":"Ada"}\n{"id":2,"name":"Grace"}\n
|
|
3439
|
+
end
|
|
3440
|
+
end
|
|
3441
|
+
```
|
|
3442
|
+
|
|
3443
|
+
An `Array` renders one line per element and any other object one line, each as the `:json` formatter renders it, so a `Grape::PrecompiledJson` element is served as its own line. A `String` is served as it is, taken to be JSON Lines already.
|
|
3444
|
+
|
|
3445
|
+
An `Enumerator` is streamed: each element is rendered only as the server reads the body, so a large export is never held in memory whole.
|
|
3446
|
+
|
|
3447
|
+
```ruby
|
|
3448
|
+
get :export do
|
|
3449
|
+
User.find_each.lazy.map { |user| user.slice(:id, :name) }
|
|
3450
|
+
end
|
|
3451
|
+
```
|
|
3452
|
+
|
|
3453
|
+
The status and headers of a streamed response are sent before its first line is rendered, so an exception raised while it streams is past `rescue_from`: the response just ends early.
|
|
3454
|
+
|
|
3455
|
+
A request body of `application/jsonl` is parsed into an Array of its lines' values, skipping blank lines. Like a JSON body that is an Array, it is available in `env['api.request.body']` and is not merged into `params`. To validate the lines as params, wrap them in a Hash with a parser of your own.
|
|
3456
|
+
|
|
3457
|
+
```ruby
|
|
3458
|
+
parser :jsonl, ->(body, env) { { items: Grape::Parser::Jsonl.call(body, env) } }
|
|
3459
|
+
|
|
3460
|
+
params do
|
|
3461
|
+
requires :items, type: Array do
|
|
3462
|
+
requires :name, type: String
|
|
3463
|
+
end
|
|
3464
|
+
end
|
|
3465
|
+
post :import do
|
|
3466
|
+
declared(params)[:items]
|
|
3467
|
+
end
|
|
3468
|
+
```
|
|
3469
|
+
|
|
3470
|
+
An error renders as a single JSON line.
|
|
3471
|
+
|
|
3372
3472
|
### JSONP
|
|
3373
3473
|
|
|
3374
3474
|
Grape supports JSONP via [Rack::JSONP](https://github.com/rack/rack-contrib), part of the [rack-contrib](https://github.com/rack/rack-contrib) gem. Add `rack-contrib` to your `Gemfile`.
|
|
@@ -3418,7 +3518,7 @@ end
|
|
|
3418
3518
|
|
|
3419
3519
|
## API Data Formats
|
|
3420
3520
|
|
|
3421
|
-
Grape accepts and parses input data sent with the POST and
|
|
3521
|
+
Grape accepts and parses input data sent with the `POST`, `PUT`, `PATCH`, `DELETE` and `QUERY` methods as described in the Parameters section above. It also supports custom data formats. You must declare additional content-types via `content_type` and optionally supply a parser via `parser` unless a parser is already available within Grape to enable a custom format. Such a parser can be a function or a class.
|
|
3422
3522
|
|
|
3423
3523
|
With a parser, parsed data is available "as-is" in `env['api.request.body']`.
|
|
3424
3524
|
Without a parser, data is available "as-is" and in `env['api.request.input']`.
|
|
@@ -3476,9 +3576,9 @@ module API
|
|
|
3476
3576
|
expose :ip, if: { type: :full }
|
|
3477
3577
|
expose :user_type, :user_id, if: ->(status, options) { status.user.public? }
|
|
3478
3578
|
expose :digest do |status, options|
|
|
3479
|
-
Digest::MD5.hexdigest(status.
|
|
3579
|
+
Digest::MD5.hexdigest(status.text)
|
|
3480
3580
|
end
|
|
3481
|
-
expose :replies, using: API::Status, as: :replies
|
|
3581
|
+
expose :replies, using: API::Entities::Status, as: :replies
|
|
3482
3582
|
end
|
|
3483
3583
|
end
|
|
3484
3584
|
|
|
@@ -3486,7 +3586,7 @@ module API
|
|
|
3486
3586
|
version 'v1'
|
|
3487
3587
|
|
|
3488
3588
|
desc 'Statuses index' do
|
|
3489
|
-
params
|
|
3589
|
+
params API::Entities::Status.documentation
|
|
3490
3590
|
end
|
|
3491
3591
|
get '/statuses' do
|
|
3492
3592
|
statuses = Status.all
|
|
@@ -3556,9 +3656,9 @@ You can present `hash` with `Grape::Presenters::Presenter` to keep things consis
|
|
|
3556
3656
|
|
|
3557
3657
|
```ruby
|
|
3558
3658
|
get '/users' do
|
|
3559
|
-
present
|
|
3659
|
+
present({ id: 10, name: :dgz }, with: Grape::Presenters::Presenter)
|
|
3560
3660
|
end
|
|
3561
|
-
|
|
3661
|
+
```
|
|
3562
3662
|
The response will be
|
|
3563
3663
|
|
|
3564
3664
|
```ruby
|
|
@@ -3697,9 +3797,9 @@ Example:
|
|
|
3697
3797
|
Grape::Middleware::Auth::Strategies.add(:my_auth, AuthMiddleware, ->(options) { [options[:realm]] } )
|
|
3698
3798
|
|
|
3699
3799
|
|
|
3700
|
-
auth :my_auth,
|
|
3800
|
+
auth :my_auth, realm: 'Test Api' do |username, password|
|
|
3701
3801
|
# lookup the user's password here
|
|
3702
|
-
{ 'user1' => 'password1' }[username]
|
|
3802
|
+
{ 'user1' => 'password1' }[username] == password
|
|
3703
3803
|
end
|
|
3704
3804
|
|
|
3705
3805
|
```
|
|
@@ -3728,7 +3828,7 @@ end
|
|
|
3728
3828
|
Examine the routes at runtime.
|
|
3729
3829
|
|
|
3730
3830
|
```ruby
|
|
3731
|
-
TwitterAPI::versions # yields [
|
|
3831
|
+
TwitterAPI::versions # yields ['v1']
|
|
3732
3832
|
TwitterAPI::routes # yields an array of Grape::Route objects
|
|
3733
3833
|
TwitterAPI::routes[0].version # => 'v1'
|
|
3734
3834
|
TwitterAPI::routes[0].description # => 'Includes custom settings.'
|
|
@@ -3755,7 +3855,7 @@ class MyAPI < Grape::API
|
|
|
3755
3855
|
requires :id, type: Integer, desc: 'Identity.'
|
|
3756
3856
|
end
|
|
3757
3857
|
get 'params/:id' do
|
|
3758
|
-
route.params[
|
|
3858
|
+
route.params['id'] # yields the parameter description
|
|
3759
3859
|
end
|
|
3760
3860
|
end
|
|
3761
3861
|
```
|
|
@@ -3767,7 +3867,7 @@ class ApiLogger < Grape::Middleware::Base
|
|
|
3767
3867
|
def before
|
|
3768
3868
|
file = env['api.endpoint'].source.source_location[0]
|
|
3769
3869
|
line = env['api.endpoint'].source.source_location[1]
|
|
3770
|
-
logger
|
|
3870
|
+
env['rack.logger']&.debug "[api] #{file}:#{line}"
|
|
3771
3871
|
end
|
|
3772
3872
|
end
|
|
3773
3873
|
```
|
|
@@ -3789,9 +3889,9 @@ Before and after callbacks execute in the following order:
|
|
|
3789
3889
|
|
|
3790
3890
|
Steps 4, 5 and 6 only happen if validation succeeds.
|
|
3791
3891
|
|
|
3792
|
-
If a request for a resource is made with an unsupported HTTP method (returning HTTP 405) only `before` callbacks will be executed. The remaining callbacks will be bypassed.
|
|
3892
|
+
If a request for a resource is made with an unsupported HTTP method (returning HTTP 405) only `before` and `finally` callbacks will be executed. The remaining callbacks will be bypassed.
|
|
3793
3893
|
|
|
3794
|
-
If a request for a resource is made that triggers the built-in `OPTIONS` handler, only `before` and `
|
|
3894
|
+
If a request for a resource is made that triggers the built-in `OPTIONS` handler, only `before`, `after` and `finally` callbacks will be executed. The remaining callbacks will be bypassed.
|
|
3795
3895
|
|
|
3796
3896
|
For example, using a simple `before` block to set a header.
|
|
3797
3897
|
|
|
@@ -3869,7 +3969,7 @@ The behavior is then:
|
|
|
3869
3969
|
|
|
3870
3970
|
```bash
|
|
3871
3971
|
GET /123 # 'Integer'
|
|
3872
|
-
GET /foo #
|
|
3972
|
+
GET /foo # 404, the declared Integer narrows the path to digits
|
|
3873
3973
|
```
|
|
3874
3974
|
|
|
3875
3975
|
**Versioning**
|
|
@@ -3903,8 +4003,8 @@ end
|
|
|
3903
4003
|
The behavior is then:
|
|
3904
4004
|
|
|
3905
4005
|
```bash
|
|
3906
|
-
GET /foo
|
|
3907
|
-
GET /foo
|
|
4006
|
+
GET /v1/foo # 'v1-hello'
|
|
4007
|
+
GET /v2/foo # 'v2-hello'
|
|
3908
4008
|
```
|
|
3909
4009
|
|
|
3910
4010
|
**Altering Responses**
|
|
@@ -3936,7 +4036,7 @@ Instead of altering a response, you can also terminate and rewrite it from any c
|
|
|
3936
4036
|
|
|
3937
4037
|
## Anchoring
|
|
3938
4038
|
|
|
3939
|
-
Grape by default anchors all request paths, which means that the request URL should match from start to end to match, otherwise a `404 Not Found` is returned. However, this is sometimes not what you want, because it is not always known upfront what can be expected from the call.
|
|
4039
|
+
Grape by default anchors all request paths, which means that the request URL should match from start to end to match, otherwise a `404 Not Found` is returned. However, this is sometimes not what you want, because it is not always known upfront what can be expected from the call.
|
|
3940
4040
|
Rails solves this problem by using a `anchor: false` option in your routes.
|
|
3941
4041
|
In Grape this option can be used as well when a method is defined.
|
|
3942
4042
|
|
|
@@ -4121,7 +4221,7 @@ describe Twitter::API do
|
|
|
4121
4221
|
it 'creates many statuses' do
|
|
4122
4222
|
statuses = [{ text: '...' }, { text: '...'}]
|
|
4123
4223
|
post '/api/statuses', statuses.to_json, 'CONTENT_TYPE' => 'application/json'
|
|
4124
|
-
expect(last_response.
|
|
4224
|
+
expect(last_response.status).to eq 201
|
|
4125
4225
|
end
|
|
4126
4226
|
end
|
|
4127
4227
|
end
|
|
@@ -4154,7 +4254,7 @@ end
|
|
|
4154
4254
|
```ruby
|
|
4155
4255
|
require 'test_helper'
|
|
4156
4256
|
|
|
4157
|
-
class Twitter::APITest <
|
|
4257
|
+
class Twitter::APITest < Minitest::Test
|
|
4158
4258
|
include Rack::Test::Methods
|
|
4159
4259
|
|
|
4160
4260
|
def app
|
|
@@ -4380,10 +4480,10 @@ For example, `payload[:endpoint].body` provides the current state of the respons
|
|
|
4380
4480
|
```ruby
|
|
4381
4481
|
ActiveSupport::Notifications.subscribe(/v1/) do |name, start, finish, id, payload|
|
|
4382
4482
|
hook_record = {
|
|
4383
|
-
hook: name
|
|
4384
|
-
status: payload[:env]&.dig("api.endpoint")&.status
|
|
4385
|
-
format: payload[:env]&.dig("api.format")
|
|
4386
|
-
body: payload[:endpoint]&.body
|
|
4483
|
+
hook: name,
|
|
4484
|
+
status: payload[:env]&.dig("api.endpoint")&.status,
|
|
4485
|
+
format: payload[:env]&.dig("api.format"),
|
|
4486
|
+
body: payload[:endpoint]&.body,
|
|
4387
4487
|
duration: (finish - start) * 1000
|
|
4388
4488
|
}
|
|
4389
4489
|
# your code to save the notification
|