paramore 0.2.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/LICENSE +21 -0
- data/README.md +174 -0
- data/bin/paramore +4 -0
- data/lib/paramore.rb +18 -0
- data/lib/paramore/cli.rb +30 -0
- data/lib/paramore/configuration.rb +15 -0
- data/lib/paramore/extension.rb +28 -0
- data/lib/paramore/format.rb +49 -0
- data/lib/paramore/railtie.rb +16 -0
- data/lib/paramore/validate.rb +40 -0
- metadata +103 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 78c8a8c6985a0f9044fa608f0473cbd6ce872c5ff7f9626a2fb6e49f39c67f69
|
4
|
+
data.tar.gz: c6c18de5f2dd039d06044ba58f7c4bfc38dda160fd6730213748c883529f8823
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e406c95c757d7c583fb5d962f4b453c8dbda6dc141ad581169aab7ba767e2903723eb31c830ef8f55b98e5a12606feb578644141800e64a154217c0cd2f886e0
|
7
|
+
data.tar.gz: 9061803ea9cdcccd492bb58f280d0037e01870c61d7dd30d8fad1d1ceabcb580f0e715531cf71ceff6fb8f875f60810ec95d7cfbedb14594f6fc65e85d491a1e
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2019 Lumzdas
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
# Paramore
|
2
|
+
|
3
|
+
Paramore is a small gem intended to make strong parameter definitions declarative
|
4
|
+
and provide a unified way to format and sanitize their values outside of controllers.
|
5
|
+
|
6
|
+
# Installation
|
7
|
+
|
8
|
+
|
9
|
+
In your Gemfile:
|
10
|
+
```ruby
|
11
|
+
gem 'paramore'
|
12
|
+
```
|
13
|
+
|
14
|
+
In your terminal:
|
15
|
+
```sh
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
# Usage
|
20
|
+
|
21
|
+
<h3>Without formatting/sanitizing</h3>
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
declare_params :item_params
|
25
|
+
item: [:name, :description, :for_sale, :price, metadata: [tags: []]]
|
26
|
+
```
|
27
|
+
|
28
|
+
This is completely equivalent (including return type) to
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
def item_params
|
32
|
+
@item_params ||= params
|
33
|
+
.require(:item)
|
34
|
+
.permit(:name, :description, :for_sale, :price, metadata: [tags: []])
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
<h3>With formatting/sanitizing</h3>
|
39
|
+
|
40
|
+
A common problem in app development is untrustworthy input given by clients.
|
41
|
+
That input needs to be sanitized and potentially formatted and type-cast for further processing.
|
42
|
+
A naive approach could be:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
# items_controller.rb
|
46
|
+
|
47
|
+
def item_params
|
48
|
+
@item_params ||= begin
|
49
|
+
_params = params
|
50
|
+
.require(:item)
|
51
|
+
.permit(:name, :description, :price, metadata: [tags: []])
|
52
|
+
|
53
|
+
_params[:name] = _params[:name].strip.squeeze(' ') if _params[:name]
|
54
|
+
_params[:description] = _params[:description].strip.squeeze(' ') if _params[:description]
|
55
|
+
_params[:for_sale] = _params[:for_sale].in?('t', 'true', '1') if _params[:for_sale]
|
56
|
+
_params[:price] = _params[:price].to_d if _params[:price]
|
57
|
+
if _params.dig(:metadata, :tags)
|
58
|
+
_params[:metadata][:tags] =
|
59
|
+
_params[:metadata][:tags].map { |tag_id| Item.tags[tag_id.to_i] }
|
60
|
+
end
|
61
|
+
|
62
|
+
_params
|
63
|
+
end
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
This approach clutters controllers with procedures to clean data, which leads to repetition and difficulties refactoring.
|
68
|
+
The next logical step is extracting those procedures - this is where Paramore steps in:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
# app/controllers/items_controller.rb
|
72
|
+
|
73
|
+
declare_params :item_params
|
74
|
+
item: [:name, :description, :price, metadata: [tags: []]],
|
75
|
+
format: {
|
76
|
+
name: :Text,
|
77
|
+
description: :Text,
|
78
|
+
for_sale: :Boolean,
|
79
|
+
price: :Decimal,
|
80
|
+
metadata: {
|
81
|
+
tags: :ItemTags
|
82
|
+
}
|
83
|
+
}
|
84
|
+
```
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
# app/formatter/text.rb
|
88
|
+
|
89
|
+
module Formatter::Text
|
90
|
+
module_function
|
91
|
+
def run(input)
|
92
|
+
input.strip.squeeze(' ')
|
93
|
+
end
|
94
|
+
end
|
95
|
+
```
|
96
|
+
```ruby
|
97
|
+
# app/formatter/boolean.rb
|
98
|
+
|
99
|
+
module Formatter::Boolean
|
100
|
+
TRUTHY_TEXT_VALUES = %w[t true 1]
|
101
|
+
|
102
|
+
module_function
|
103
|
+
def run(input)
|
104
|
+
input.in?(TRUTHY_TEXT_VALUES)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
```
|
108
|
+
```ruby
|
109
|
+
# app/formatter/decimal.rb
|
110
|
+
|
111
|
+
module Formatter::Decimal
|
112
|
+
module_function
|
113
|
+
def run(input)
|
114
|
+
input.to_d
|
115
|
+
end
|
116
|
+
end
|
117
|
+
```
|
118
|
+
```ruby
|
119
|
+
# app/formatter/item_tags.rb
|
120
|
+
|
121
|
+
module Formatter::ItemTags
|
122
|
+
module_function
|
123
|
+
def run(input)
|
124
|
+
input.map { |tag_id| Item.tags[tag_id.to_i] }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
|
130
|
+
Now, given `params` are:
|
131
|
+
```ruby
|
132
|
+
<ActionController::Parameters {
|
133
|
+
"unpermitted"=>"parameter",
|
134
|
+
"name"=>"Shoe \n",
|
135
|
+
"description"=>"Black, with laces",
|
136
|
+
"for_sale"=>"true",
|
137
|
+
"price"=>"39.99",
|
138
|
+
"metadata"=><ActionController::Parameters { "tags"=>["38", "112"] } permitted: false>
|
139
|
+
} permitted: false>
|
140
|
+
```
|
141
|
+
Calling `item_params` will return:
|
142
|
+
```ruby
|
143
|
+
<ActionController::Parameters {
|
144
|
+
"name"=>"Shoe",
|
145
|
+
"description"=>"Black, with laces",
|
146
|
+
"for_sale"=>true,
|
147
|
+
"price"=>39.99,
|
148
|
+
"metadata"=><ActionController::Parameters { "tags"=>[:shoe, :new] } permitted: true>
|
149
|
+
} permitted: true>
|
150
|
+
```
|
151
|
+
|
152
|
+
This is useful when the values are not used with Rails models, but are passed to simple functions for processing.
|
153
|
+
The formatters can also be easily reused anywhere in the app,
|
154
|
+
since they are completely decoupled from Rails.
|
155
|
+
|
156
|
+
<h3>Configuration</h3>
|
157
|
+
|
158
|
+
Running `$ paramore` will generate a configuration file located in `config/initializers/paramore.rb`.
|
159
|
+
- `config.formatter_namespace` - default is `Formatter`. Set to `nil` to have top level named formatters
|
160
|
+
(this also allows specifying the formatter object itself, eg.: `name: Formatter::Text`).
|
161
|
+
- `config.formatter_method_name` - default is `run`. Don't set to `nil` :D
|
162
|
+
|
163
|
+
<h3>Safety</h3>
|
164
|
+
|
165
|
+
- Formatters will not be called if their parameter is missing (no key in the param hash)
|
166
|
+
- Formatters are validated - all given formatter names must match actual modules/classes defined in the app
|
167
|
+
and must respond to the configured `formatter_method_name`.
|
168
|
+
This means that all used formatters are loaded when the controller is loaded.
|
169
|
+
|
170
|
+
# License
|
171
|
+
|
172
|
+
Paramore is released under the MIT license:
|
173
|
+
|
174
|
+
* https://opensource.org/licenses/MIT
|
data/bin/paramore
ADDED
data/lib/paramore.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'paramore/configuration'
|
4
|
+
require_relative 'paramore/railtie'
|
5
|
+
|
6
|
+
module Paramore
|
7
|
+
class << self
|
8
|
+
attr_reader :configuration
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.configuration
|
12
|
+
@configuration ||= Paramore::Configuration.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.configure
|
16
|
+
yield(configuration)
|
17
|
+
end
|
18
|
+
end
|
data/lib/paramore/cli.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Paramore
|
4
|
+
module Cli
|
5
|
+
module_function
|
6
|
+
def config!
|
7
|
+
config_file_path = 'config/initializers/paramore.rb'
|
8
|
+
|
9
|
+
if File.exists?(config_file_path)
|
10
|
+
puts "#{config_file_path} already exists, skipping"
|
11
|
+
exit 0
|
12
|
+
end
|
13
|
+
|
14
|
+
File.write(config_file_path, <<~CONF)
|
15
|
+
# frozen_string_literal: true
|
16
|
+
|
17
|
+
Paramore.configure do |config|
|
18
|
+
# change this to any level you need, eg.: `'A::B'` for doubly nested formatters
|
19
|
+
# or `nil` for top level formatters
|
20
|
+
# config.formatter_namespace = 'Formatter'
|
21
|
+
|
22
|
+
# what method name to call formatters with
|
23
|
+
# config.formatter_method_name = 'run'
|
24
|
+
end
|
25
|
+
CONF
|
26
|
+
|
27
|
+
puts "#{config_file_path} created"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Paramore
|
4
|
+
class Configuration
|
5
|
+
DEFAULT_FORMATTER_NAMESPACE = 'Formatter'
|
6
|
+
DEFAULT_FORMATTER_METHOD_NAME = 'run'
|
7
|
+
|
8
|
+
attr_accessor :formatter_namespace, :formatter_method_name
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@formatter_namespace = DEFAULT_FORMATTER_NAMESPACE
|
12
|
+
@formatter_method_name = DEFAULT_FORMATTER_METHOD_NAME
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'validate'
|
4
|
+
require_relative 'format'
|
5
|
+
|
6
|
+
module Paramore
|
7
|
+
module Extension
|
8
|
+
def declare_params(accessor_name, param_definition)
|
9
|
+
format_definition = param_definition.delete(:format)
|
10
|
+
|
11
|
+
Validate.run(param_definition, format_definition)
|
12
|
+
|
13
|
+
required = param_definition.keys.first
|
14
|
+
permitted = param_definition.values.first
|
15
|
+
|
16
|
+
define_method(accessor_name) do |rails_parameters = params|
|
17
|
+
return instance_variable_get("@#{accessor_name}") if instance_variable_defined?("@#{accessor_name}")
|
18
|
+
|
19
|
+
permitted_params = rails_parameters.require(required).permit(permitted)
|
20
|
+
|
21
|
+
instance_variable_set(
|
22
|
+
"@#{accessor_name}",
|
23
|
+
permitted_params.merge(Format.run(format_definition, permitted_params)).permit!
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Paramore
|
4
|
+
module Format
|
5
|
+
module_function
|
6
|
+
def run(format_definition, permitted_params)
|
7
|
+
return {} unless format_definition
|
8
|
+
|
9
|
+
recursive_merge(
|
10
|
+
recursive_format(
|
11
|
+
format_definition, permitted_params
|
12
|
+
)
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
def recursive_merge(nested_hash_array)
|
17
|
+
nested_hash_array.reduce(:merge).map do |param_name, value|
|
18
|
+
if value.kind_of?(Array) && value.all? { |_value| _value.kind_of?(Hash) }
|
19
|
+
{ param_name => recursive_merge(value) }
|
20
|
+
else
|
21
|
+
{ param_name => value }
|
22
|
+
end
|
23
|
+
end.reduce(:merge)
|
24
|
+
end
|
25
|
+
|
26
|
+
def recursive_format(format_definition, permitted_params)
|
27
|
+
format_definition.map do |param_name, value|
|
28
|
+
next {} unless permitted_params[param_name]
|
29
|
+
|
30
|
+
if value.kind_of?(Hash)
|
31
|
+
{ param_name => recursive_format(value, permitted_params[param_name]) }
|
32
|
+
else
|
33
|
+
{ param_name => formatted_value(permitted_params[param_name], formatter_for(value)) }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def formatted_value(value, formatter)
|
39
|
+
formatter.send(Paramore.configuration.formatter_method_name, value)
|
40
|
+
end
|
41
|
+
|
42
|
+
def formatter_for(formatter_name)
|
43
|
+
Object.const_get(
|
44
|
+
[Paramore.configuration.formatter_namespace, formatter_name].compact.join('::'),
|
45
|
+
false # inherit=false - only get exact match
|
46
|
+
)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'extension'
|
4
|
+
|
5
|
+
return unless defined?(Rails)
|
6
|
+
|
7
|
+
module Paramore
|
8
|
+
class Railtie < Rails::Railtie
|
9
|
+
initializer 'paramore.action_controller' do
|
10
|
+
ActiveSupport.on_load(:action_controller) do
|
11
|
+
ActionController::Base.extend(Paramore::Extension)
|
12
|
+
ActionController::API.extend(Paramore::Extension)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Paramore
|
4
|
+
module Validate
|
5
|
+
module_function
|
6
|
+
def run(param_definition, format_definition)
|
7
|
+
unless param_definition.keys.size == 1
|
8
|
+
raise ArgumentError,
|
9
|
+
"Paramore: exactly one required attribute allowed! Given: #{param_definition.keys}"
|
10
|
+
end
|
11
|
+
|
12
|
+
return unless format_definition
|
13
|
+
|
14
|
+
formatter_names(format_definition).each do |formatter_name|
|
15
|
+
formatter =
|
16
|
+
begin
|
17
|
+
Paramore::Format.formatter_for(formatter_name)
|
18
|
+
rescue NameError => e
|
19
|
+
raise NameError, "Paramore: formatter `#{formatter_name}` is undefined! #{e}"
|
20
|
+
end
|
21
|
+
|
22
|
+
unless formatter.respond_to?(Paramore.configuration.formatter_method_name)
|
23
|
+
raise NoMethodError,
|
24
|
+
"Paramore: formatter `#{formatter_name}` does not respond to " +
|
25
|
+
"`#{Paramore.configuration.formatter_method_name}`!"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def formatter_names(format_definition)
|
31
|
+
format_definition.flat_map do |_, value|
|
32
|
+
if value.kind_of?(Hash)
|
33
|
+
formatter_names(value)
|
34
|
+
else
|
35
|
+
value
|
36
|
+
end
|
37
|
+
end.uniq
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
metadata
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: paramore
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Lukas Kairevičius
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-09-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rails
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
55
|
+
description: |
|
56
|
+
Paramore lets you declare which parameters are permitted and what object is responsible
|
57
|
+
for formatting/sanitizing/type-casting them before they passed along to your models/processors.
|
58
|
+
It is intended to reduce the amount of imperative code in controllers.
|
59
|
+
email: lukas.kairevicius9@gmail.com
|
60
|
+
executables:
|
61
|
+
- paramore
|
62
|
+
extensions: []
|
63
|
+
extra_rdoc_files: []
|
64
|
+
files:
|
65
|
+
- LICENSE
|
66
|
+
- README.md
|
67
|
+
- bin/paramore
|
68
|
+
- lib/paramore.rb
|
69
|
+
- lib/paramore/cli.rb
|
70
|
+
- lib/paramore/configuration.rb
|
71
|
+
- lib/paramore/extension.rb
|
72
|
+
- lib/paramore/format.rb
|
73
|
+
- lib/paramore/railtie.rb
|
74
|
+
- lib/paramore/validate.rb
|
75
|
+
homepage: https://github.com/lumzdas/paramore
|
76
|
+
licenses:
|
77
|
+
- MIT
|
78
|
+
metadata: {}
|
79
|
+
post_install_message: |
|
80
|
+
Thank you for installing Paramore 0.2.0 !
|
81
|
+
From the command line you can run `paramore` to generate a configuration file
|
82
|
+
|
83
|
+
More details here : https://github.com/lumzdas/paramore/blob/master/README.md
|
84
|
+
Feel free to report issues: https://github.com/lumzdas/paramore/issues
|
85
|
+
rdoc_options: []
|
86
|
+
require_paths:
|
87
|
+
- lib
|
88
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
requirements: []
|
99
|
+
rubygems_version: 3.0.3
|
100
|
+
signing_key:
|
101
|
+
specification_version: 4
|
102
|
+
summary: A declarative approach to Rails' strong parameter formatting and sanitizing
|
103
|
+
test_files: []
|