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 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
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../lib/paramore/cli'
3
+
4
+ Paramore::Cli.config!
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
@@ -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: []