action_spec 0.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +480 -0
- data/Rakefile +3 -0
- data/config/locales/en.yml +6 -0
- data/config/locales/zh.yml +6 -0
- data/lib/action_spec/configuration.rb +38 -0
- data/lib/action_spec/doc/dsl.rb +109 -0
- data/lib/action_spec/doc/endpoint.rb +140 -0
- data/lib/action_spec/doc.rb +68 -0
- data/lib/action_spec/header_hash.rb +11 -0
- data/lib/action_spec/invalid_parameters.rb +14 -0
- data/lib/action_spec/railtie.rb +14 -0
- data/lib/action_spec/schema/array_of.rb +31 -0
- data/lib/action_spec/schema/base.rb +67 -0
- data/lib/action_spec/schema/field.rb +27 -0
- data/lib/action_spec/schema/object_of.rb +52 -0
- data/lib/action_spec/schema/resolver.rb +56 -0
- data/lib/action_spec/schema/scalar.rb +30 -0
- data/lib/action_spec/schema/type_caster.rb +115 -0
- data/lib/action_spec/schema.rb +72 -0
- data/lib/action_spec/validation_result.rb +71 -0
- data/lib/action_spec/validator/runner.rb +79 -0
- data/lib/action_spec/validator.rb +46 -0
- data/lib/action_spec/version.rb +3 -0
- data/lib/action_spec.rb +24 -0
- data/lib/tasks/action_spec_tasks.rake +4 -0
- metadata +103 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_spec/schema/base"
|
|
4
|
+
require "action_spec/schema/field"
|
|
5
|
+
require "action_spec/schema/scalar"
|
|
6
|
+
require "action_spec/schema/object_of"
|
|
7
|
+
require "action_spec/schema/array_of"
|
|
8
|
+
require "action_spec/schema/resolver"
|
|
9
|
+
require "action_spec/schema/type_caster"
|
|
10
|
+
|
|
11
|
+
module ActionSpec
|
|
12
|
+
module Schema
|
|
13
|
+
Missing = Object.new.freeze
|
|
14
|
+
OPTION_KEYS = %i[default desc enum range pattern allow_nil allow_blank example examples].freeze
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def build(type = nil, **options)
|
|
18
|
+
definition = options.symbolize_keys
|
|
19
|
+
definition[:type] = type if type
|
|
20
|
+
from_definition(definition)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def from_definition(definition)
|
|
24
|
+
return Scalar.new(String) if definition.blank?
|
|
25
|
+
return ArrayOf.new(from_definition(type: definition.first)) if definition.is_a?(Array) && definition.one?
|
|
26
|
+
return ArrayOf.new(from_definition(type: nil)) if definition == []
|
|
27
|
+
return Scalar.new(definition) unless definition.is_a?(Hash)
|
|
28
|
+
|
|
29
|
+
definition = definition.deep_symbolize_keys
|
|
30
|
+
if definition.key?(:type)
|
|
31
|
+
type = definition[:type]
|
|
32
|
+
options = definition.except(:type)
|
|
33
|
+
return ArrayOf.new(from_definition(type: type.first), options) if type.is_a?(Array) && type.one?
|
|
34
|
+
return ObjectOf.new(build_fields(definition.except(:type, *OPTION_KEYS))) if type == Object && definition.except(:type, *OPTION_KEYS).present?
|
|
35
|
+
|
|
36
|
+
return Scalar.new(type, options)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
ObjectOf.new(build_fields(definition))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def build_fields(definition_hash)
|
|
43
|
+
definition_hash.each_with_object(ActiveSupport::OrderedHash.new) do |(name, definition), fields|
|
|
44
|
+
schema = build_field_schema(definition)
|
|
45
|
+
fields[field_name(name)] = Field.new(
|
|
46
|
+
name: field_name(name),
|
|
47
|
+
required: required_key?(name),
|
|
48
|
+
schema:
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def field_name(name)
|
|
54
|
+
name.to_s.delete_suffix("!").to_sym
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def required_key?(name)
|
|
58
|
+
name.to_s.end_with?("!")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def build_field_schema(definition)
|
|
62
|
+
return from_definition(type: definition) unless definition.is_a?(Hash)
|
|
63
|
+
|
|
64
|
+
definition = definition.symbolize_keys
|
|
65
|
+
return from_definition(definition) if definition.key?(:type)
|
|
66
|
+
return from_definition(definition) if (definition.keys - OPTION_KEYS).present?
|
|
67
|
+
|
|
68
|
+
from_definition(definition.merge(type: String))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionSpec
|
|
4
|
+
class ValidationResult
|
|
5
|
+
extend ActiveModel::Naming
|
|
6
|
+
extend ActiveModel::Translation
|
|
7
|
+
|
|
8
|
+
attr_reader :errors, :px
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@errors = ActiveModel::Errors.new(self)
|
|
12
|
+
@px = ActiveSupport::HashWithIndifferentAccess.new(
|
|
13
|
+
path: ActiveSupport::HashWithIndifferentAccess.new,
|
|
14
|
+
query: ActiveSupport::HashWithIndifferentAccess.new,
|
|
15
|
+
body: ActiveSupport::HashWithIndifferentAccess.new,
|
|
16
|
+
headers: HeaderHash.new,
|
|
17
|
+
cookies: ActiveSupport::HashWithIndifferentAccess.new
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def invalid?
|
|
22
|
+
errors.any?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def assign(location, key, value)
|
|
26
|
+
bucket(location)[key] = value
|
|
27
|
+
px[key] = value if root_bucket?(location)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def add_error(attribute, type, **options)
|
|
31
|
+
if (message = ActionSpec.config.message_for(attribute, type, options))
|
|
32
|
+
errors.add(attribute, message)
|
|
33
|
+
else
|
|
34
|
+
errors.add(attribute, type, **options)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def read_attribute_for_validation(_attribute)
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.lookup_ancestors
|
|
43
|
+
[self]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.model_name
|
|
47
|
+
@model_name ||= ActiveModel::Name.new(self, nil, "ActionSpec::Parameters")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.human_attribute_name(attribute, options = {})
|
|
51
|
+
key = attribute.to_s
|
|
52
|
+
defaults = [
|
|
53
|
+
:"activemodel.attributes.#{model_name.i18n_key}.#{key}",
|
|
54
|
+
:"activemodel.attributes.#{model_name.i18n_key}.#{key.tr('.', '_')}",
|
|
55
|
+
key.tr(".", " ").humanize
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
I18n.translate(defaults.shift, **options, default: defaults)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def bucket(location)
|
|
64
|
+
px.fetch(location)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def root_bucket?(location)
|
|
68
|
+
%i[path query body].include?(location)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionSpec
|
|
4
|
+
module Validator
|
|
5
|
+
class Runner
|
|
6
|
+
def initialize(endpoint:, controller:, coerce:)
|
|
7
|
+
@endpoint = endpoint
|
|
8
|
+
@controller = controller
|
|
9
|
+
@coerce = coerce
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
result = ValidationResult.new
|
|
14
|
+
merge_group!(result, endpoint.request.path, source: path_source, location: :path)
|
|
15
|
+
merge_group!(result, endpoint.request.query, source: params_source, location: :query)
|
|
16
|
+
merge_group!(result, endpoint.request.body, source: params_source, location: :body)
|
|
17
|
+
merge_group!(result, endpoint.request.header, source: header_source, location: :headers)
|
|
18
|
+
merge_group!(result, endpoint.request.cookie, source: cookie_source, location: :cookies)
|
|
19
|
+
result
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :endpoint, :controller, :coerce
|
|
25
|
+
|
|
26
|
+
def merge_group!(result, group, source:, location:)
|
|
27
|
+
group.fields.each do |field|
|
|
28
|
+
value = resolve_field(field, result:, source:, location:)
|
|
29
|
+
next if value.equal?(ActionSpec::Schema::Missing)
|
|
30
|
+
|
|
31
|
+
result.assign(location, storage_key(field, location), value)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def resolve_field(field, result:, source:, location:)
|
|
36
|
+
raw_source = location == :headers ? normalize_headers(source) : source
|
|
37
|
+
field_source = raw_source.is_a?(Hash) ? raw_source.with_indifferent_access : raw_source
|
|
38
|
+
ActionSpec::Schema::Resolver.new(
|
|
39
|
+
field:,
|
|
40
|
+
source: field_source,
|
|
41
|
+
context: controller,
|
|
42
|
+
coerce:,
|
|
43
|
+
result:,
|
|
44
|
+
path: []
|
|
45
|
+
).resolve
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def params_source
|
|
49
|
+
controller.params.to_unsafe_h
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def path_source
|
|
53
|
+
controller.request.path_parameters.except(:controller, :action)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def header_source
|
|
57
|
+
controller.request.headers.to_h
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def cookie_source
|
|
61
|
+
return {} unless controller.respond_to?(:cookies, true)
|
|
62
|
+
|
|
63
|
+
controller.send(:cookies).to_hash
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def normalize_headers(headers)
|
|
67
|
+
headers.each_with_object(HeaderHash.new) do |(key, value), normalized|
|
|
68
|
+
normalized[key] = value
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def storage_key(field, location)
|
|
73
|
+
return field.name.to_s.tr("_", "-").downcase if location == :headers
|
|
74
|
+
|
|
75
|
+
field.name
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_spec/validator/runner"
|
|
4
|
+
|
|
5
|
+
module ActionSpec
|
|
6
|
+
module Validator
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
rescue_from ActionSpec::InvalidParameters, with: :render_invalid_parameters if ActionSpec.config.rescue_invalid_parameters
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def px
|
|
14
|
+
@px ||= ActiveSupport::HashWithIndifferentAccess.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def validate_params!
|
|
18
|
+
@px = validate_with(coerce: false)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def validate_and_coerce_params!
|
|
22
|
+
@px = validate_with(coerce: true)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def validate_with(coerce:)
|
|
28
|
+
endpoint = self.class.respond_to?(:action_spec_for) ? self.class.action_spec_for(action_name) : nil
|
|
29
|
+
return ActiveSupport::HashWithIndifferentAccess.new unless endpoint
|
|
30
|
+
|
|
31
|
+
result = Runner.new(endpoint:, controller: self, coerce:).call
|
|
32
|
+
raise ActionSpec.config.invalid_parameters_exception_class.new(result) if result.invalid?
|
|
33
|
+
|
|
34
|
+
result.px
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def render_invalid_parameters(error)
|
|
38
|
+
if (renderer = ActionSpec.config.invalid_parameters_renderer)
|
|
39
|
+
return renderer.arity == 2 ? renderer.call(self, error) : instance_exec(error, &renderer)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
render json: { errors: error.errors.to_hash(full_messages: true) },
|
|
43
|
+
status: ActionSpec.config.invalid_parameters_status
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/action_spec.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require "action_spec/version"
|
|
2
|
+
require "active_support"
|
|
3
|
+
require "active_support/core_ext"
|
|
4
|
+
require "active_model"
|
|
5
|
+
require "action_spec/configuration"
|
|
6
|
+
require "action_spec/header_hash"
|
|
7
|
+
require "action_spec/validation_result"
|
|
8
|
+
require "action_spec/invalid_parameters"
|
|
9
|
+
require "action_spec/doc"
|
|
10
|
+
require "action_spec/schema"
|
|
11
|
+
require "action_spec/validator"
|
|
12
|
+
require "action_spec/railtie"
|
|
13
|
+
|
|
14
|
+
module ActionSpec
|
|
15
|
+
class << self
|
|
16
|
+
def config
|
|
17
|
+
@config ||= Configuration.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def configure
|
|
21
|
+
yield config
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: action_spec
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- zhandao
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 7.0.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 7.0.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rspec-rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.0'
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '9.0'
|
|
36
|
+
type: :development
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '7.0'
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '9.0'
|
|
46
|
+
description: Concise and Powerful API Documentation Solution for Rails.
|
|
47
|
+
email:
|
|
48
|
+
- a@skipping.cat
|
|
49
|
+
executables: []
|
|
50
|
+
extensions: []
|
|
51
|
+
extra_rdoc_files: []
|
|
52
|
+
files:
|
|
53
|
+
- MIT-LICENSE
|
|
54
|
+
- README.md
|
|
55
|
+
- Rakefile
|
|
56
|
+
- config/locales/en.yml
|
|
57
|
+
- config/locales/zh.yml
|
|
58
|
+
- lib/action_spec.rb
|
|
59
|
+
- lib/action_spec/configuration.rb
|
|
60
|
+
- lib/action_spec/doc.rb
|
|
61
|
+
- lib/action_spec/doc/dsl.rb
|
|
62
|
+
- lib/action_spec/doc/endpoint.rb
|
|
63
|
+
- lib/action_spec/header_hash.rb
|
|
64
|
+
- lib/action_spec/invalid_parameters.rb
|
|
65
|
+
- lib/action_spec/railtie.rb
|
|
66
|
+
- lib/action_spec/schema.rb
|
|
67
|
+
- lib/action_spec/schema/array_of.rb
|
|
68
|
+
- lib/action_spec/schema/base.rb
|
|
69
|
+
- lib/action_spec/schema/field.rb
|
|
70
|
+
- lib/action_spec/schema/object_of.rb
|
|
71
|
+
- lib/action_spec/schema/resolver.rb
|
|
72
|
+
- lib/action_spec/schema/scalar.rb
|
|
73
|
+
- lib/action_spec/schema/type_caster.rb
|
|
74
|
+
- lib/action_spec/validation_result.rb
|
|
75
|
+
- lib/action_spec/validator.rb
|
|
76
|
+
- lib/action_spec/validator/runner.rb
|
|
77
|
+
- lib/action_spec/version.rb
|
|
78
|
+
- lib/tasks/action_spec_tasks.rake
|
|
79
|
+
homepage: https://github.com/action-spec/action_spec
|
|
80
|
+
licenses:
|
|
81
|
+
- MIT
|
|
82
|
+
metadata:
|
|
83
|
+
homepage_uri: https://github.com/action-spec/action_spec
|
|
84
|
+
source_code_uri: https://github.com/action-spec/action_spec
|
|
85
|
+
changelog_uri: https://github.com/action-spec/action_spec/CHANils.
|
|
86
|
+
rdoc_options: []
|
|
87
|
+
require_paths:
|
|
88
|
+
- lib
|
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: 3.1.0
|
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
- - ">="
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: '0'
|
|
99
|
+
requirements: []
|
|
100
|
+
rubygems_version: 4.0.3
|
|
101
|
+
specification_version: 4
|
|
102
|
+
summary: Concise and Powerful API Documentation Solution for Rails.
|
|
103
|
+
test_files: []
|