action_spec 0.2.0 → 1.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/README.md +101 -31
- data/lib/action_spec/open_api/generator.rb +5 -3
- data/lib/action_spec/open_api/schema.rb +11 -0
- data/lib/action_spec/railtie.rb +6 -4
- data/lib/action_spec/schema/active_record.rb +155 -0
- data/lib/action_spec/schema/array_of.rb +1 -1
- data/lib/action_spec/schema/base.rb +2 -1
- data/lib/action_spec/schema/object_of.rb +1 -1
- data/lib/action_spec/schema/scalar.rb +1 -1
- data/lib/action_spec/schema.rb +2 -1
- data/lib/action_spec/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0adf599aac952bde5c1969385d71ea5a023fe20814abd835a8ab55dca32ca319
|
|
4
|
+
data.tar.gz: 6761792a92ae44fd93b7fad87d350d952494ff6c3b7bc17023ae5fd7020d373f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5490efab1cd187ab21a97c10401dc3471c81dccc161e40955b5e53cb2f9b5cacf375cb55cbed382bc19f47e7bd0b9f69aa870c4ca0bf9f78663b6f89e1973d74
|
|
7
|
+
data.tar.gz: ee1d9eb0eaacbbc8df53b5db72568006dde268ecf400f0de4f60087ee60511a51fde327716c5a23612398dd01a0f6e37665123569f28e9e95bba32bd74a03107
|
data/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Concise and Powerful API Documentation Solution for Rails.
|
|
|
6
6
|
|
|
7
7
|
- OpenAPI version: `v3.2.0`
|
|
8
8
|
- Requires: Ruby 3.1+ and Rails 7.0+
|
|
9
|
-
- Note: this project was implemented with Codex in about
|
|
9
|
+
- Note: this project was implemented with Codex in about 2 hours, has not yet been manually reviewed, and has not been validated in production. It does, however, come with fairly detailed RSpec tests generated with Codex.
|
|
10
10
|
|
|
11
11
|
## Table Of Contents
|
|
12
12
|
|
|
@@ -16,10 +16,11 @@ Concise and Powerful API Documentation Solution for Rails.
|
|
|
16
16
|
- [`doc_dry`](#doc_dry)
|
|
17
17
|
- [DSL Reference](#dsl-reference)
|
|
18
18
|
- [Schemas](#schemas)
|
|
19
|
-
- [Required
|
|
20
|
-
- [
|
|
19
|
+
- [Declare A Required Field](#declare-a-required-field)
|
|
20
|
+
- [Field Types](#field-types)
|
|
21
|
+
- [Field Options](#field-options)
|
|
22
|
+
- [Schemas From ActiveRecord](#schemas-from-activerecord)
|
|
21
23
|
- [Type And Boundary Matrix](#type-and-boundary-matrix)
|
|
22
|
-
- [Supported Runtime Options](#supported-runtime-options)
|
|
23
24
|
- [Parameter Validation And Type Coercion](#parameter-validation-and-type-coercion)
|
|
24
25
|
- [Validation Flow](#validation-flow)
|
|
25
26
|
- [Reading Validated Values With `px`](#reading-validated-values-with-px)
|
|
@@ -250,7 +251,7 @@ Response declarations are stored as metadata now. They are not yet used to rende
|
|
|
250
251
|
|
|
251
252
|
## Schemas
|
|
252
253
|
|
|
253
|
-
#### Required
|
|
254
|
+
#### Declare A Required Field
|
|
254
255
|
|
|
255
256
|
Use `!` in either place:
|
|
256
257
|
|
|
@@ -271,7 +272,7 @@ Meaning of `!`:
|
|
|
271
272
|
- keys such as `name!:` or `nickname!:` mark nested object fields as required
|
|
272
273
|
- `body!`, `json!`, and `form!` are currently accepted for DSL consistency, but today they behave the same as the non-bang form at runtime
|
|
273
274
|
|
|
274
|
-
####
|
|
275
|
+
#### Field Types
|
|
275
276
|
|
|
276
277
|
Scalar types currently supported by validation/coercion:
|
|
277
278
|
|
|
@@ -294,28 +295,12 @@ json data: {
|
|
|
294
295
|
profile: {
|
|
295
296
|
nickname!: String
|
|
296
297
|
},
|
|
297
|
-
settings: { type: Object }
|
|
298
|
+
settings: { type: Object },
|
|
299
|
+
users: [{ id: Integer }]
|
|
298
300
|
}
|
|
299
301
|
```
|
|
300
302
|
|
|
301
|
-
####
|
|
302
|
-
|
|
303
|
-
| Type | Accepted examples | Rejected examples / notes |
|
|
304
|
-
| --- | --- | --- |
|
|
305
|
-
| `String` | `12`, `true`, `""` | Follows `ActiveModel::Type::String`, so `true` becomes `"t"` |
|
|
306
|
-
| `Integer` | `"0"`, `"-12"`, `"+7"`, `12` | Rejects `"12.3"`, `"abc"`, `""` |
|
|
307
|
-
| `Float` | `"0"`, `"-12.5"`, `12`, `12.5` | Rejects `"12.3.4"`, `"abc"` |
|
|
308
|
-
| `BigDecimal` | `"0"`, `"-12.50"`, `12`, `12.5` | Rejects `"abc"` |
|
|
309
|
-
| `:boolean` / host-defined `Boolean` | `true`, `false`, `"1"`, `"0"`, `"true"`, `"false"`, `"yes"`, `"no"`, `"on"`, `"off"` | Rejects ambiguous values such as `""`, `"2"`, `"TRUE "`, `"maybe"` |
|
|
310
|
-
| `Date` | `"2025-10-17"` | Rejects invalid dates such as `"2025-02-30"` |
|
|
311
|
-
| `DateTime` | `"2025-10-17T12:30:00Z"` | Rejects invalid datetimes such as `"2025-10-17 25:00:00"` |
|
|
312
|
-
| `Time` | `"2025-10-17T12:30:00Z"` | Follows `ActiveModel::Type::Time`, so the date part becomes `2000-01-01` |
|
|
313
|
-
| `File` | `ActionDispatch::Http::UploadedFile`, `Tempfile`, file-like IO objects | Keeps the object as-is and does not read file contents into memory |
|
|
314
|
-
| `Object` | `Hash`, `ActionController::Parameters`, arbitrary Ruby objects | Passed through for scalar `Object`; nested hashes use object schema resolution |
|
|
315
|
-
| `[Type]` | arrays such as `%w[1 2 3]` for `[Integer]` | Rejects non-array values, and reports item errors like `tags.1` |
|
|
316
|
-
| nested object | `{ profile: { nickname: "neo" } }` | Rejects non-hash values, and reports nested paths like `profile.nickname` |
|
|
317
|
-
|
|
318
|
-
#### Supported Runtime Options
|
|
303
|
+
#### Field Options
|
|
319
304
|
|
|
320
305
|
These options are currently used by the validator:
|
|
321
306
|
|
|
@@ -327,14 +312,85 @@ query :score, Integer, range: { ge: 1, le: 5 }
|
|
|
327
312
|
query :slug, String, pattern: /\A[a-z\-]+\z/
|
|
328
313
|
```
|
|
329
314
|
|
|
330
|
-
These options are currently
|
|
315
|
+
These options are currently used by OpenAPI generation, but are not yet used by the runtime validator:
|
|
331
316
|
|
|
332
317
|
- `desc`
|
|
333
318
|
- `example`
|
|
334
319
|
- `examples`
|
|
320
|
+
|
|
321
|
+
These options are not yet used by either the runtime validator or OpenAPI generation:
|
|
322
|
+
|
|
335
323
|
- `allow_nil`
|
|
336
324
|
- `allow_blank`
|
|
337
325
|
|
|
326
|
+
#### Schemas From ActiveRecord
|
|
327
|
+
|
|
328
|
+
If your model is an `ActiveRecord::Base`, you can derive an ActionSpec-friendly schema hash directly from the model:
|
|
329
|
+
|
|
330
|
+
```ruby
|
|
331
|
+
class UsersController < ApplicationController
|
|
332
|
+
doc {
|
|
333
|
+
form data: User.schemas
|
|
334
|
+
}
|
|
335
|
+
def create
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
`User.schemas` returns a hash that can be passed directly into `form data:`, `json data:`, or `body`.
|
|
341
|
+
|
|
342
|
+
By default it includes all model fields:
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
User.schemas
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
You can also limit the exported fields:
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
User.schemas(only: %i[name phone role])
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
ActionSpec extracts schema-relevant information from ActiveRecord / ActiveModel when available, including:
|
|
355
|
+
|
|
356
|
+
- field type
|
|
357
|
+
- requiredness, rendered as bang keys such as `"name!"`
|
|
358
|
+
- enum values from `enum`
|
|
359
|
+
- `default`
|
|
360
|
+
- `desc` from column comments
|
|
361
|
+
- `pattern` from format validators
|
|
362
|
+
- `range` from numericality validators
|
|
363
|
+
- `length` from length validators and string column limits
|
|
364
|
+
|
|
365
|
+
Example output:
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
User.schemas
|
|
369
|
+
# {
|
|
370
|
+
# "name!" => { type: String, desc: "user name", length: { maximum: 20 } },
|
|
371
|
+
# "phone!" => { type: String, length: { maximum: 13 }, pattern: /\A1\d{10}\z/ },
|
|
372
|
+
# "role" => { type: String, enum: %w[admin member visitor] }
|
|
373
|
+
# }
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
#### Type And Boundary Matrix
|
|
377
|
+
|
|
378
|
+
| Type | Accepted examples | Rejected examples / notes |
|
|
379
|
+
| --- | --- | --- |
|
|
380
|
+
| `String` | `12`, `true`, `""` | Follows `ActiveModel::Type::String`, so `true` becomes `"t"` |
|
|
381
|
+
| `Integer` | `"0"`, `"-12"`, `"+7"`, `12` | Rejects `"12.3"`, `"abc"`, `""` |
|
|
382
|
+
| `Float` | `"0"`, `"-12.5"`, `12`, `12.5` | Rejects `"12.3.4"`, `"abc"` |
|
|
383
|
+
| `BigDecimal` | `"0"`, `"-12.50"`, `12`, `12.5` | Rejects `"abc"` |
|
|
384
|
+
| `:boolean` / `Boolean` | `true`, `false`, `"1"`, `"0"`, `"true"`, `"false"`, `"yes"`, `"no"`, `"on"`, `"off"` | Rejects ambiguous values such as `""`, `"2"`, `"TRUE "`, `"maybe"` |
|
|
385
|
+
| `Date` | `"2025-10-17"` | Rejects invalid dates such as `"2025-02-30"` |
|
|
386
|
+
| `DateTime` | `"2025-10-17T12:30:00Z"` | Rejects invalid datetimes such as `"2025-10-17 25:00:00"` |
|
|
387
|
+
| `Time` | `"2025-10-17T12:30:00Z"` | Follows `ActiveModel::Type::Time`, so the date part becomes `2000-01-01` |
|
|
388
|
+
| `File` | `ActionDispatch::Http::UploadedFile`, `Tempfile`, file-like IO objects | Keeps the object as-is and does not read file contents into memory |
|
|
389
|
+
| `Object` | `Hash`, `ActionController::Parameters`, arbitrary Ruby objects | Passed through for scalar `Object`; nested hashes use object schema resolution |
|
|
390
|
+
| `[Type]` | arrays such as `%w[1 2 3]` for `[Integer]` | Rejects non-array values, and reports item errors like `tags.1` |
|
|
391
|
+
| nested object | `{ profile: { nickname: "neo" } }` | Rejects non-hash values, and reports nested paths like `profile.nickname` |
|
|
392
|
+
|
|
393
|
+
|
|
338
394
|
## Parameter Validation And Type Coercion
|
|
339
395
|
|
|
340
396
|
### Validation Flow
|
|
@@ -501,7 +557,7 @@ Available config keys:
|
|
|
501
557
|
|
|
502
558
|
### I18n
|
|
503
559
|
|
|
504
|
-
ActionSpec
|
|
560
|
+
ActionSpec uses `ActiveModel::Errors`, so you can override both messages and attribute names:
|
|
505
561
|
|
|
506
562
|
```yml
|
|
507
563
|
en:
|
|
@@ -529,10 +585,24 @@ end
|
|
|
529
585
|
|
|
530
586
|
## What Is Not Implemented Yet
|
|
531
587
|
|
|
532
|
-
-
|
|
533
|
-
-
|
|
534
|
-
-
|
|
535
|
-
-
|
|
588
|
+
- reusable `components` generation
|
|
589
|
+
- `$ref` generation and deduplication
|
|
590
|
+
- `description`, `operationId`, `tags`, `externalDocs`, `deprecated`, and `security` on operations
|
|
591
|
+
- parameter-level `style`, `explode`, `allowReserved`, `examples`, and richer header/cookie serialization controls
|
|
592
|
+
- request body `encoding`
|
|
593
|
+
- multiple request/response media types beyond the current direct DSL mapping
|
|
594
|
+
- response body schema generation; current `response` / `resp` / `error` declarations only generate response descriptions
|
|
595
|
+
- response headers
|
|
596
|
+
- response links
|
|
597
|
+
- callbacks
|
|
598
|
+
- webhooks
|
|
599
|
+
- path-level shared parameters
|
|
600
|
+
- top-level `components.parameters`, `components.requestBodies`, `components.responses`, `components.headers`, `components.examples`, `components.links`, `components.callbacks`, `components.schemas`, `components.securitySchemes`, and `components.pathItems`
|
|
601
|
+
- top-level `security`
|
|
602
|
+
- top-level `tags`
|
|
603
|
+
- top-level `externalDocs`
|
|
604
|
+
- `jsonSchemaDialect`
|
|
605
|
+
- richer schema keywords beyond the current subset, including nullable/blank semantics, object-level constraints, and composition keywords such as `oneOf`, `anyOf`, `allOf`, and `not`
|
|
536
606
|
|
|
537
607
|
## Contributing
|
|
538
608
|
.
|
|
@@ -53,7 +53,9 @@ module ActionSpec
|
|
|
53
53
|
next if path.blank?
|
|
54
54
|
|
|
55
55
|
hash[path] ||= ActiveSupport::OrderedHash.new
|
|
56
|
-
|
|
56
|
+
route_verbs(route).each do |verb|
|
|
57
|
+
hash[path][verb] = Operation.new(endpoint).build
|
|
58
|
+
end
|
|
57
59
|
end
|
|
58
60
|
end
|
|
59
61
|
|
|
@@ -87,7 +89,7 @@ module ActionSpec
|
|
|
87
89
|
defaults.to_h.symbolize_keys
|
|
88
90
|
end
|
|
89
91
|
|
|
90
|
-
def
|
|
92
|
+
def route_verbs(route)
|
|
91
93
|
raw_verb =
|
|
92
94
|
if route.respond_to?(:verb) && route.verb.respond_to?(:source)
|
|
93
95
|
route.verb.source
|
|
@@ -97,7 +99,7 @@ module ActionSpec
|
|
|
97
99
|
route[:verb].to_s
|
|
98
100
|
end
|
|
99
101
|
|
|
100
|
-
raw_verb.gsub(/[$^]/, "").split("|").
|
|
102
|
+
raw_verb.gsub(/[$^]/, "").split("|").filter_map { |verb| verb.presence&.downcase }
|
|
101
103
|
end
|
|
102
104
|
|
|
103
105
|
def normalized_path(route)
|
|
@@ -108,6 +108,7 @@ module ActionSpec
|
|
|
108
108
|
definition["default"] = schema.default unless schema.default.respond_to?(:call) || schema.default.nil?
|
|
109
109
|
definition["enum"] = schema.enum if schema.enum.present?
|
|
110
110
|
definition["pattern"] = regex_source(schema.pattern) if schema.pattern.present?
|
|
111
|
+
apply_length(definition, schema.length, definition["type"])
|
|
111
112
|
definition["example"] = schema.example if schema.example.present?
|
|
112
113
|
definition["examples"] = schema.examples if schema.examples.present?
|
|
113
114
|
apply_range(definition, schema.range)
|
|
@@ -124,6 +125,16 @@ module ActionSpec
|
|
|
124
125
|
definition["exclusiveMaximum"] = rules[:lt] if rules.key?(:lt)
|
|
125
126
|
end
|
|
126
127
|
|
|
128
|
+
def apply_length(definition, length, type)
|
|
129
|
+
return if length.blank?
|
|
130
|
+
|
|
131
|
+
rules = length.symbolize_keys
|
|
132
|
+
return unless type == "string"
|
|
133
|
+
|
|
134
|
+
definition["minLength"] = rules[:minimum] if rules.key?(:minimum)
|
|
135
|
+
definition["maxLength"] = rules[:maximum] if rules.key?(:maximum)
|
|
136
|
+
end
|
|
137
|
+
|
|
127
138
|
def scalar_type(type)
|
|
128
139
|
case ActionSpec::Schema::TypeCaster.normalize(type)
|
|
129
140
|
when :string then "string"
|
data/lib/action_spec/railtie.rb
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
module ActionSpec
|
|
2
2
|
class Railtie < ::Rails::Railtie
|
|
3
|
-
initializer "action_spec.i18n" do |app|
|
|
4
|
-
app.config.i18n.load_path += Dir[root.join("config/locales/*.yml")]
|
|
5
|
-
end
|
|
6
|
-
|
|
7
3
|
initializer "action_spec.controller" do
|
|
8
4
|
ActiveSupport.on_load(:action_controller_base) do
|
|
9
5
|
include ActionSpec::Doc
|
|
10
6
|
include ActionSpec::Validator
|
|
11
7
|
end
|
|
12
8
|
end
|
|
9
|
+
|
|
10
|
+
initializer "action_spec.active_record" do
|
|
11
|
+
ActiveSupport.on_load(:active_record) do
|
|
12
|
+
include ActionSpec::Schema::ActiveRecord
|
|
13
|
+
end
|
|
14
|
+
end
|
|
13
15
|
end
|
|
14
16
|
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionSpec
|
|
4
|
+
module Schema
|
|
5
|
+
module ActiveRecord
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def schemas(only: nil)
|
|
10
|
+
names = selected_column_names(only)
|
|
11
|
+
@action_spec_validator_index = build_validator_index
|
|
12
|
+
|
|
13
|
+
names.each_with_object(ActiveSupport::OrderedHash.new) do |name, hash|
|
|
14
|
+
hash[output_name(name)] = schema_definition_for(name)
|
|
15
|
+
end
|
|
16
|
+
ensure
|
|
17
|
+
remove_instance_variable(:@action_spec_validator_index) if instance_variable_defined?(:@action_spec_validator_index)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def selected_column_names(only)
|
|
23
|
+
selected = Array(only).presence&.map { |name| normalize_name(name) } || column_names
|
|
24
|
+
|
|
25
|
+
column_names.select { |name| selected.include?(name) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def output_name(name)
|
|
29
|
+
required_attribute?(name) ? "#{name}!" : name
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def schema_definition_for(name)
|
|
33
|
+
definition = { type: schema_type_for(name) }
|
|
34
|
+
definition[:default] = column_default_for(name) unless column_default_for(name).nil?
|
|
35
|
+
definition[:desc] = column_comment_for(name) if column_comment_for(name).present?
|
|
36
|
+
definition[:enum] = resolved_enum_for(name) if resolved_enum_for(name).present?
|
|
37
|
+
definition[:pattern] = pattern_for(name) if pattern_for(name)
|
|
38
|
+
definition[:range] = range_for(name) if range_for(name).present?
|
|
39
|
+
definition[:length] = length_for(name) if length_for(name).present?
|
|
40
|
+
definition
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def schema_type_for(name)
|
|
44
|
+
return String if enum_values_for(name).present?
|
|
45
|
+
|
|
46
|
+
case columns_hash.fetch(name).type
|
|
47
|
+
when :string, :text, :binary then String
|
|
48
|
+
when :integer, :bigint then Integer
|
|
49
|
+
when :float then Float
|
|
50
|
+
when :decimal then BigDecimal
|
|
51
|
+
when :boolean then :boolean
|
|
52
|
+
when :date then Date
|
|
53
|
+
when :datetime, :timestamp then DateTime
|
|
54
|
+
when :time then Time
|
|
55
|
+
when :json, :jsonb then Object
|
|
56
|
+
else String
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def required_attribute?(name)
|
|
61
|
+
(!column_nullable?(name) && column_default_for(name).nil?) || presence_validated?(name)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def column_nullable?(name)
|
|
65
|
+
columns_hash.fetch(name).null
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def column_default_for(name)
|
|
69
|
+
columns_hash.fetch(name).default
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def column_comment_for(name)
|
|
73
|
+
columns_hash.fetch(name).comment
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def enum_values_for(name)
|
|
77
|
+
defined_enums.fetch(name.to_s, nil)&.keys
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def inclusion_values_for(name)
|
|
81
|
+
validator_for(name, ActiveModel::Validations::InclusionValidator)&.options&.fetch(:in, nil)&.to_a
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def resolved_enum_for(name)
|
|
85
|
+
enum_values_for(name).presence || inclusion_values_for(name).presence
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def pattern_for(name)
|
|
89
|
+
validator_for(name, ActiveModel::Validations::FormatValidator)&.options&.fetch(:with, nil)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def range_for(name)
|
|
93
|
+
options = validator_for(name, ActiveModel::Validations::NumericalityValidator)&.options
|
|
94
|
+
return if options.blank?
|
|
95
|
+
|
|
96
|
+
{}.tap do |range|
|
|
97
|
+
range[:gt] = options[:greater_than] if options.key?(:greater_than)
|
|
98
|
+
range[:ge] = options[:greater_than_or_equal_to] if options.key?(:greater_than_or_equal_to)
|
|
99
|
+
range[:lt] = options[:less_than] if options.key?(:less_than)
|
|
100
|
+
range[:le] = options[:less_than_or_equal_to] if options.key?(:less_than_or_equal_to)
|
|
101
|
+
end.presence
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def length_for(name)
|
|
105
|
+
definition = {}.tap do |length|
|
|
106
|
+
limit = string_limit_for(name)
|
|
107
|
+
length[:maximum] = limit if limit
|
|
108
|
+
|
|
109
|
+
options = validator_for(name, ActiveModel::Validations::LengthValidator)&.options || {}
|
|
110
|
+
length[:minimum] = options[:minimum] if options.key?(:minimum)
|
|
111
|
+
length[:maximum] = options[:maximum] if options.key?(:maximum)
|
|
112
|
+
|
|
113
|
+
if options.key?(:is)
|
|
114
|
+
length[:minimum] = options[:is]
|
|
115
|
+
length[:maximum] = options[:is]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
definition.presence
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def string_limit_for(name)
|
|
123
|
+
column = columns_hash.fetch(name)
|
|
124
|
+
return unless column.type.in?([:string, :text])
|
|
125
|
+
|
|
126
|
+
column.limit
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def presence_validated?(name)
|
|
130
|
+
validator_for(name, ActiveModel::Validations::PresenceValidator).present?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def validator_for(name, klass)
|
|
134
|
+
validator_index.fetch(name.to_s, []).find { |validator| validator.is_a?(klass) }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def normalize_name(name)
|
|
138
|
+
name.to_s.delete_suffix("!")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def build_validator_index
|
|
142
|
+
validators.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |validator, index|
|
|
143
|
+
validator.attributes.each do |attribute|
|
|
144
|
+
index[attribute.to_s] << validator
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def validator_index
|
|
150
|
+
@action_spec_validator_index ||= build_validator_index
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -24,7 +24,7 @@ module ActionSpec
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def copy
|
|
27
|
-
self.class.new(item.copy, default:, enum:, range:, pattern:, allow_nil:, allow_blank:, desc: description, example:, examples:)
|
|
27
|
+
self.class.new(item.copy, default:, enum:, range:, pattern:, length:, allow_nil:, allow_blank:, desc: description, example:, examples:)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module ActionSpec
|
|
4
4
|
module Schema
|
|
5
5
|
class Base
|
|
6
|
-
attr_reader :default, :enum, :range, :pattern, :allow_nil, :allow_blank, :description, :example, :examples
|
|
6
|
+
attr_reader :default, :enum, :range, :pattern, :length, :allow_nil, :allow_blank, :description, :example, :examples
|
|
7
7
|
|
|
8
8
|
def initialize(options = {})
|
|
9
9
|
options = options.symbolize_keys
|
|
@@ -11,6 +11,7 @@ module ActionSpec
|
|
|
11
11
|
@enum = options[:enum]
|
|
12
12
|
@range = options[:range]
|
|
13
13
|
@pattern = options[:pattern]
|
|
14
|
+
@length = options[:length]
|
|
14
15
|
@allow_nil = options[:allow_nil]
|
|
15
16
|
@allow_blank = options[:allow_blank]
|
|
16
17
|
@description = options[:desc] || options[:description]
|
|
@@ -34,7 +34,7 @@ module ActionSpec
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def copy
|
|
37
|
-
self.class.new(fields.transform_values(&:copy), default:, enum:, range:, pattern:, allow_nil:, allow_blank:, desc: description, example:, examples:)
|
|
37
|
+
self.class.new(fields.transform_values(&:copy), default:, enum:, range:, pattern:, length:, allow_nil:, allow_blank:, desc: description, example:, examples:)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
private
|
|
@@ -23,7 +23,7 @@ module ActionSpec
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def copy
|
|
26
|
-
self.class.new(type, default:, enum:, range:, pattern:, allow_nil:, allow_blank:, desc: description, example:, examples:)
|
|
26
|
+
self.class.new(type, default:, enum:, range:, pattern:, length:, allow_nil:, allow_blank:, desc: description, example:, examples:)
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
end
|
data/lib/action_spec/schema.rb
CHANGED
|
@@ -5,13 +5,14 @@ require "action_spec/schema/field"
|
|
|
5
5
|
require "action_spec/schema/scalar"
|
|
6
6
|
require "action_spec/schema/object_of"
|
|
7
7
|
require "action_spec/schema/array_of"
|
|
8
|
+
require "action_spec/schema/active_record"
|
|
8
9
|
require "action_spec/schema/resolver"
|
|
9
10
|
require "action_spec/schema/type_caster"
|
|
10
11
|
|
|
11
12
|
module ActionSpec
|
|
12
13
|
module Schema
|
|
13
14
|
Missing = Object.new.freeze
|
|
14
|
-
OPTION_KEYS = %i[default desc enum range pattern allow_nil allow_blank example examples].freeze
|
|
15
|
+
OPTION_KEYS = %i[default desc enum range pattern length allow_nil allow_blank example examples].freeze
|
|
15
16
|
|
|
16
17
|
class << self
|
|
17
18
|
def build(type = nil, **options)
|
data/lib/action_spec/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: action_spec
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- zhandao
|
|
@@ -43,7 +43,7 @@ dependencies:
|
|
|
43
43
|
- - "<"
|
|
44
44
|
- !ruby/object:Gem::Version
|
|
45
45
|
version: '9.0'
|
|
46
|
-
description:
|
|
46
|
+
description: A concise Rails DSL for declaring API request and response schemas.
|
|
47
47
|
email:
|
|
48
48
|
- a@skipping.cat
|
|
49
49
|
executables: []
|
|
@@ -69,6 +69,7 @@ files:
|
|
|
69
69
|
- lib/action_spec/open_api/schema.rb
|
|
70
70
|
- lib/action_spec/railtie.rb
|
|
71
71
|
- lib/action_spec/schema.rb
|
|
72
|
+
- lib/action_spec/schema/active_record.rb
|
|
72
73
|
- lib/action_spec/schema/array_of.rb
|
|
73
74
|
- lib/action_spec/schema/base.rb
|
|
74
75
|
- lib/action_spec/schema/field.rb
|
|
@@ -86,8 +87,8 @@ licenses:
|
|
|
86
87
|
- MIT
|
|
87
88
|
metadata:
|
|
88
89
|
homepage_uri: https://github.com/action-spec/action_spec
|
|
89
|
-
source_code_uri: https://github.com/action-spec/action_spec
|
|
90
|
-
changelog_uri: https://github.com/action-spec/action_spec/
|
|
90
|
+
source_code_uri: https://github.com/action-spec/action_spec/tree/main
|
|
91
|
+
changelog_uri: https://github.com/action-spec/action_spec/releases
|
|
91
92
|
rdoc_options: []
|
|
92
93
|
require_paths:
|
|
93
94
|
- lib
|