apicraft-rails 0.5.1.beta1 → 1.0.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: 03c78edc81f87417dfdd7411be50609da4d39bb0827775ea16b609cce641a7bd
4
- data.tar.gz: 652eb5761da35bc28f8a779015c74ef0b971d94231250aa02edcd3c3ff1cc7b3
3
+ metadata.gz: 7692b6e9a2cd444c2f5db48a932c78d64e4a23172575cafb76ed307c527c1ed3
4
+ data.tar.gz: 120c7cf23160c70761a168a815704bbcafc2e1c87901d5b2c58c037c840c4151
5
5
  SHA512:
6
- metadata.gz: fe96cef636e07bc57e26ff11401e315ab457783cf254f33de98ec233a8f2c4b72be7c4b4f593f023deb615b17ce5166f8309fabc6a298139913d58e478363489
7
- data.tar.gz: f23638693ebe922a7de1b98bfef20f7cad9777704c21603489ffe6677afc33ec9020173a2cfdb76dd59c52e7246a42dbb55fcd2443f9ff7fa4946a6764c84dd6
6
+ metadata.gz: dfe9dc9efc333d44b43825b5f0b5a3377ffdb1e3016a725c131b923ac15be131b5e8bc5e1ad6dc06cfd58ccc7ae493cf18b0c54846507491828550c884657e0b
7
+ data.tar.gz: a9bb65b6c85b07e5a17c52b99bcf59435cff0810191358b214fdd45bbea8cb5e181f5d7e1332431b74c9473f6a6a531e063e9391500eef78fad67b2b2d197231
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # APICraft Rails (Beta)
1
+ # APICraft Rails
2
2
  [![Build](https://github.com/apicraft-dev/apicraft-rails/actions/workflows/build.yml/badge.svg)](https://github.com/apicraft-dev/apicraft-rails/actions/workflows/build.yml)
3
- [![Gem Version](https://badge.fury.io/rb/apicraft-rails.svg)](https://badge.fury.io/rb/apicraft-rails)
3
+ [![Gem Version](https://badge.fury.io/rb/apicraft-rails.svg?v=1.0.0)](https://badge.fury.io/rb/apicraft-rails)
4
4
 
5
5
  🚀 Accelerates your development by 2-3x with an API Design First approach. Seamlessly integrates with your Rails application server — no fancy tooling or expenses required.
6
6
 
@@ -12,23 +12,26 @@ It avoids the pitfalls of the code-first methodology, where contracts are auto-g
12
12
 
13
13
  ![APICraft Rails Logo](assets/apicraft_rails.png)
14
14
 
15
- - [APICraft Rails (Beta)](#apicraft-rails-beta)
15
+ - [APICraft Rails](#apicraft-rails)
16
16
  - [✨ Features](#-features)
17
17
  - [🔜 Upcoming Features](#-upcoming-features)
18
18
  - [🪄 Works Like Magic](#-works-like-magic)
19
19
  - [🕊 API Design First Philosophy](#-api-design-first-philosophy)
20
20
  - [🏗 Installation](#-installation)
21
21
  - [⚙️ Usage](#️-usage)
22
- - [🎭 API Mocking](#-api-mocking)
23
- - [🎮 API Mocking (Behaviours)](#-api-mocking-behaviours)
24
- - [🧐 API Introspection](#-api-introspection)
25
- - [📖 API Documentation (Swagger docs and RapiDoc)](#-api-documentation-swagger-docs-and-rapidoc)
22
+ - [🛡️ Request Validations](#️-request-validations)
23
+ - [🎭 Mocking](#-mocking)
24
+ - [🎮 Behaviour Mocking](#-behaviour-mocking)
25
+ - [🧐 Introspection](#-introspection)
26
+ - [📖 Documentation (Swagger docs and RapiDoc)](#-documentation-swagger-docs-and-rapidoc)
26
27
  - [🔧 Configuration](#-configuration)
27
28
  - [🤝 Contributing](#-contributing)
28
29
  - [📝 License](#-license)
29
30
  - [📘 Code of Conduct](#-code-of-conduct)
30
31
 
31
32
  ## ✨ Features
33
+ - 🛡️ **Automatic Request Validations** - Validates the request based on the openapi specs so that you don't need to add params validations everywhere in your controllers.
34
+
32
35
  - 🧑‍💻️ **Dynamic Mock Data Generation** - Detects the specifications and instantly mounts working routes with mock responses. No extra configuration required.
33
36
 
34
37
  - ⚙️ **Customizable Mock Responses** - Tailor mock responses to simulate different scenarios and edge cases, helping your team prepare for real-world conditions right from the start.
@@ -40,7 +43,6 @@ It avoids the pitfalls of the code-first methodology, where contracts are auto-g
40
43
  - 🗂 **Easy Contracts Management** - Management of `openapi` specifications from within `app/contracts` directory. No new syntax, just plain old `openapi` standard with `.json` or `.yaml` formats
41
44
 
42
45
  ## 🔜 Upcoming Features
43
- - 💢 **Request Validations** - Automatic request validations.
44
46
  - 💎 **Clean & Custom Ruby DSL** - Support for a Ruby DSL alongwith the current `.json` and `.yaml` formats.
45
47
 
46
48
 
@@ -72,7 +74,7 @@ By adopting an API Design First approach with APICraft Rails, you can accelerate
72
74
  Add this line to your application's Gemfile:
73
75
 
74
76
  ```ruby
75
- gem 'apicraft-rails', '~> 0.5.1.beta1'
77
+ gem 'apicraft-rails', '~> 1.0.0'
76
78
  ```
77
79
 
78
80
  And then execute:
@@ -89,8 +91,11 @@ module App
89
91
  class Application < Rails::Application
90
92
  # Rest of the configuration...
91
93
 
92
- config.middleware.use Apicraft::Middlewares::Mocker
93
- config.middleware.use Apicraft::Middlewares::Introspector
94
+ [
95
+ Apicraft::Middlewares::Mocker,
96
+ Apicraft::Middlewares::Introspector,
97
+ Apicraft::Middlewares::RequestValidator
98
+ ].each { |mw| config.middleware.use mw }
94
99
 
95
100
  Apicraft.configure do |config|
96
101
  config.contracts_path = Rails.root.join("app/contracts")
@@ -117,7 +122,11 @@ my_rails_app/
117
122
  │ │ ├── user.rb
118
123
  │ │ └── order.rb
119
124
  ```
120
- ### 🎭 API Mocking
125
+
126
+ ### 🛡️ Request Validations
127
+ All incoming requests will be validated against the defined schema. This ensures that by the time the params reach the controller they are adhering to all the schema requirements. It's enabled by default. You can customize the response of a failed validation. Check the [configuration section](#-configuration) section for a full list of options for this.
128
+
129
+ ### 🎭 Mocking
121
130
  **APICraft** dynamically generates mock APIs by interpreting contract specifications on the fly. You can request the mock response by passing `Apicraft-Mock: true` in the headers.
122
131
 
123
132
  `https://yoursite.com/api/orders`
@@ -141,14 +150,16 @@ headers: {
141
150
  ]
142
151
  ```
143
152
 
144
- ### 🎮 API Mocking (Behaviours)
153
+ ### 🎮 Behaviour Mocking
145
154
  The above is an example of a 200 response. If you have more responses documented you can force that behaviour using `Apicraft-Response-Code` header in the mock request.
155
+ You can find a list of all the supported headers in the [configuration section](#-configuration) that would allow you to manipulate the API Behaviour.
146
156
 
147
157
  `https://yoursite.com/api/orders`
148
158
  ```
149
159
  headers: {
150
160
  Apicraft-Response-Code: 400
151
161
  Apicraft-Mock: true
162
+ Apicraft-Delay: 5
152
163
  }
153
164
  ```
154
165
  ```json
@@ -158,12 +169,12 @@ headers: {
158
169
  }
159
170
  ```
160
171
 
161
- ### 🧐 API Introspection
162
- All APIs are can be introspected. You can do so by passing the `Apicraft-Introspection` header.
172
+ ### 🧐 Introspection
173
+ All APIs are can be introspected. You can do so by passing the `Apicraft-Introspect` header.
163
174
 
164
175
  ```
165
176
  headers: {
166
- Apicraft-Introspection: true
177
+ Apicraft-Introspect: true
167
178
  }
168
179
  ```
169
180
 
@@ -193,7 +204,7 @@ Example: `https://yoursite.com/api/orders`
193
204
  }
194
205
  }
195
206
  ```
196
- ### 📖 API Documentation (Swagger docs and RapiDoc)
207
+ ### 📖 Documentation (Swagger docs and RapiDoc)
197
208
 
198
209
  Mount the documentation views in your route file.
199
210
 
@@ -223,6 +234,9 @@ module App
223
234
  end
224
235
  end
225
236
  ```
237
+ RapiDoc | SwaggerDoc
238
+ :-------------------------:|:-------------------------:
239
+ ![](assets/rapidoc.png) | ![](assets/swaggerdoc.png)
226
240
 
227
241
  ## 🔧 Configuration
228
242
 
@@ -249,6 +263,10 @@ Apicraft.configure do |config|
249
263
  # Defaults to true
250
264
  config.strict_reference_validation = true
251
265
 
266
+ # When simulating delay using the mocks, the max
267
+ # delay in seconds that can be simulated
268
+ config.max_allowed_delay = 30
269
+
252
270
  config.headers = {
253
271
  # The name of the header used to control
254
272
  # the response code of the mock
@@ -257,11 +275,28 @@ Apicraft.configure do |config|
257
275
 
258
276
  # The name of the header to introspect the API.
259
277
  # Defaults to Apicraft-Introspect
260
- introspect: "Apicraft-Introspect"
278
+ introspect: "Apicraft-Introspect",
261
279
 
262
280
  # The name of the header to mock the API.
263
281
  # Defaults to Apicraft-Mock
264
- mock: "Apicraft-Mock"
282
+ mock: "Apicraft-Mock",
283
+
284
+ # Delay simulation header name
285
+ delay: "Apicraft-Delay"
286
+ }
287
+
288
+ config.request_validation = {
289
+ enabled: true,
290
+
291
+ # Return the http code for validation errors, defaults to 400
292
+ http_code: 400,
293
+
294
+ # Return a custom response body, defaults to `{ message: "..." }`
295
+ response_body: proc do |ex|
296
+ {
297
+ message: ex.message
298
+ }
299
+ end
265
300
  }
266
301
  end
267
302
 
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apicraft
4
+ module Concerns
5
+ # Helper class with shared methods
6
+ module MiddlewareUtil
7
+ def config
8
+ @config ||= Apicraft.config
9
+ end
10
+
11
+ def convertor(format)
12
+ return if format.blank?
13
+
14
+ Apicraft::Constants::MIME_TYPE_CONVERTORS[format]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "concerns/cacheable"
4
+ require_relative "concerns/middleware_util"
4
5
 
5
6
  module Apicraft
6
7
  # Namespace module for Concerns
@@ -13,18 +13,22 @@ module Apicraft
13
13
  response_code: "Apicraft-Response-Code",
14
14
  introspect: "Apicraft-Introspect",
15
15
  mock: "Apicraft-Mock",
16
+ delay: "Apicraft-Delay",
16
17
  content_type: "Content-Type"
17
18
  },
18
19
  mocks: true,
19
20
  introspection: true,
20
21
  strict_reference_validation: true,
21
- request_validations: true
22
+ max_allowed_delay: 30,
23
+ request_validation: {
24
+ enabled: true,
25
+ http_code: 400,
26
+ response_body: proc { |ex| { message: ex.message } }
27
+ }
22
28
  }.with_indifferent_access
23
29
 
24
- def initialize(opts = {})
25
- @opts = DEFAULTS.merge(
26
- opts
27
- ).with_indifferent_access
30
+ def initialize
31
+ @opts = DEFAULTS
28
32
  end
29
33
 
30
34
  def headers
@@ -47,6 +51,26 @@ module Apicraft
47
51
  @opts[:introspection]
48
52
  end
49
53
 
54
+ def max_allowed_delay
55
+ @opts[:max_allowed_delay]
56
+ end
57
+
58
+ def request_validation
59
+ @opts[:request_validation]
60
+ end
61
+
62
+ def request_validation_enabled?
63
+ @opts[:request_validation][:enabled]
64
+ end
65
+
66
+ def request_validation_http_code
67
+ @opts[:request_validation][:http_code] || DEFAULTS[:request_validation][:http_code]
68
+ end
69
+
70
+ def request_validation_response_body
71
+ @opts[:request_validation][:response_body]
72
+ end
73
+
50
74
  def contracts_path=(contracts_path)
51
75
  @opts[:contracts_path] = contracts_path
52
76
  end
@@ -63,8 +87,14 @@ module Apicraft
63
87
  @opts[:strict_reference_validation] = enabled
64
88
  end
65
89
 
66
- def request_validations=(enabled)
67
- @opts[:request_validations] = enabled
90
+ def request_validation=(request_validation_opts)
91
+ @opts[:request_validation] = @opts[:request_validation].merge(
92
+ request_validation_opts.with_indifferent_access
93
+ )
94
+ end
95
+
96
+ def max_allowed_delay=(enabled)
97
+ @opts[:max_allowed_delay] = enabled
68
98
  end
69
99
 
70
100
  def headers=(headers)
@@ -7,5 +7,6 @@ module Apicraft
7
7
  class InvalidContract < StandardError; end
8
8
  class InvalidOperation < StandardError; end
9
9
  class InvalidResponse < StandardError; end
10
+ class DelayTooHigh < StandardError; end
10
11
  end
11
12
  end
@@ -4,6 +4,8 @@ module Apicraft
4
4
  module Middlewares
5
5
  # Apicraft Middleware to handle API Introspection.
6
6
  class Introspector
7
+ include Concerns::MiddlewareUtil
8
+
7
9
  def initialize(app)
8
10
  @app = app
9
11
  end
@@ -30,10 +32,6 @@ module Apicraft
30
32
 
31
33
  private
32
34
 
33
- def config
34
- @config ||= Apicraft.config
35
- end
36
-
37
35
  def introspect?(request)
38
36
  request.headers[config.headers[:introspect]].present?
39
37
  end
@@ -5,6 +5,8 @@ module Apicraft
5
5
  # Apicraft Middleware to handle routing
6
6
  # and make mock calls available.
7
7
  class Mocker
8
+ include Concerns::MiddlewareUtil
9
+
8
10
  def initialize(app)
9
11
  @app = app
10
12
  end
@@ -15,28 +17,15 @@ module Apicraft
15
17
  request = ActionDispatch::Request.new(env)
16
18
  return @app.call(env) unless mock?(request)
17
19
 
18
- contract = Apicraft::Openapi::Contract.find_by_operation(
19
- request.method, request.path_info
20
- )
21
- raise Errors::InvalidContract if contract.blank?
22
-
23
- operation = contract.operation(
24
- request.method, request.path_info
25
- )
26
- raise Errors::InvalidOperation if operation.blank?
20
+ use_delay!(request)
21
+ contract = find_contract!(request)
22
+ operation = find_operation!(contract, request)
27
23
 
28
24
  code = request.headers[config.headers[:response_code]] || "200"
29
25
  response = operation.response_for(code.to_s)
30
26
  raise Errors::InvalidResponse if response.blank?
31
27
 
32
- # Determine the format passed in the request.
33
- # If passed we use it and the response format.
34
- # If not we use the first format from the specs.
35
- request.format.to_s
36
- # indicates that not format was specified.
37
- format = nil
38
-
39
- content, content_type = response.mock(format)
28
+ content, content_type = response.mock(request.content_type)
40
29
 
41
30
  [
42
31
  code.to_i,
@@ -51,18 +40,37 @@ module Apicraft
51
40
 
52
41
  private
53
42
 
54
- def config
55
- @config ||= Apicraft.config
43
+ def mock?(request)
44
+ request.headers[config.headers[:mock]].present?
56
45
  end
57
46
 
58
- def convertor(format)
59
- return if format.blank?
47
+ def find_contract!(request)
48
+ contract = Apicraft::Openapi::Contract.find_by_operation(
49
+ request.method, request.path_info
50
+ )
51
+ raise Errors::InvalidContract if contract.blank?
60
52
 
61
- Apicraft::Constants::MIME_TYPE_CONVERTORS[format]
53
+ contract
62
54
  end
63
55
 
64
- def mock?(request)
65
- request.headers[config.headers[:mock]].present?
56
+ def find_operation!(contract, request)
57
+ operation = contract.operation(
58
+ request.method, request.path_info
59
+ )
60
+ raise Errors::InvalidOperation if operation.blank?
61
+
62
+ operation
63
+ end
64
+
65
+ def use_delay!(request)
66
+ request_delay = delay(request)
67
+ raise Errors::DelayTooHigh if request_delay > config.max_allowed_delay
68
+
69
+ sleep(request_delay)
70
+ end
71
+
72
+ def delay(request)
73
+ request.headers[config.headers[:delay]].to_i
66
74
  end
67
75
  end
68
76
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apicraft
4
+ module Middlewares
5
+ # Middleware to handle Request Validation.
6
+ class RequestValidator
7
+ include Concerns::MiddlewareUtil
8
+
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ def call(env)
14
+ return @app.call(env) unless validate?
15
+
16
+ request = ActionDispatch::Request.new(env)
17
+ content_type = request.content_type
18
+
19
+ operation = Apicraft::Openapi::Contract.find_by_operation(
20
+ request.method, request.path_info
21
+ )&.operation(
22
+ request.method, request.path_info
23
+ )
24
+ return @app.call(env) if operation.blank?
25
+
26
+ operation.validate_request_body(
27
+ content_type,
28
+ request.params
29
+ )
30
+ @app.call(env)
31
+ rescue OpenAPIParser::OpenAPIError => e
32
+ [
33
+ config.request_validation_http_code,
34
+ { 'Content-Type': content_type },
35
+ [
36
+ response_body(e)&.send(convertor(content_type))
37
+ ].compact
38
+ ]
39
+ end
40
+
41
+ private
42
+
43
+ def validate?
44
+ config.request_validation_enabled?
45
+ end
46
+
47
+ def response_body(ex)
48
+ config.request_validation_response_body.call(ex)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "middlewares/mocker"
4
4
  require_relative "middlewares/introspector"
5
+ require_relative "middlewares/request_validator"
5
6
 
6
7
  module Apicraft
7
8
  # Namespace module for Concerns
@@ -15,10 +15,10 @@ module Apicraft
15
15
  end
16
16
 
17
17
  def operation(method, path)
18
- with_cache("operation-#{method.downcase}-#{path}") do
19
- op = document.request_operation(method.downcase, path)
20
- Operation.new(op) if op.present?
21
- end
18
+ # with_cache("operation-#{method.downcase}-#{path}") do
19
+ op = document.request_operation(method.downcase, path)
20
+ Operation.new(op) if op.present?
21
+ # end
22
22
  end
23
23
 
24
24
  class << self
@@ -8,15 +8,16 @@ module Apicraft
8
8
  attr_accessor :operation
9
9
 
10
10
  def initialize(operation)
11
- @operation = operation.operation_object
11
+ @operation = operation
12
+ @operation_object = operation.operation_object
12
13
  end
13
14
 
14
15
  def responses
15
- @operation.responses
16
+ @operation_object.responses
16
17
  end
17
18
 
18
19
  def summary
19
- @operation.summary
20
+ @operation_object.summary
20
21
  end
21
22
 
22
23
  def response_for(code)
@@ -29,7 +30,14 @@ module Apicraft
29
30
  end
30
31
 
31
32
  def raw_schema
32
- operation.raw_schema
33
+ @operation_object.raw_schema
34
+ end
35
+
36
+ def validate_request_body(content_type, body)
37
+ operation.validate_request_body(
38
+ content_type,
39
+ body
40
+ )
33
41
  end
34
42
  end
35
43
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Current version of Apicraft.
4
4
  module Apicraft
5
- VERSION = "0.5.1.beta1"
5
+ VERSION = "1.0.0"
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apicraft-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1.beta1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abhishek Sarkar
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-10 00:00:00.000000000 Z
11
+ date: 2024-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -126,6 +126,7 @@ files:
126
126
  - lib/apicraft-rails.rb
127
127
  - lib/apicraft/concerns.rb
128
128
  - lib/apicraft/concerns/cacheable.rb
129
+ - lib/apicraft/concerns/middleware_util.rb
129
130
  - lib/apicraft/config.rb
130
131
  - lib/apicraft/constants.rb
131
132
  - lib/apicraft/errors.rb
@@ -133,6 +134,7 @@ files:
133
134
  - lib/apicraft/middlewares.rb
134
135
  - lib/apicraft/middlewares/introspector.rb
135
136
  - lib/apicraft/middlewares/mocker.rb
137
+ - lib/apicraft/middlewares/request_validator.rb
136
138
  - lib/apicraft/mocker.rb
137
139
  - lib/apicraft/mocker/all_of.rb
138
140
  - lib/apicraft/mocker/any_of.rb
@@ -183,7 +185,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
183
185
  - !ruby/object:Gem::Version
184
186
  version: '0'
185
187
  requirements: []
186
- rubygems_version: 3.5.11
188
+ rubygems_version: 3.2.33
187
189
  signing_key:
188
190
  specification_version: 4
189
191
  summary: APICraft Rails - Simplified API Design First Development