openapi_first 0.10.2 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49ebcf2af159defac87d97f5d9cc1b2c76a1f76ce09376f209bc38458c2eee07
4
- data.tar.gz: dbe17ecf51792614df624c91a704f9a319bf1bf2b1b1cded454a748f193c2e21
3
+ metadata.gz: '03752095ad50ff4a6142b3f716f00aba532191a12f700227eca0d2d3c9c7a6b1'
4
+ data.tar.gz: 7c0271d081181b18999ce75dc72ee9fa4677eb18fc0ee02ed1a40505da9aa072
5
5
  SHA512:
6
- metadata.gz: 9d478201cf1e856d745dff02e4977552b95c09500956be61c6d169cf179a1f15968bca460a63a9ce76fb97bba0c5d5713e94d5e802d8fe0446428fdfd6b3feb8
7
- data.tar.gz: dac17e6ec186b821a6fbdf09c47c20478f43bb21d30175b5d3e8150e0e99cf98110b2a7c01e143207176733485b52df1972277bc6790a7730c6a365dde32e3b1
6
+ metadata.gz: f7a2454ed16c69e7d0b32f610ed985640735bba36a1e28798319b501f3f1eb80bb14b8329c8794f0dd87e3d38db037210319e3e25f82b1b5e0110f63c24ea4d3
7
+ data.tar.gz: 479eedd62e341f834ed258504d2699a94cfa924301ae85a016d075c56df870041e3b0504829bbce456694e138f7598129c20684731257ff6be23d5dba4b9b2f8
@@ -8,10 +8,22 @@ Metrics/BlockLength:
8
8
  Exclude:
9
9
  - 'spec/**/*.rb'
10
10
  - '*.gemspec'
11
+ Layout/EmptyLinesAroundAttributeAccessor:
12
+ Enabled: true
11
13
  Layout/SpaceAroundMethodCallOperator:
12
14
  Enabled: true
15
+ Lint/DeprecatedOpenSSLConstant:
16
+ Enabled: true
13
17
  Lint/RaiseException:
14
18
  Enabled: true
19
+ Lint/MixedRegexpCaptureTypes:
20
+ Enabled: true
21
+ Style/RedundantRegexpCharacterClass:
22
+ Enabled: true
23
+ Style/RedundantRegexpEscape:
24
+ Enabled: true
25
+ Style/SlicingWithRange:
26
+ Enabled: true
15
27
  Lint/StructNewOverride:
16
28
  Enabled: true
17
29
  Style/HashEachMethods:
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.0
4
+ - Change `ResponseValidator` to raise an exception if it found a problem
5
+ - Params have symbolized keys now
6
+ - Remove `not_found` option from Router. Return 405 if HTTP verb is not allowed (via Hanami::Router)
7
+ - Add `raise_error` option to OpenapiFirst.app (false by default)
8
+ - Add ResponseValidation to OpenapiFirst.app if raise_error option is true
9
+ - Rename `raise` option to `raise_error`
10
+ - Add `raise_error` option to RequestValidation middleware
11
+ - Raise error if handler could not be found by Responder
12
+ - Add `Operation#name` that returns a human readable name for an operation
13
+
14
+ ## 0.11.0
15
+ - Raise error if you forgot to add the Router middleware
16
+ - Make OpenapiFirst.app raise an error in test env when request path is not specified
17
+ - Rename OperationResolver to Responder
18
+ - Add ResponseValidation middleware that validates the response body
19
+ - Add `raise` option to Router middleware to raise an error if request could not be found in the API description similar to committee's raise option.
20
+ - Move namespace option from Router to OperationResolver
21
+
3
22
  ## 0.10.2
