zero-params_processor 0.4.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8ec687b8215056d4e4197b1c0a2e11425e00460d84c6e204b93f756f4ece1b4e
4
+ data.tar.gz: a494c7786f8390b2f254c6b9bffffe6d303447aa859d390c1a25beb18052dc20
5
+ SHA512:
6
+ metadata.gz: a0e1025443eee3ba5a337dd7107c7621c716679cd790f7944ac25f1d6f3ad8ce1979debd97d4cae212bbbc61aac6e42f6e7b2e4cc86844364e66565d93b126d6
7
+ data.tar.gz: c50e9d5f36bccb010b1b23eea0557523df285a40fbcb9bd24accc2057fe67efa3308e924d480cba011979c4e57f36fccd8aef842076927222402728892f6e4b8
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ .idea/*
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,91 @@
1
+ # This is the configuration used to check the rubocop source code.
2
+
3
+ #require: rbocop/cop/internal_affairs
4
+
5
+ AllCops:
6
+ # Include common Ruby source files.
7
+ Exclude:
8
+ - 'spec/**/*'
9
+ - 'zero-params_processor.gemspec'
10
+ - 'Gemfile'
11
+ - 'bin/**/*'
12
+ DisplayCopNames: true
13
+ TargetRubyVersion: 2.4
14
+ TargetRailsVersion: 5.1
15
+
16
+ Style/FrozenStringLiteralComment:
17
+ Enabled: false
18
+
19
+ Metrics/LineLength:
20
+ Max: 1200
21
+
22
+ Metrics/ClassLength:
23
+ Max: 1200
24
+
25
+ Metrics/MethodLength:
26
+ Max: 1200
27
+
28
+ Metrics/ModuleLength:
29
+ Max: 1200
30
+
31
+ Metrics/BlockLength:
32
+ Max: 1200
33
+
34
+ Style/Documentation:
35
+ Enabled: false
36
+
37
+ Layout/SpaceInsideBrackets:
38
+ Enabled: false
39
+
40
+ Layout/SpaceInsideHashLiteralBraces:
41
+ Enabled: false
42
+
43
+ Layout/SpaceInsidePercentLiteralDelimiters:
44
+ Enabled: false
45
+
46
+ Style/AsciiComments:
47
+ Enabled: false
48
+
49
+ Layout/IndentHash:
50
+ Enabled: false
51
+
52
+ Layout/AlignHash:
53
+ Enabled: false
54
+
55
+ Style/HashSyntax:
56
+ Enabled: false
57
+
58
+ Style/ClassAndModuleChildren:
59
+ Enabled: false
60
+
61
+ Layout/EmptyLines:
62
+ Enabled: false
63
+
64
+ Lint/UnneededSplatExpansion:
65
+ Enabled: false
66
+
67
+ #Style/StringLiterals:
68
+ # Enabled: true
69
+ # EnforcedStyle: double_quotes
70
+
71
+
72
+
73
+
74
+ Style/UnneededPercentQ:
75
+ Enabled: false
76
+
77
+ Style/RedundantSelf:
78
+ Enabled: false
79
+
80
+ Metrics/CyclomaticComplexity:
81
+ Enabled: false
82
+
83
+ Metrics/AbcSize:
84
+ Enabled: false
85
+
86
+ Metrics/PerceivedComplexity:
87
+ Enabled: false
88
+
89
+ # False positives
90
+ Layout/EmptyLineBetweenDefs:
91
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,16 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.5.1
5
+ before_install: gem install bundler
6
+ env:
7
+ global:
8
+ - CC_TEST_REPORTER_ID=5f2477af6b38bd4db35abe49930cbae98e867cb3163b2da1a465176b2f0a361c
9
+ before_script:
10
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
11
+ - chmod +x ./cc-test-reporter
12
+ - ./cc-test-reporter before-build
13
+ script:
14
+ - bundle exec rspec
15
+ after_script:
16
+ - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in zero-params_processor.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,157 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ zero-params_processor (0.4.0)
5
+ activesupport (>= 3)
6
+ multi_json
7
+ rails (>= 3)
8
+ zero-rails_openapi (>= 1.5.2)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ actioncable (5.2.1)
14
+ actionpack (= 5.2.1)
15
+ nio4r (~> 2.0)
16
+ websocket-driver (>= 0.6.1)
17
+ actionmailer (5.2.1)
18
+ actionpack (= 5.2.1)
19
+ actionview (= 5.2.1)
20
+ activejob (= 5.2.1)
21
+ mail (~> 2.5, >= 2.5.4)
22
+ rails-dom-testing (~> 2.0)
23
+ actionpack (5.2.1)
24
+ actionview (= 5.2.1)
25
+ activesupport (= 5.2.1)
26
+ rack (~> 2.0)
27
+ rack-test (>= 0.6.3)
28
+ rails-dom-testing (~> 2.0)
29
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
30
+ actionview (5.2.1)
31
+ activesupport (= 5.2.1)
32
+ builder (~> 3.1)
33
+ erubi (~> 1.4)
34
+ rails-dom-testing (~> 2.0)
35
+ rails-html-sanitizer (~> 1.0, >= 1.0.3)
36
+ activejob (5.2.1)
37
+ activesupport (= 5.2.1)
38
+ globalid (>= 0.3.6)
39
+ activemodel (5.2.1)
40
+ activesupport (= 5.2.1)
41
+ activerecord (5.2.1)
42
+ activemodel (= 5.2.1)
43
+ activesupport (= 5.2.1)
44
+ arel (>= 9.0)
45
+ activestorage (5.2.1)
46
+ actionpack (= 5.2.1)
47
+ activerecord (= 5.2.1)
48
+ marcel (~> 0.3.1)
49
+ activesupport (5.2.1)
50
+ concurrent-ruby (~> 1.0, >= 1.0.2)
51
+ i18n (>= 0.7, < 2)
52
+ minitest (~> 5.1)
53
+ tzinfo (~> 1.1)
54
+ arel (9.0.0)
55
+ builder (3.2.3)
56
+ concurrent-ruby (1.0.5)
57
+ crass (1.0.4)
58
+ diff-lcs (1.3)
59
+ docile (1.1.5)
60
+ erubi (1.7.1)
61
+ globalid (0.4.1)
62
+ activesupport (>= 4.2.0)
63
+ i18n (1.1.0)
64
+ concurrent-ruby (~> 1.0)
65
+ json (2.1.0)
66
+ loofah (2.2.2)
67
+ crass (~> 1.0.2)
68
+ nokogiri (>= 1.5.9)
69
+ mail (2.7.0)
70
+ mini_mime (>= 0.1.1)
71
+ marcel (0.3.2)
72
+ mimemagic (~> 0.3.2)
73
+ method_source (0.9.0)
74
+ mimemagic (0.3.2)
75
+ mini_mime (1.0.1)
76
+ mini_portile2 (2.3.0)
77
+ minitest (5.11.3)
78
+ multi_json (1.13.1)
79
+ nio4r (2.3.1)
80
+ nokogiri (1.8.4)
81
+ mini_portile2 (~> 2.3.0)
82
+ rack (2.0.5)
83
+ rack-test (1.1.0)
84
+ rack (>= 1.0, < 3)
85
+ rails (5.2.1)
86
+ actioncable (= 5.2.1)
87
+ actionmailer (= 5.2.1)
88
+ actionpack (= 5.2.1)
89
+ actionview (= 5.2.1)
90
+ activejob (= 5.2.1)
91
+ activemodel (= 5.2.1)
92
+ activerecord (= 5.2.1)
93
+ activestorage (= 5.2.1)
94
+ activesupport (= 5.2.1)
95
+ bundler (>= 1.3.0)
96
+ railties (= 5.2.1)
97
+ sprockets-rails (>= 2.0.0)
98
+ rails-dom-testing (2.0.3)
99
+ activesupport (>= 4.2.0)
100
+ nokogiri (>= 1.6)
101
+ rails-html-sanitizer (1.0.4)
102
+ loofah (~> 2.2, >= 2.2.2)
103
+ railties (5.2.1)
104
+ actionpack (= 5.2.1)
105
+ activesupport (= 5.2.1)
106
+ method_source
107
+ rake (>= 0.8.7)
108
+ thor (>= 0.19.0, < 2.0)
109
+ rake (10.5.0)
110
+ rspec (3.6.0)
111
+ rspec-core (~> 3.6.0)
112
+ rspec-expectations (~> 3.6.0)
113
+ rspec-mocks (~> 3.6.0)
114
+ rspec-core (3.6.0)
115
+ rspec-support (~> 3.6.0)
116
+ rspec-expectations (3.6.0)
117
+ diff-lcs (>= 1.2.0, < 2.0)
118
+ rspec-support (~> 3.6.0)
119
+ rspec-mocks (3.6.0)
120
+ diff-lcs (>= 1.2.0, < 2.0)
121
+ rspec-support (~> 3.6.0)
122
+ rspec-support (3.6.0)
123
+ simplecov (0.15.1)
124
+ docile (~> 1.1.0)
125
+ json (>= 1.8, < 3)
126
+ simplecov-html (~> 0.10.0)
127
+ simplecov-html (0.10.2)
128
+ sprockets (3.7.2)
129
+ concurrent-ruby (~> 1.0)
130
+ rack (> 1, < 3)
131
+ sprockets-rails (3.2.1)
132
+ actionpack (>= 4.0)
133
+ activesupport (>= 4.0)
134
+ sprockets (>= 3.0.0)
135
+ thor (0.20.0)
136
+ thread_safe (0.3.6)
137
+ tzinfo (1.2.5)
138
+ thread_safe (~> 0.1)
139
+ websocket-driver (0.7.0)
140
+ websocket-extensions (>= 0.1.0)
141
+ websocket-extensions (0.1.3)
142
+ zero-rails_openapi (1.5.2)
143
+ activesupport (>= 3)
144
+ rails (>= 3)
145
+
146
+ PLATFORMS
147
+ ruby
148
+
149
+ DEPENDENCIES
150
+ bundler (~> 1.16.a)
151
+ rake (~> 10.0)
152
+ rspec (~> 3.0)
153
+ simplecov
154
+ zero-params_processor!
155
+
156
+ BUNDLED WITH
157
+ 1.16.3
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 zhandao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # Zero::ParamsProcessor
2
+
3
+ [![Build Status](https://travis-ci.org/zhandao/zero-params_processor.svg?branch=test)](https://travis-ci.org/zhandao/zero-params_processor)
4
+ [![Maintainability](https://api.codeclimate.com/v1/badges/4d2fd3c04abf75a1158b/maintainability)](https://codeclimate.com/github/zhandao/zero-params_processor/maintainability)
5
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/4d2fd3c04abf75a1158b/test_coverage)](https://codeclimate.com/github/zhandao/zero-params_processor/test_coverage)
6
+
7
+ ```ruby
8
+ # declare aop callback which provided by zpp
9
+ before_action :process_params!
10
+
11
+ # if you defined following spec in your api doc
12
+ # `query!` bang method means it's a required param
13
+ query! :time, Date, gt: '2018/1/1'.to_date, permit: true
14
+
15
+ # THEN in your controller action, you will get:
16
+ # 1. param validate: require, Date type and range
17
+ # 2. value convert: JSON has not Date type, you must
18
+ # do a convert from String, but it can do it for you:
19
+ params[:time] = params[:time].to_date # after some format checkers
20
+ # 3. set instance variable: allows you get the param by `@time`
21
+ # instead of `params[:time]`
22
+ # 4. permitted: if you defined a lot of params with `permit: true`,
23
+ # you will be allowed to get them by calling `permitted`, like:
24
+ Book.create(permitted)
25
+ ```
26
+
27
+ ## ONLY FOR the RAILS app that using Zero-Rails_OpenApi, like [Zero-Rails](https://github.com/zhandao/zero-rails)
28
+
29
+ ## Installation
30
+
31
+ Add this line to your application's Gemfile:
32
+
33
+ ```ruby
34
+ gem 'zero-params_processor'#, github: 'zhandao/zero-params_processor'
35
+ ```
36
+
37
+ And then execute:
38
+
39
+ $ bundle
40
+
41
+ ## Usage
42
+
43
+ ```ruby
44
+ before_action { process_params_by :validate!, :convert }
45
+ before_action :process_params! # all actions will be called
46
+ ```
47
+ Action options: %i[ validate! convert set_instance_var set_permitted ]
48
+
49
+ ### validate!
50
+
51
+ Check each input parameter based on `OpenApi.docs` (Zero-Rails_OpenApi's cattr), it will
52
+ raise `ParamsProcessor::ValidationFailed < StandardError` if check failed.
53
+
54
+ Note: If it did not find the corresponding open-api information in `OpenApi.docs`,
55
+ the check will be skipped.
56
+
57
+ ### convert
58
+
59
+ Convert each input parameter to the specified type base on `OpenApi.docs`. For example:
60
+
61
+ We declare the parameter like this:
62
+ ```ruby
63
+ query :price, Integer
64
+ query :like, Boolean
65
+ query :time, Date
66
+ ```
67
+ In case params[:price] == `'1'`, the Converter will make it to be `1` (a Integer).
68
+ In case params[:like] == `0`, the Converter will make it to be `false`.
69
+ In case params[:time] == `'2018/1/1'`, the Converter will make it to be `Date.new(2018, 1, 1)`.
70
+
71
+ ### set_instance_var
72
+
73
+ Let (converted) input parameters to be instance variables of the current controller.
74
+
75
+ After that, you can access the params value like: `@price`, `@like`, `@time`.
76
+
77
+ ### set_permitted
78
+
79
+ Permit the parameters that having `permit: true` attribute in `OpenApi.docs`. Like this:
80
+
81
+ ```ruby
82
+ query :price, Integer, pmt: true
83
+ ```
84
+
85
+ Then, the :price parameter will be permitted.
86
+
87
+ After this action, you can access all the permitted input via method `permitted`. Like:
88
+
89
+ ```ruby
90
+ Book.create(permitted)
91
+ ```
92
+
93
+ Note: `not_permit: true` attribute will lead to a slightly different behavior: All the
94
+ parameter that are not having `not_permit: true` will be permitted. For example:
95
+
96
+ ```ruby
97
+ query :price, Integer
98
+ query :like, Boolean, npmt: true
99
+ query :time, Date
100
+ ```
101
+
102
+ Then, the :price and :time will be permitted.
103
+
104
+ ## Development
105
+
106
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
107
+
108
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
109
+
110
+ ## Contributing
111
+
112
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/zero-params_processor. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
113
+
114
+ ## License
115
+
116
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
117
+
118
+ ## Code of Conduct
119
+
120
+ Everyone interacting in the Zero::ParamsProcessor project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/zero-params_processor/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'zero/params_processor'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,90 @@
1
+ require 'params_processor/version'
2
+ require 'params_processor/validate'
3
+ require 'params_processor/type_convert'
4
+
5
+ module ParamsProcessor
6
+ private
7
+
8
+ def process_params_by(*actions)
9
+ return if params_doc.blank?
10
+ pdocs = params_doc.map do |doc|
11
+ param_doc = ParamDoc.new(doc) # TODO: performance
12
+ _validate_param!(param_doc) if actions.index(:validate!)
13
+ _convert_param(param_doc) if actions.index(:convert)
14
+ _set_instance_var(param_doc) if actions.index(:set_instance_var)
15
+ param_doc
16
+ end
17
+ _set_permitted(pdocs) if actions.index(:set_permitted)
18
+ end
19
+
20
+ def process_params!
21
+ actions = Config.actions || %i[ validate! convert set_instance_var set_permitted ]
22
+ process_params_by *actions
23
+ end
24
+
25
+ def _validate_param!(param_doc)
26
+ input = param_doc.in == 'header' ? request.headers[param_doc.name.to_s] : params[param_doc.name.to_sym]
27
+ error_class = "#{controller_name.camelize}Error".constantize rescue nil
28
+ Validate.(input, based_on: param_doc, raise: error_class)
29
+ end
30
+
31
+ def _convert_param(param_doc)
32
+ name = param_doc.name.to_sym
33
+ params[name] = param_doc.dft if params[name].nil? && !param_doc.dft.nil?
34
+ return if params[name].nil?
35
+
36
+ params[name] = TypeConvert.(params[name], based_on: param_doc)
37
+ # mapping param key
38
+ params[param_doc.as] = params.delete(name) if param_doc.as.present?
39
+ end
40
+
41
+ def _set_instance_var(param_doc)
42
+ key = param_doc.real_name
43
+ return if (value = params[key]).nil?
44
+
45
+ instance_variable_set("@#{key}", value)
46
+ _permit_hash_and_array(value) if param_doc.permit?
47
+ _auto_find_by_id(key, value) if @route_path.match?(/\{#{key}\}/) # e.g. "/examples/{id}"
48
+ end
49
+
50
+ # If params[:data] == [{..}, {..}], each `{..}` in the array
51
+ # is an instance of ActionController::Parameters.
52
+ # So, if :data is allowed to permit, it's values should also permit.
53
+ def _permit_hash_and_array(value)
54
+ value.map!(&:permit!) if value.is_a?(Array) && value.first.is_a?(ActionController::Parameters)
55
+ end
56
+
57
+ def _auto_find_by_id(key, value)
58
+ whos_id = (key.to_sym == :id ? controller_name : key.to_s.sub('_id', '')).singularize
59
+ model = whos_id.camelize.constantize rescue return
60
+ model_instance = model.find_by(id: value) || self.class.error_cls.not_found! # TODO HACK
61
+ instance_variable_set("@#{whos_id}", model_instance)
62
+ end
63
+
64
+ def _set_permitted(params_docs)
65
+ exist_not_permit = params_docs.map(&:not_permit?).any?(&:present?)
66
+ keys = params_docs.map { |p| p.doced_permit? ? p.real_name : nil }.compact
67
+ keys = exist_not_permit ? params_docs.map(&:real_name) - keys : keys
68
+ @permitted = params.permit(*keys)
69
+ # @permitted = params.slice(*keys).to_unsafe_h
70
+ end
71
+
72
+ def permitted; @permitted end
73
+
74
+ # TODO: performance
75
+ def params_doc
76
+ current_api = OpenApi.routes_index[self.class.controller_path]
77
+ return [ ] unless current_api
78
+
79
+ DocConverter.docs ||= DocConverter.new(OpenApi.docs)
80
+ @route_path = OpenApi::Generator.find_path_httpverb_by(self.class.controller_path, action_name).first
81
+ path_doc = DocConverter.docs[current_api][:paths][@route_path]
82
+ # nil check is for skipping this before_action when the action is not doced.
83
+ path_doc&.[](request.method.downcase)&.[](:parameters) || [ ]
84
+ end
85
+
86
+
87
+ class ValidationFailed < StandardError
88
+ def info; { code: 400, msg: "#{Config.prefix}".concat(message), http_status: :bad_request }; end
89
+ end
90
+ end
@@ -0,0 +1,61 @@
1
+ require 'active_support/all'
2
+
3
+ module ParamsProcessor
4
+ module Config
5
+ cattr_accessor :actions do
6
+ nil
7
+ end
8
+
9
+ cattr_accessor :strict_check do
10
+ false
11
+ end
12
+
13
+ cattr_accessor :prefix do
14
+ 'parameter'
15
+ end
16
+
17
+ cattr_accessor :production_msg do
18
+ # 'validation failed'
19
+ end
20
+
21
+ cattr_accessor :not_passed do
22
+ 'is required'
23
+ end
24
+
25
+ cattr_accessor :is_blank do
26
+ 'should not be blank'
27
+ end
28
+
29
+ cattr_accessor :wrong_type do
30
+ 'must be'
31
+ end
32
+
33
+ cattr_accessor :wrong_size do
34
+ 'length must be(in)'
35
+ end
36
+
37
+ cattr_accessor :is_not_entity do
38
+ 'must be'
39
+ end
40
+
41
+ cattr_accessor :not_in_allowable_values do
42
+ 'must in'
43
+ end
44
+
45
+ cattr_accessor :not_match_pattern do
46
+ 'must match'
47
+ end
48
+
49
+ cattr_accessor :out_of_range do
50
+ 'is out of range'
51
+ end
52
+
53
+ cattr_accessor :wrong_combined_type do
54
+ 'must be'
55
+ end
56
+
57
+ cattr_accessor :test do
58
+ false
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,76 @@
1
+ require 'active_support/hash_with_indifferent_access'
2
+
3
+ module ParamsProcessor
4
+ class DocConverter < HashWithIndifferentAccess
5
+ cattr_accessor :docs
6
+
7
+ def initialize(inhert_hash = { })
8
+ super(inhert_hash)
9
+ convert
10
+ end
11
+
12
+ def fill_with_ref(who, ref_to)
13
+ return unless who.key? '$ref'
14
+ ref_name = who.delete('$ref').split('/').last
15
+ who.merge! @api_components.fetch(ref_to).fetch(ref_name)
16
+ end
17
+
18
+ # TODO: refactor
19
+ def convert
20
+ return if blank?
21
+ self.each do |_api, api_doc|
22
+ @api_components = api_doc[:components]
23
+ api_doc[:paths].each do |_path, path_doc|
24
+ path_doc.each do |_method, action_doc|
25
+ # 将 Reference Obj 填充进来
26
+ # body ref
27
+ request_body = action_doc[:requestBody]
28
+ fill_with_ref request_body, :requestBodies if request_body.present?
29
+ request_body&.[](:content)&.each do |_media, media_doc|
30
+ media_doc.each do |mtype, mtype_doc|
31
+ fill_with_ref mtype_doc, :schemas if mtype == 'schema'
32
+ end
33
+ end
34
+
35
+ # 将 form-data 提到 parameters,方便统一访问接口
36
+ # 在 param ref 处理之前上提,使后续可以一并将 properties 中的 schma 进行处理
37
+ form = action_doc[:requestBody]&.[](:content)&.[]('multipart/form-data')
38
+ if form.present?
39
+ required = form[:schema][:required] || [ ]
40
+ permit = form[:schema][:permit] ? true : false
41
+ form[:schema][:properties]&.each do |name, prop_schema|
42
+ (action_doc[:parameters] ||= [ ]) << {
43
+ 'name' => name,
44
+ 'in' => 'form',
45
+ 'required' => required.include?(name),
46
+ 'schema' => prop_schema.reverse_merge!(permit: permit),
47
+ }
48
+ end
49
+ end
50
+
51
+
52
+ # param ref
53
+ action_doc[:parameters]&.each do |param|
54
+ fill_with_ref param, :parameters
55
+
56
+ # schema ref
57
+ # TODO: Support nested scanning
58
+ fill_with_ref param[:schema], :schemas if param[:schema].present?
59
+ end
60
+
61
+
62
+ # response ref
63
+ action_doc[:responses]&.each do |_resp, resp_doc|
64
+ fill_with_ref resp_doc, :responses
65
+ resp_doc&.[](:content)&.each do |_media, media_doc|
66
+ media_doc.each do |_mtype, mtype_doc|
67
+ fill_with_ref mtype_doc, :schemas
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,65 @@
1
+ require 'active_support/hash_with_indifferent_access'
2
+
3
+ module ParamsProcessor
4
+ class ParamDoc < HashWithIndifferentAccess
5
+ # Interfaces for directly taking the processed info what we focus on.
6
+ def range
7
+ return if (schema.keys & %w[ minimum maximum ]).blank?
8
+ {
9
+ min: schema[:minimum] || -Float::INFINITY,
10
+ max: schema[:maximum] || Float::INFINITY,
11
+ should_neq_min?: schema[:exclusiveMinimum] || false,
12
+ should_neq_max?: schema[:exclusiveMaximum] || false
13
+ }
14
+ end
15
+
16
+ def size
17
+ return if (schema.keys & %w[ minItems maxItems minLength maxLength ]).blank?
18
+ size = if type.in? %w[ array object ]
19
+ [schema[:minItems], schema[:maxItems]]
20
+ else
21
+ [schema[:minLength], schema[:maxLength]]
22
+ end
23
+ { min: size[0] || 0, max: size[1] || Float::INFINITY }
24
+ end
25
+
26
+ def combined
27
+ { all_of: all_of, one_of: one_of, any_of: any_of, not: not_be }.keep_if { |_k, v| !v.nil? }
28
+ end
29
+
30
+ def combined?; combined.present? end
31
+
32
+ def combined_modes; combined.keys end
33
+
34
+ def real_name; as || name end
35
+
36
+ def doced_permit?; permit? || not_permit? end
37
+
38
+ { # INTERFACE_MAPPING
39
+ name: %i[ name ],
40
+ required: %i[ required ],
41
+ in: %i[ in ],
42
+ schema: %i[ schema ],
43
+ enum: %i[ schema enum ],
44
+ pattern: %i[ schema pattern ],
45
+ regexp: %i[ schema pattern ],
46
+ type: %i[ schema type ],
47
+ format: %i[ schema format ],
48
+ all_of: %i[ schema allOf ],
49
+ one_of: %i[ schema oneOf ],
50
+ any_of: %i[ schema anyOf ],
51
+ not_be: %i[ schema not ],
52
+ is: %i[ schema is ],
53
+ dft: %i[ schema default ],
54
+ as: %i[ schema as ],
55
+ items: %i[ schema items ],
56
+ props: %i[ schema properties ],
57
+ blankable: %i[ schema blankable ],
58
+ permit?: %i[ schema permit ],
59
+ not_permit?: %i[ schema not_permit ],
60
+ }.each do |method, path|
61
+ define_method method do self.dig(*path) end
62
+ end
63
+ alias required? required
64
+ end
65
+ end
@@ -0,0 +1,81 @@
1
+ module ParamsProcessor
2
+ class TypeConvert
3
+ class << self
4
+ def call(input, based_on:)
5
+ @input = input
6
+ @doc = based_on
7
+ convert
8
+ end
9
+
10
+ # TODO: 循环和递归转换
11
+ def convert
12
+ send(@doc.type || @doc.combined_modes.first) # TODO
13
+ rescue NoMethodError
14
+ @input
15
+ end
16
+
17
+ # int32 / int64
18
+ def integer
19
+ @input.to_i
20
+ end
21
+
22
+ # float / double
23
+ def number
24
+ @input.to_f
25
+ end
26
+
27
+ def boolean
28
+ @input.to_s.in?(%w[ true 1 ]) ? true : false
29
+ end
30
+
31
+ # date / date-time / base64
32
+ def string
33
+ case @doc.format
34
+ when 'date' then parse_time(Date)
35
+ when 'date-time' then parse_time(DateTime)
36
+ when 'base64' then @input # Base64.strict_decode64(@input)
37
+ else @input.to_s
38
+ end
39
+ end
40
+
41
+ def array
42
+ @input
43
+ end
44
+
45
+ def object
46
+ @input
47
+ end
48
+
49
+ # combined TODO
50
+
51
+ def all_of
52
+ doc = ParamDoc.new name: @doc.name, schema: @doc.all_of.reduce({}, :merge)
53
+ TypeConvert.(@input, based_on: doc)
54
+ end
55
+
56
+ def one_of
57
+ doc = ParamDoc.new name: @doc.name, schema: @doc.all_of.reduce({}, :merge)
58
+ TypeConvert.(@input, based_on: doc)
59
+ end
60
+
61
+ def any_of
62
+ doc = ParamDoc.new name: @doc.name, schema: @doc.all_of.reduce({}, :merge)
63
+ TypeConvert.(@input, based_on: doc)
64
+ end
65
+
66
+ def not
67
+ @input
68
+ end
69
+
70
+ # helpers
71
+
72
+ def parse_time(cls)
73
+ if @doc.pattern
74
+ cls.send(:strptime, @input, @doc.pattern)
75
+ else
76
+ cls.send(:parse, @input)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,198 @@
1
+ require 'multi_json'
2
+ require 'params_processor/config'
3
+ require 'params_processor/doc_converter'
4
+ require 'params_processor/param_doc'
5
+
6
+ module ParamsProcessor
7
+ class Validate
8
+ class << self
9
+ def call(input, based_on:, raise: nil)
10
+ @error_class = raise
11
+ input(input).check! based_on
12
+ end
13
+
14
+ def input(input)
15
+ @input = input
16
+ @str_input = input.to_s
17
+ self
18
+ end
19
+
20
+ def check!(param_doc)
21
+ @doc = param_doc
22
+ check (if_is_passed do
23
+ check if_is_present
24
+ break if @input.nil?# || (@doc.blankable != false && @input.blank? && @input != false)
25
+ check_combined_types if @doc.combined?
26
+ check type if @doc.type
27
+ check size if @doc.size
28
+ check if_is_entity if @doc.is
29
+ check if_in_allowable_values if @doc.enum
30
+ check if_match_pattern if @doc.pattern
31
+ check if_is_in_range if @doc.range
32
+ check_each_element if @doc.type == 'array'
33
+ check_each_pair if @doc.type == 'object'
34
+ end)
35
+ end
36
+
37
+ def if_is_passed(&block)
38
+ return [:not_passed, ''] if @doc.required && @input.nil?
39
+ self.instance_eval(&block) unless @input.nil?
40
+ end
41
+
42
+ def if_is_present
43
+ [:is_blank, ''] if @doc.blankable == false && @input.blank? && @input != false
44
+ end
45
+
46
+ # TODO: combined type
47
+ def type
48
+ case @doc.type
49
+ when 'integer' then @str_input.match?(/^-?\d*$/)
50
+ when 'boolean' then @str_input.in? %w[ true 1 false 0 ]
51
+ when 'array' then @input.is_a? Array
52
+ when 'object' then @input.is_a?(ActionController::Parameters) || @input.is_a?(Hash)
53
+ when 'number' then _number_type
54
+ when 'string' then _string_type
55
+ else true # TODO
56
+ end or [:wrong_type, @doc.format.to_s]
57
+ end
58
+
59
+ def _number_type
60
+ case @doc.format
61
+ when 'float' then @str_input.match?(/^[-+]?\d*\.?\d+$/)
62
+ when 'double' then @str_input.match?(/^[-+]?\d*\.?\d+$/)
63
+ else true # TODO
64
+ end or [:wrong_type, @doc.format.to_s]
65
+ end
66
+
67
+ def _string_type
68
+ # return false unless @input.is_a? String
69
+ case @doc.format
70
+ when 'date' then parse_time!(Date)
71
+ when 'date-time' then parse_time!(DateTime)
72
+ when 'base64' then Base64.strict_decode64(@input)
73
+ when 'json' then MultiJson.load(@input)
74
+ else Config.strict_check ? @input.is_a?(String) : true
75
+ end
76
+ rescue ArgumentError, MultiJson::ParseError
77
+ false
78
+ end
79
+
80
+ def size
81
+ if @doc.type.in? %w[ array object ]
82
+ @input.size >= @doc.size[:min] && @input.size <= @doc.size[:max]
83
+ else
84
+ @str_input.length >= @doc.size[:min] && @str_input.length <= @doc.size[:max]
85
+ end or [:wrong_size, @doc.size.values.join('..')]
86
+ end
87
+
88
+ def if_is_entity
89
+ case @doc.is
90
+ when 'email'; @str_input.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]{2,}\z/i)
91
+ else true # TODO
92
+ end or [:is_not_entity, @doc.is.to_s]
93
+ end
94
+
95
+ def if_in_allowable_values
96
+ case @doc.type
97
+ when 'integer' then @doc.enum.include?(@input.to_i)
98
+ else @doc.enum.map(&:to_s).include?(@str_input)
99
+ end or [:not_in_allowable_values, @doc.enum.to_s.delete('\"')]
100
+ end
101
+
102
+ def if_match_pattern
103
+ unless @str_input.match?(Regexp.new(@doc.pattern))
104
+ [:not_match_pattern, "/#{@doc.pattern}/"]
105
+ end
106
+ end
107
+
108
+ def if_is_in_range
109
+ rg = @doc.range
110
+ fmt = @doc.format.tr('-', '_').camelize.constantize if @doc.format&.match?('date')
111
+ min = fmt ? parse_time!(fmt, rg[:min] || '1-1-1') : rg[:min]
112
+ max = fmt ? parse_time!(fmt, rg[:max] || '9999-12-31') : rg[:max]
113
+ @input = fmt ? parse_time!(fmt, @input) : @input.to_f
114
+
115
+ left_op = rg[:should_neq_min?] ? :< : :<=
116
+ right_op = rg[:should_neq_max?] ? :< : :<=
117
+ is_in_range = min&.send(left_op, @input)
118
+ is_in_range &= @input.send(right_op, max)
119
+ [:out_of_range, "#{min} #{left_op} x #{right_op} #{max}"] unless is_in_range
120
+ end
121
+
122
+ def check_combined_types
123
+ @doc.combined.each do |mode, schemas|
124
+ results = check_each_schema(schemas)
125
+
126
+ case mode
127
+ when :all_of then results.all?(&:present?)
128
+ when :one_of then results.count(true) == 1
129
+ when :any_of then results.include?(true)
130
+ when :not then results.all?(&:blank?)
131
+ end or raise ValidationFailed, " `#{@doc.name.to_sym}` #{Config.wrong_combined_type} #{mode} the specified schemas."
132
+ end
133
+ end
134
+
135
+ def check_each_schema(schemas)
136
+ results = [ ]
137
+ strict_check = Config.strict_check.clone
138
+ _doc, Config.strict_check = @doc, true
139
+ schemas.each do |schema|
140
+ doc = ParamDoc.new name: @doc.name, schema: schema
141
+ begin
142
+ Validate.(@input, based_on: doc, raise: @error_class)
143
+ results << true
144
+ rescue ValidationFailed
145
+ results << false
146
+ end
147
+ end
148
+ @doc, Config.strict_check = _doc, strict_check
149
+ results
150
+ end
151
+
152
+ def check_each_element
153
+ return if @doc.items.blank?
154
+
155
+ _doc, _input = @doc, @input
156
+ items_doc = ParamDoc.new name: @doc.name, schema: @doc.items
157
+ @input.each do |input|
158
+ Validate.(input, based_on: items_doc, raise: @error_class)
159
+ end
160
+ @doc, @input = _doc, _input
161
+ end
162
+
163
+ def check_each_pair
164
+ return if @doc.props.blank?
165
+
166
+ _doc = @doc
167
+ required = (@doc[:schema][:required] || [ ]).map(&:to_s)
168
+ @doc.props.each do |name, schema|
169
+ prop_doc = ParamDoc.new name: name, required: required.include?(name), schema: schema
170
+ _input = @input
171
+ Validate.(@input[name] || @input[name.to_sym], based_on: prop_doc, raise: @error_class)
172
+ @input = _input
173
+ end
174
+ @doc = _doc
175
+ end
176
+
177
+ def check msg
178
+ return unless msg.is_a? Array
179
+ @error_class.send("#{@doc.name}_#{msg.first}!") if @error_class.respond_to? "#{@doc.name}_#{msg.first}!"
180
+ @error_class.send("#{msg.first}!") if @error_class.respond_to? msg.first
181
+ raise ValidationFailed, Config.production_msg if Config.production_msg.present?
182
+
183
+ test_msg = Config.send(msg.first) if Config.test
184
+ msg = "#{Config.send(msg.first)}#{' ' + msg.last if msg.last.present?}"
185
+ msg = " `#{@doc.name.to_sym}` " << msg
186
+ raise ValidationFailed, test_msg || msg
187
+ end
188
+
189
+ def parse_time!(cls, value = nil)
190
+ if @doc.pattern
191
+ cls.send(:strptime, value || @input, @doc.pattern)
192
+ else
193
+ cls.send(:parse, value || @input)
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,3 @@
1
+ module ParamsProcessor
2
+ VERSION = '0.4.0'
3
+ end
@@ -0,0 +1,34 @@
1
+
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'params_processor/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'zero-params_processor'
8
+ spec.version = ParamsProcessor::VERSION
9
+ spec.authors = ['zhandao']
10
+ spec.email = ['x@skippingcat.com']
11
+
12
+ spec.summary = 'Process parameters base on OpenApi3 JSON documentation'
13
+ spec.description = 'Process parameters base on OpenApi3 JSON documentation, such as: ' \
14
+ 'validation and type conversion'
15
+ spec.homepage = 'https://github.com/zhandao/zero-params_processor'
16
+ spec.license = 'MIT'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
+ f.match(%r{^(test|spec|features)/})
20
+ end
21
+ spec.bindir = 'exe'
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ['lib']
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.16.a'
26
+ spec.add_development_dependency 'rake', '~> 10.0'
27
+ spec.add_development_dependency 'rspec', '~> 3.0'
28
+ spec.add_development_dependency 'simplecov'
29
+
30
+ spec.add_runtime_dependency 'zero-rails_openapi', '>= 1.5.2'
31
+ spec.add_runtime_dependency 'rails', '>= 3'
32
+ spec.add_runtime_dependency 'activesupport', '>= 3'
33
+ spec.add_runtime_dependency 'multi_json'
34
+ end
metadata ADDED
@@ -0,0 +1,176 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zero-params_processor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - zhandao
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-08-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.16.a
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.16.a
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: zero-rails_openapi
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 1.5.2
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 1.5.2
83
+ - !ruby/object:Gem::Dependency
84
+ name: rails
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '3'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '3'
97
+ - !ruby/object:Gem::Dependency
98
+ name: activesupport
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '3'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '3'
111
+ - !ruby/object:Gem::Dependency
112
+ name: multi_json
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: 'Process parameters base on OpenApi3 JSON documentation, such as: validation
126
+ and type conversion'
127
+ email:
128
+ - x@skippingcat.com
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - ".gitignore"
134
+ - ".rspec"
135
+ - ".rubocop.yml"
136
+ - ".travis.yml"
137
+ - Gemfile
138
+ - Gemfile.lock
139
+ - LICENSE.txt
140
+ - README.md
141
+ - Rakefile
142
+ - bin/console
143
+ - bin/setup
144
+ - lib/params_processor.rb
145
+ - lib/params_processor/config.rb
146
+ - lib/params_processor/doc_converter.rb
147
+ - lib/params_processor/param_doc.rb
148
+ - lib/params_processor/type_convert.rb
149
+ - lib/params_processor/validate.rb
150
+ - lib/params_processor/version.rb
151
+ - zero-params_processor.gemspec
152
+ homepage: https://github.com/zhandao/zero-params_processor
153
+ licenses:
154
+ - MIT
155
+ metadata: {}
156
+ post_install_message:
157
+ rdoc_options: []
158
+ require_paths:
159
+ - lib
160
+ required_ruby_version: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ required_rubygems_version: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ requirements: []
171
+ rubyforge_project:
172
+ rubygems_version: 2.7.6
173
+ signing_key:
174
+ specification_version: 4
175
+ summary: Process parameters base on OpenApi3 JSON documentation
176
+ test_files: []