zero-params_processor 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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: []