4
23
  - Return 400 if request body has invalid JSON ([issue](https://github.com/ahx/openapi_first/issues/73)) thanks Thomas Frütel
5
24
 
@@ -1,9 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openapi_first (0.10.2)
4
+ openapi_first (0.12.0)
5
5
  deep_merge (>= 1.2.1)
6
- hanami-router (~> 2.0.alpha2)
6
+ hanami-router (~> 2.0.alpha3)
7
7
  hanami-utils (~> 2.0.alpha1)
8
8
  json_schemer (~> 0.2)
9
9
  multi_json (~> 1.14)
@@ -13,7 +13,7 @@ PATH
13
13
  GEM
14
14
  remote: https://rubygems.org/
15
15
  specs:
16
- activesupport (6.0.3)
16
+ activesupport (6.0.3.1)
17
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
18
18
  i18n (>= 0.7, < 2)
19
19
  minitest (~> 5.1)
@@ -21,16 +21,16 @@ GEM
21
21
  zeitwerk (~> 2.2, >= 2.2.2)
22
22
  addressable (2.7.0)
23
23
  public_suffix (>= 2.0.2, < 5.0)
24
- ast (2.4.0)
24
+ ast (2.4.1)
25
25
  builder (3.2.4)
26
- coderay (1.1.2)
26
+ coderay (1.1.3)
27
27
  concurrent-ruby (1.1.6)
28
28
  deep_merge (1.2.1)
29
29
  diff-lcs (1.3)
30
30
  ecma-re-validator (0.2.1)
31
31
  regexp_parser (~> 1.2)
32
32
  hana (1.3.6)
33
- hanami-router (2.0.0.alpha2)
33
+ hanami-router (2.0.0.alpha3)
34
34
  mustermann (~> 1.0)
35
35
  mustermann-contrib (~> 1.0)
36
36
  rack (~> 2.0)
@@ -39,9 +39,8 @@ GEM
39
39
  transproc (~> 1.0)
40
40
  hansi (0.2.0)
41
41
  hash-deep-merge (0.1.1)
42
- i18n (1.8.2)
42
+ i18n (1.8.3)
43
43
  concurrent-ruby (~> 1.0)
44
- jaro_winkler (1.5.4)
45
44
  json_schemer (0.2.11)
46
45
  ecma-re-validator (~> 0.2)
47
46
  hana (~> 1.3)
@@ -49,7 +48,7 @@ GEM
49
48
  uri_template (~> 0.7)
50
49
  method_source (1.0.0)
51
50
  mini_portile2 (2.4.0)
52
- minitest (5.14.0)
51
+ minitest (5.14.1)
53
52
  multi_json (1.14.1)
54
53
  mustermann (1.1.1)
55
54
  ruby2_keywords (~> 0.0.1)
@@ -67,18 +66,18 @@ GEM
67
66
  mustermann-contrib (~> 1.1.1)
68
67
  nokogiri
69
68
  parallel (1.19.1)
70
- parser (2.7.1.2)
69
+ parser (2.7.1.3)
71
70
  ast (~> 2.4.0)
72
71
  pry (0.13.1)
73
72
  coderay (~> 1.1)
74
73
  method_source (~> 1.0)
75
- public_suffix (4.0.4)
74
+ public_suffix (4.0.5)
76
75
  rack (2.2.2)
77
76
  rack-test (1.1.0)
78
77
  rack (>= 1.0, < 3)
79
78
  rainbow (3.0.0)
80
79
  rake (13.0.1)
81
- regexp_parser (1.7.0)
80
+ regexp_parser (1.7.1)
82
81
  rexml (3.2.4)
83
82
  rspec (3.9.0)
84
83
  rspec-core (~> 3.9.0)
@@ -86,21 +85,24 @@ GEM
86
85
  rspec-mocks (~> 3.9.0)
87
86
  rspec-core (3.9.2)
88
87
  rspec-support (~> 3.9.3)
89
- rspec-expectations (3.9.1)
88
+ rspec-expectations (3.9.2)
90
89
  diff-lcs (>= 1.2.0, < 2.0)
91
90
  rspec-support (~> 3.9.0)
92
91
  rspec-mocks (3.9.1)
93
92
  diff-lcs (>= 1.2.0, < 2.0)
94
93
  rspec-support (~> 3.9.0)
95
94
  rspec-support (3.9.3)
96
- rubocop (0.82.0)
97
- jaro_winkler (~> 1.5.1)
95
+ rubocop (0.85.1)
98
96
  parallel (~> 1.10)
99
97
  parser (>= 2.7.0.1)
100
98
  rainbow (>= 2.2.2, < 4.0)
99
+ regexp_parser (>= 1.7)
101
100
  rexml
101
+ rubocop-ast (>= 0.0.3)
102
102
  ruby-progressbar (~> 1.7)
103
103
  unicode-display_width (>= 1.4.0, < 2.0)
104
+ rubocop-ast (0.0.3)
105
+ parser (>= 2.7.0.1)
104
106
  ruby-progressbar (1.10.1)
105
107
  ruby2_keywords (0.0.2)
106
108
  thread_safe (0.3.6)
data/README.md CHANGED
@@ -1,73 +1,124 @@
1
1
  # OpenapiFirst
2
2
 
3
+ [![Join the chat at https://gitter.im/openapi_first/community](https://badges.gitter.im/openapi_first/community.svg)](https://gitter.im/openapi_first/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4
+
3
5
  OpenapiFirst helps to implement HTTP APIs based on an [OpenApi](https://www.openapis.org/) API description. The idea is that you create an API description first, then add code that returns data and implements your business logic and be done.
4
6
 
5
- Start with writing an OpenAPI file that describes the API, which you are about to write. Use a [validator](https://github.com/stoplightio/spectral/) to make sure the file is valid.
7
+ Start with writing an OpenAPI file that describes the API, which you are about to implement. Use a [validator](https://github.com/stoplightio/spectral/) to make sure the file is valid.
8
+
9
+ You can use OpenapiFirst via its [Rack middlewares](#rack-middlewares) or in [standalone mode](#standalone-usage).
10
+
11
+ ## Alternatives
12
+
13
+ This gem is inspired by [committee](https://github.com/interagent/committee) (Ruby) and [connexion](https://github.com/zalando/connexion) (Python).
14
+
15
+ Here's a [comparison between committee and openapi_first](https://gist.github.com/ahx/1538c31f0652f459861713b5259e366a).
6
16
 
7
17
  ## Rack middlewares
8
18
  OpenapiFirst consists of these Rack middlewares:
9
19
 
10
- - `OpenapiFirst::Router` finds the operation for the current request or returns 404 if no operation was found.
11
- - `OpenapiFirst::RequestValidation` validates the request against the found operation and returns 400 if the request is invalid.
12
- - `OpenapiFirst::OperationResolver` calls the [handler](#handlers) found for the operation.
20
+ - [`OpenapiFirst::Router`](#OpenapiFirst::Router) Finds the OpenAPI operation for the current request or returns 404 if no operation was found. This can be customized.
21
+ - [`OpenapiFirst::RequestValidation`](#OpenapiFirst::RequestValidation) Validates the request against the API description and returns 400 if the request is invalid.
22
+ - [`OpenapiFirst::Responder`](#OpenapiFirst::Responder) calls the [handler](#handlers) found for the operation.
23
+ - [`OpenapiFirst::ResponseValidation`](#OpenapiFirst::ResponseValidation) Validates the response and raises an exception if the response body is invalid.
13
24
 
14
- ## Usage within your Rack webframework
15
- If you just want to use the request validation part without any handlers you can use the rack middlewares standalone:
25
+ ## OpenapiFirst::Router
26
+ You always have to add this middleware first in order to make the other middlewares work.
16
27
 
17
28
  ```ruby
18
29
  use OpenapiFirst::Router, spec: OpenapiFirst.load('./openapi/openapi.yaml')
19
- use OpenapiFirst::RequestValidation
20
30
  ```
21
31
 
22
- ### Rack env variables
23
- These variables will available in your rack env:
32
+ This middleware adds `env[OpenapiFirst::OPERATION]` which holds an Operation object that responds to `operation_id` and `path`.
24
33
 
25
- - `env[OpenapiFirst::OPERATION]` - Holds an Operation object that responsed about `operation_id` and `path`. This is useful for introspection.
26
- - `env[OpenapiFirst::INBOX]`. Holds the (filtered) path and query parameters and the parsed request body.
34
+ Options and their defaults:
27
35
 
28
- ## Standalone usage
29
- You can implement your API in conveniently with just OpenapiFirst.
36
+ | Name | Possible values | Description | Default
37
+ |:---|---|---|---|
38
+ |`spec:`| | The spec loaded via `OpenapiFirst.load` ||
39
+ | `raise_error:` |`false`, `true` | If set to true the middleware raises `OpenapiFirst::NotFoundError` when a path or method was not found in the API description. This is useful during testing to spot an incomplete API description. | `false` (don't raise an exception)
40
+
41
+ ## OpenapiFirst::RequestValidation
42
+
43
+ This middleware returns a 400 status code with a body that describes the error if the request is not valid.
30
44
 
31
45
  ```ruby
32
- module Pets
33
- def self.find_pet(params, res)
46
+ use OpenapiFirst::RequestValidation
47
+ ```
48
+
49
+
50
+ Options and their defaults:
51
+
52
+ | Name | Possible values | Description | Default
53
+ |:---|---|---|---|
54
+ | `raise_error:` |`false`, `true` | If set to true the middleware raises `OpenapiFirst::RequestInvalidError` instead of returning 4xx. | `false` (don't raise an exception)
55
+
56
+ The error responses conform with [JSON:API](https://jsonapi.org).
57
+
58
+ Here's an example response body for a missing query parameter "search":
59
+
60
+ ```json
61
+ http-status: 400
62
+ content-type: "application/vnd.api+json"
63
+
64
+ {
65
+ "errors": [
34
66
  {
35
- id: params['id'],
36
- name: 'Oscar'
67
+ "title": "is missing",
68
+ "source": {
69
+ "parameter": "search"
70
+ }
37
71
  }
38
- end
39
- end
40
-
41
- # In config.ru:
42
- require 'openapi_first'
43
- run OpenapiFirst.app('./openapi/openapi.yaml', namespace: Pets)
72
+ ]
73
+ }
44
74
  ```
45
75
 
46
- The above will use the mentioned Rack middlewares to:
76
+ This middleware adds `env[OpenapiFirst::INBOX]` which holds the (filtered) path and query parameters and the parsed request body.
47
77
 
48
- - Validate the request and respond with 400 if the request does not match with your API description
49
- - Map the request to a method call `Pets.find_pet` based on the `operationId` in the API description
50
- - Set the response content type according to your spec (here with the default status code `200`)
78
+ ### Parameter validation
51
79
 
52
- Handler functions (`find_pet`) are called with two arguments:
80
+ The middleware filteres all top-level query parameters and paths parameters and tries to convert numeric values. Meaning, if you have an `:something_id` path with `type: integer`, it will try convert the value to an integer.
81
+ Note that is currently does not convert date, date-time or time formats and that conversion is currently done only for path and query parameters, but not for the request body. It just works with a parameter with `name: filter[age]`.
53
82
 
54
- - `params` - Holds the parsed request body, filtered query params and path parameters
55
- - `res` - Holds a Rack::Response that you can modify if needed
56
- If you want to access to plain Rack env you can call `params.env`.
83
+ If you want to forbid _nested_ query parameters you will need to use [`additionalProperties: false`](https://json-schema.org/understanding-json-schema/reference/object.html#properties) in your query parameter JSON schema.
57
84
 
58
- ### Handlers
85
+ _OpenapiFirst always treats query parameters like [`style: deepObject`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#style-values), **but** it just works with nested objects (`filter[foo][bar]=baz`) (see [this discussion](https://github.com/OAI/OpenAPI-Specification/issues/1706))._
86
+
87
+ ### Request body validation
88
+
89
+ The middleware will return a status `415` if the requests content type does not match or `400` if the request body is invalid.
90
+ This will also add the parsed request body to `env[OpenapiFirst::REQUEST_BODY]`.
91
+
92
+ ### Header, Cookie, Path parameter validation
93
+
94
+ tbd.
59
95
 
60
- OpenapiFirst maps the HTTP request to a method call based on the [operationId](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operation-object) in your API description and calls it via the `OperationResolver` middleware.
96
+ ## OpenapiFirst::Responder
97
+
98
+ This Rack endpoint maps the HTTP request to a method call based on the [operationId](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operation-object) in your API description and calls it. Responder also adds a content-type to the response.
99
+
100
+ Currently there are no customization options for this part. Please [share your ideas](#contributing) on how to best meet your needs and preferred style.
101
+
102
+ ```ruby
103
+ run OpenapiFirst::Responder, spec: OpenapiFirst.load('./openapi/openapi.yaml')
104
+ ```
105
+
106
+ | Name | Description
107
+ |:---|---|
108
+ |`spec:`| The spec loaded via `OpenapiFirst.load` |
109
+ | `namespace:` | A class or module where to find the handler method. |
61
110
 
62
111
  It works like this:
63
112
 
64
- - "create_pet" or "createPet" or "create pet" calls `MyApi.create_pet(params, response)`
113
+ - An operationId "create_pet" or "createPet" or "create pet" calls `MyApi.create_pet(params, response)`
65
114
  - "some_things.create" calls: `MyApi::SomeThings.create(params, response)`
66
115
  - "pets#create" calls: `MyApi::Pets::Create.new.call(params, response)` If `MyApi::Pets::Create.new` accepts an argument, it will pass the rack `env`.
67
116
 
117
+ ### Handlers
118
+
68
119
  These handler methods are called with two arguments:
69
120
 
70
- - `params` - Holds the parsed request body, filtered query params and path parameters
121
+ - `params` - Holds the parsed request body, filtered query params and path parameters (same as `env[OpenapiFirst::INBOX]`)
71
122
  - `res` - Holds a Rack::Response that you can modify if needed
72
123
 
73
124
  You can call `params.env` to access the Rack env (just like in [Hanami actions](https://guides.hanamirb.org/actions/parameters/))
@@ -75,75 +126,70 @@ You can call `params.env` to access the Rack env (just like in [Hanami actions](
75
126
  There are two ways to set the response body:
76
127
 
77
128
  - Calling `res.write "things"` (see [Rack::Response](https://www.rubydoc.info/github/rack/rack/Rack/Response))
78
- - Returning a value from the function (see example above) (this will always converted to JSON)
129
+ - Returning a value which will get converted to JSON
79
130
 
80
- ### If your API description does not contain all endpoints
131
+ ## OpenapiFirst::ResponseValidation
132
+ This middleware is especially useful when testing. It *always* raises an error if the response is not valid.
81
133
 
82
134
  ```ruby
83
- run OpenapiFirst.middleware('./openapi/openapi.yaml', namespace: Pets)
135
+ use OpenapiFirst::ResponseValidation if ENV['RACK_ENV'] == 'test'
84
136
  ```
85
137
 
86
- Here all requests that are not part of the API description will be passed to the next app.
87
-
88
- ### Try it out
89
-
90
- See [examples](examples).
91
-
92
- ## Installation
93
-
94
- Add this line to your application's Gemfile:
138
+ ## Standalone usage
139
+ Instead of composing these middlewares yourself you can use `OpenapiFirst.app`.
95
140
 
96
141
  ```ruby
97
- gem 'openapi_first'
142
+ module Pets
143
+ def self.find_pet(params, res)
144
+ {
145
+ id: params[:id],
146
+ name: 'Oscar'
147
+ }
148
+ end
149
+ end
150
+
151
+ # In config.ru:
152
+ require 'openapi_first'
153
+ run OpenapiFirst.app('./openapi/openapi.yaml', namespace: Pets)
98
154
  ```
99
155
 
100
- OpenapiFirst uses [`multi_json`](https://rubygems.org/gems/multi_json).
156
+ The above will use the mentioned Rack middlewares to:
101
157
 
102
- ## Request validation
158
+ - Validate the request and respond with 400 if the request does not match with your API description
159
+ - Map the request to a method call `Pets.find_pet` based on the `operationId` in the API description
160
+ - Set the response content type according to your spec (here with the default status code `200`)
103
161
 
104
- If the request is not valid, these middlewares return a 400 status code with a body that describes the error.
162
+ Handler functions (`find_pet`) are called with two arguments:
105
163
 
106
- The error responses conform with [JSON:API](https://jsonapi.org).
164
+ - `params` - Holds the parsed request body, filtered query params and path parameters
165
+ - `res` - Holds a Rack::Response that you can modify if needed
166
+ If you want to access to plain Rack env you can call `params.env`.
107
167
 
108
- Here's an example response body for a missing query parameter "search":
168
+ ## If your API description does not contain all endpoints
109
169
 
110
- ```json
111
- http-status: 400
112
- content-type: "application/vnd.api+json"
113
-
114
- {
115
- "errors": [
116
- {
117
- "title": "is missing",
118
- "source": {
119
- "parameter": "search"
120
- }
121
- }
122
- ]
123
- }
170
+ ```ruby
171
+ run OpenapiFirst.middleware('./openapi/openapi.yaml', namespace: Pets)
124
172
  ```
125
173
 
126
- ### Parameter validation
127
-
128
- The middleware filteres all top-level query parameters and paths parameters and tries to convert numeric values. Meaning, if you have an `:something_id` path with `type: integer`, it will try convert the value to an integer.
129
- Note that is currently does not convert date, date-time or time formats and that conversion is currently on done for path and query parameters, but not for request bodies.
174
+ Here all requests that are not part of the API description will be passed to the next app.
130
175
 
131
- If you want to forbid _nested_ query parameters you will need to use [`additionalProperties: false`](https://json-schema.org/understanding-json-schema/reference/object.html#properties) in your query parameter JSON schema.
176
+ ## Try it out
132
177
 
133
- _OpenapiFirst always treats query parameters like [`style: deepObject`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#style-values), **but** it just works with nested objects (`filter[foo][bar]=baz`) (see [this discussion](https://github.com/OAI/OpenAPI-Specification/issues/1706))._
178
+ See [examples](examples).
134
179
 
135
- ### Request body validation
180
+ ## Installation
136
181
 
137
- The middleware will return a `415` if the requests content type does not match or `400` if the request body is invalid.
138
- This will add the parsed request body to `env[OpenapiFirst::REQUEST_BODY]`.
182
+ Add this line to your application's Gemfile:
139
183
 
140
- ### Header, Cookie, Path parameter validation
184
+ ```ruby
185
+ gem 'openapi_first'
186
+ ```
141
187
 
142
- tbd.
188
+ OpenapiFirst uses [`multi_json`](https://rubygems.org/gems/multi_json).
143
189
 
144
- ## Response validation
190
+ ## Manual response validation
145
191
 
146
- Response validation is useful to make sure your app responds as described in your API description. You usually do this in your tests using [rack-test](https://github.com/rack-test/rack-test).
192
+ Instead of using the ResponseValidation middleware you can validate the response in your test manually via [rack-test](https://github.com/rack-test/rack-test) and ResponseValidator.
147
193
 
148
194
  ```ruby
149
195
  # In your test (rspec example):
@@ -151,7 +197,8 @@ require 'openapi_first'
151
197
  spec = OpenapiFirst.load('petstore.yaml')
152
198
  validator = OpenapiFirst::ResponseValidator.new(spec)
153
199
 
154
- expect(validator.validate(last_request, last_response).errors).to be_empty
200
+ # This will raise an exception if it found an error
201
+ validator.validate(last_request, last_response)
155
202
  ```
156
203
 
157
204
  ## Handling only certain paths
@@ -202,10 +249,6 @@ end
202
249
 
203
250
  Out of scope. Use [Prism](https://github.com/stoplightio/prism) or [fakeit](https://github.com/JustinFeng/fakeit).
204
251
 
205
- ## Alternatives
206
-
207
- This gem is inspired by [committee](https://github.com/interagent/committee), which has much more features like response stubs or support for Hyper-Schema or OpenAPI 2.
208
-
209
252
  ## Development
210
253
 
211
254
  Run `bin/setup` to install dependencies.
@@ -224,6 +267,6 @@ bundle exec ruby benchmarks.rb
224
267
 
225
268
  ## Contributing
226
269
 
227
- If you have a question or an idea or found a bug don't hesitate to [create an issue on GitHub](https://github.com/ahx/openapi_first/issues).
270
+ If you have a question or an idea or found a bug don't hesitate to [create an issue on GitHub](https://github.com/ahx/openapi_first/issues) or [reach out via chat](https://gitter.im/openapi_first/community).
228
271
 
229
272
  Pull requests are very welcome as well, of course. Feel free to create a "draft" pull request early on, even if your change is still work in progress. 🤗