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.
@@ -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
@@ -0,0 +1,3 @@
1
+ module ActionSpec
2
+ VERSION = "0.1.0"
3
+ end
@@ -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
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :action_spec do
3
+ # # Task goes here
4
+ # 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: []