formeze 4.3.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 65adcff0153dbcbf39d46bbdb6ad7a60765e57e8329b89d3003ed1e4c9c9bcf5
4
- data.tar.gz: 99f871b6b7228ec472abaf8a2dbe820ea33a7cc5b3f3ef6dcd83f95a18410bda
3
+ metadata.gz: bd2160c23942cd4a127e7f84993aceb1a416a687060fb4f907181d295d5b26d5
4
+ data.tar.gz: 7406001697348315beeeb47c7400756e4bf8fb8acb60b8d872004f8de2966382
5
5
  SHA512:
6
- metadata.gz: d1a01576d7f38d324e8aa7c33279fd25f36227d49e124b8f53f6f56605cd632478e88616cf13c9e4c37994472905bea37cc7fe211f85ac6e98aca5dbe2bf37c1
7
- data.tar.gz: 12564e8c34d417f605c886fe43202fdde129aea2370699ee8411cb3e4703f99fbeb4ba5a4ad054a295ae8387708da3fe6db80454a5de3f779c69852e3c7777db
6
+ metadata.gz: 83a0278250e4a4d9a1e7adae850e34cc4d44bdfa0e71638cf15ec7042abce347889495a4c6466d189ca30a9eb7b35e23472db91237f85f11841ef83fee2e01f8
7
+ data.tar.gz: b4218dd2e97436668378a40863a652ef1e7f7e329c0fa903e44197846b935ba8af21c7d2e34f804e483c9fda385deb39529b8a2049f69e4130b2238ef1ef9c2b
data/CHANGES.md CHANGED
@@ -1,3 +1,31 @@
1
+ # 5.0.0
2
+
3
+ * Added a `fill` field option to specify how to fill a field. For example:
4
+
5
+ class ExampleForm < Formeze::Form
6
+ field :year, required: false, fill: ->{ _1.date&.year }
7
+ end
8
+
9
+ * Added functionality for defining field specific error messages via i18n.
10
+
11
+ Add translations to your locale files like this:
12
+
13
+ ExampleForm:
14
+ errors:
15
+ comments:
16
+ required: 'are required'
17
+
18
+ * Removed support for older rubies. **Required ruby version is now 3.0.0**
19
+
20
+ * Changed from cgi to rack for parsing form data, adding support for parsing
21
+ requests with different methods e.g. PUT and PATCH instead of just POST.
22
+
23
+ Whilst this should largely be backwards compatible there are some differences,
24
+ for example uploaded files will be instances of `Rack::Multipart::UploadedFile`
25
+ instead of `StringIO` instances as they were before.
26
+
27
+ * Changed the default blank value to `nil`
28
+
1
29
  # 4.3.0
2
30
 
3
31
  * Added dependency on cgi gem
data/README.md CHANGED
@@ -3,19 +3,19 @@
3
3
  ![Gem Version](https://badge.fury.io/rb/formeze.svg)
4
4
  ![Test Status](https://github.com/readysteady/formeze/actions/workflows/test.yml/badge.svg)
5
5
 
6
- Ruby gem for validating form data.
6
+ Ruby gem for parsing and validating form data.
7
7
 
8
8
 
9
9
  ## Motivation
10
10
 
11
- Most web apps built for end users will need to process url-encoded form data.
11
+ Most web applications built for end users will need to process form data.
12
12
  Registration forms, profile forms, checkout forms, contact forms, and forms
13
13
  for adding/editing application specific data.
14
14
 
15
- As developers we would like to process this data safely, to minimise the
16
- possibility of security holes within our application that could be exploited.
17
- Formeze adopts the approach of being "strict by default", forcing the application
18
- code to be explicit in what it accepts as input.
15
+ With formeze you can define form objects that explicitly define what your
16
+ application expects as input. This is more secure, and leads to much better
17
+ separation of responsibilities, and also allows for implementing different
18
+ validation rules in different contexts.
19
19
 
20
20
 
21
21
  ## Install
@@ -53,7 +53,8 @@ else
53
53
  end
54
54
  ```
55
55
 
56
- Formeze will automatically ignore the Rails "utf8" and "authenticity_token" parameters.
56
+ Formeze will automatically ignore the Rails "authenticity_token", "commit",
57
+ and "utf8" parameters.
57
58
 
58
59
  If you prefer not to inherit from the `Formeze::Form` class then you can
59
60
  instead call the `Formeze.setup` method on your classes like this:
@@ -89,9 +90,9 @@ messages specific to a single field.
89
90
 
90
91
  ## Field options
91
92
 
92
- By default fields cannot be blank, they are limited to 64 characters,
93
- and they cannot contain newlines. These restrictions can be overridden
94
- by setting various field options.
93
+ By default fields are required (i.e. they cannot be blank), they are limited
94
+ to 64 characters, and they cannot contain newlines. These restrictions can be
95
+ overridden by setting various field options.
95
96
 
96
97
  Defining a field without any options works well for a simple text input.
97
98
  If the default length limit is too big or too small you can override it
@@ -101,11 +102,14 @@ by setting the `maxlength` option. For example:
101
102
  field :title, maxlength: 200
102
103
  ```
103
104
 
104
- Similarly there is a `minlength` option for validating fields that should
105
- have a minimum number of characters (e.g. passwords).
105
+ Similarly there is a `minlength` option for defining a minimum length:
106
+
107
+ ```ruby
108
+ field :password, minlength: 8
109
+ ```
106
110
 
107
111
  Fields are required by default. Specify the `required` option if the field
108
- is not required, i.e. the value of the field can be blank/empty. For example:
112
+ is optional. For example:
109
113
 
110
114
  ```ruby
111
115
  field :title, required: false
@@ -308,11 +312,11 @@ would include the value of the `formeze.errors.does_not_match` I18n key.
308
312
 
309
313
  ## I18n integration
310
314
 
311
- Formeze integrates with [I18n](http://edgeguides.rubyonrails.org/i18n.html)
315
+ Formeze integrates with the [i18n gem](https://rubygems.org/gems/i18n)
312
316
  so that you can define custom error messages and field labels within your
313
317
  locales (useful both for localization, and when working with designers).
314
- For example, here is how you would change the "required" error message
315
- (which defaults to "is required"):
318
+
319
+ Here is an example of how you would change the "required" error message:
316
320
 
317
321
  ```yaml
318
322
  # config/locales/en.yml
@@ -322,8 +326,20 @@ en:
322
326
  required: "cannot be blank"
323
327
  ```
324
328
 
325
- And here is an example of how you would set a custom label for fields named
326
- "first_name" (for which the default label would be "First name"):
329
+ Error messages defined in this way apply globally to all Formeze forms.
330
+
331
+ You can also change error messages on a per field basis, for example:
332
+
333
+ ```yaml
334
+ # config/locales/en.yml
335
+ en:
336
+ ExampleForm:
337
+ errors:
338
+ comments:
339
+ required: 'are required'
340
+ ```
341
+
342
+ Here is an example of how to define a custom label for "first_name" fields:
327
343
 
328
344
  ```yaml
329
345
  # config/locales/en.yml
data/formeze.gemspec CHANGED
@@ -1,15 +1,15 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'formeze'
3
- s.version = '4.3.0'
3
+ s.version = '5.0.0'
4
4
  s.license = 'LGPL-3.0'
5
5
  s.platform = Gem::Platform::RUBY
6
6
  s.authors = ['Tim Craft']
7
7
  s.email = ['mail@timcraft.com']
8
8
  s.homepage = 'https://github.com/readysteady/formeze'
9
- s.description = 'Ruby gem for validating form data'
9
+ s.description = 'Ruby gem for parsing and validating form data'
10
10
  s.summary = 'See description'
11
11
  s.files = Dir.glob('lib/**/*.rb') + %w(CHANGES.md LICENSE.txt README.md formeze.gemspec)
12
- s.required_ruby_version = '>= 2.4.0'
12
+ s.required_ruby_version = '>= 3.0.0'
13
13
  s.require_path = 'lib'
14
14
  s.metadata = {
15
15
  'homepage' => 'https://github.com/readysteady/formeze',
@@ -17,5 +17,5 @@ Gem::Specification.new do |s|
17
17
  'bug_tracker_uri' => 'https://github.com/readysteady/formeze/issues',
18
18
  'changelog_uri' => 'https://github.com/readysteady/formeze/blob/main/CHANGES.md'
19
19
  }
20
- s.add_dependency 'cgi'
20
+ s.add_dependency 'rack', '~> 3'
21
21
  end
@@ -1,4 +1,4 @@
1
- module Formeze::Condition
1
+ module Formeze::Block
2
2
  def self.evaluate(instance, block)
3
3
  block = block.to_proc
4
4
 
@@ -1,11 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Formeze::Errors
2
4
  SCOPE = [:formeze, :errors].freeze
3
5
 
4
- def self.translate(error, default)
5
- if defined?(I18n)
6
- return I18n.translate(error, scope: SCOPE, default: default)
7
- end
6
+ DEFAULT = {
7
+ bad_value: 'is invalid',
8
+ not_accepted: 'is not an accepted file type',
9
+ not_multiline: 'cannot contain newlines',
10
+ no_match: 'is invalid',
11
+ required: 'is required',
12
+ too_large: 'is too large',
13
+ too_long: 'is too long',
14
+ too_short: 'is too short',
15
+ }
16
+
17
+ def self.translate(error, scope:)
18
+ default = DEFAULT[error] || 'is invalid'
19
+
20
+ return default unless defined?(I18n)
21
+
22
+ message = I18n.translate(error, scope: scope, default: nil)
8
23
 
9
- default
24
+ message || I18n.translate(error, scope: SCOPE, default: default)
10
25
  end
11
26
  end
data/lib/formeze/field.rb CHANGED
@@ -16,7 +16,7 @@ class Formeze::Field
16
16
  if String === value
17
17
  value = validate(value, form)
18
18
  else
19
- form.add_error(self, :not_accepted, 'is not an accepted file type') unless acceptable_file?(value)
19
+ form.add_error(self, :not_accepted) unless acceptable_file?(value)
20
20
 
21
21
  size += value.size
22
22
  end
@@ -26,26 +26,26 @@ class Formeze::Field
26
26
  form.send(:"#{name}=", value)
27
27
  end
28
28
 
29
- form.add_error(self, :too_large, 'is too large') if maxsize? && size > maxsize
29
+ form.add_error(self, :too_large) if maxsize? && size > maxsize
30
30
  end
31
31
 
32
32
  def validate(value, form)
33
33
  value = Formeze.scrub(value, @options[:scrub])
34
34
 
35
35
  if blank?(value)
36
- form.add_error(self, :required, 'is required') if required?
36
+ form.add_error(self, :required) if required?
37
37
 
38
- value = blank_value if blank_value?
38
+ value = blank_value
39
39
  else
40
- form.add_error(self, :not_multiline, 'cannot contain newlines') if !multiline? && value.lines.count > 1
40
+ form.add_error(self, :not_multiline) if !multiline? && value.lines.count > 1
41
41
 
42
- form.add_error(self, :too_long, 'is too long') if too_long?(value)
42
+ form.add_error(self, :too_long) if too_long?(value)
43
43
 
44
- form.add_error(self, :too_short, 'is too short') if too_short?(value)
44
+ form.add_error(self, :too_short) if too_short?(value)
45
45
 
46
- form.add_error(self, :no_match, 'is invalid') if no_match?(value)
46
+ form.add_error(self, :no_match) if no_match?(value)
47
47
 
48
- form.add_error(self, :bad_value, 'is invalid') if values? && !values.include?(value)
48
+ form.add_error(self, :bad_value) if values? && !values.include?(value)
49
49
  end
50
50
 
51
51
  value
@@ -95,6 +95,10 @@ class Formeze::Field
95
95
  @options.fetch(:maxsize)
96
96
  end
97
97
 
98
+ def accept?
99
+ @options.key?(:accept)
100
+ end
101
+
98
102
  def accept
99
103
  @accept ||= @options.fetch(:accept).split(',').flat_map { |type| MIME::Types[type] }
100
104
  end
@@ -111,12 +115,8 @@ class Formeze::Field
111
115
  @options.key?(:pattern) && value !~ @options[:pattern]
112
116
  end
113
117
 
114
- def blank_value?
115
- @options.key?(:blank)
116
- end
117
-
118
118
  def blank_value
119
- @options.fetch(:blank)
119
+ @options[:blank]
120
120
  end
121
121
 
122
122
  def values?
@@ -142,4 +142,22 @@ class Formeze::Field
142
142
  def defined_unless
143
143
  @options.fetch(:defined_unless)
144
144
  end
145
+
146
+ def undefined?(form)
147
+ if defined_if?
148
+ !Formeze::Block.evaluate(form, defined_if)
149
+ elsif defined_unless?
150
+ Formeze::Block.evaluate(form, defined_unless)
151
+ else
152
+ false
153
+ end
154
+ end
155
+
156
+ def fill_proc?
157
+ @options.key?(:fill)
158
+ end
159
+
160
+ def fill_proc
161
+ @options[:fill]
162
+ end
145
163
  end
@@ -1,23 +1,53 @@
1
- require 'cgi'
1
+ # frozen_string_literal: true
2
+ require 'rack'
2
3
 
3
4
  module Formeze::FormData
4
- class CGI < ::CGI
5
- def env_table
6
- @options[:request].env
5
+ def self.parse(input)
6
+ if input.is_a?(String)
7
+ query_parser.parse_query(input)
8
+ elsif input.respond_to?(:env)
9
+ body = input.body
10
+ body.rewind if body.respond_to?(:rewind)
11
+ case input.media_type
12
+ when 'multipart/form-data'
13
+ Rack::Multipart.parse_multipart(input.env, Params)
14
+ when 'application/x-www-form-urlencoded'
15
+ query_parser.parse_query(body.read)
16
+ else
17
+ raise ArgumentError, "can't parse #{input.media_type.inspect} form data"
18
+ end
19
+ else
20
+ raise ArgumentError, "can't parse #{input.class} form data"
21
+ end
22
+ end
23
+
24
+ module Params
25
+ def self.make_params
26
+ ParamsHash.new { |h, k| h[k] = Array.new }
7
27
  end
8
28
 
9
- def stdinput
10
- @options[:request].body.tap do |body|
11
- body.rewind if body.respond_to?(:rewind)
29
+ def self.normalize_params(params, key, value)
30
+ if value.is_a?(Hash)
31
+ value = Rack::Multipart::UploadedFile.new(io: value[:tempfile], filename: value[:filename], content_type: value[:type])
12
32
  end
33
+
34
+ params[key] << value
13
35
  end
14
36
  end
15
37
 
16
- def self.parse(input)
17
- if input.is_a?(String)
18
- CGI.parse(input)
19
- else
20
- CGI.new(request: input).params
38
+ class ParamsHash < ::Hash
39
+ alias_method :to_params_hash, :to_h
40
+ end
41
+
42
+ class QueryParser < Rack::QueryParser
43
+ def make_params
44
+ Hash.new { |h, k| h[k] = Array.new }
21
45
  end
22
46
  end
47
+
48
+ def self.query_parser
49
+ @query_parser ||= QueryParser.new(nil, 0)
50
+ end
51
+
52
+ private_class_method :query_parser
23
53
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  class Formeze::Validation
4
2
  include Formeze::Presence
5
3
 
@@ -14,7 +12,7 @@ class Formeze::Validation
14
12
  end
15
13
 
16
14
  def validates?(form)
17
- @precondition ? Formeze::Condition.evaluate(form, @precondition) : true
15
+ @precondition ? Formeze::Block.evaluate(form, @precondition) : true
18
16
  end
19
17
 
20
18
  def field_value?(form)
@@ -33,7 +31,7 @@ class Formeze::Validation
33
31
  form.instance_eval(&@block)
34
32
  end
35
33
 
36
- form.add_error(@field, @error, 'is invalid') unless return_value
34
+ form.add_error(@field, @error) unless return_value
37
35
  end
38
36
  end
39
37
  end
data/lib/formeze.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Formeze
4
- autoload :Condition, 'formeze/condition'
4
+ autoload :Block, 'formeze/block'
5
5
  autoload :Errors, 'formeze/errors'
6
6
  autoload :Field, 'formeze/field'
7
7
  autoload :Form, 'formeze/form'
@@ -36,16 +36,24 @@ module Formeze
36
36
 
37
37
  class ValueError < StandardError; end
38
38
 
39
- class ValidationError < StandardError; end
39
+ class ValidationError < StandardError
40
+ def initialize(field, message)
41
+ @field = field
40
42
 
41
- RAILS_FORM_KEYS = %w[utf8 authenticity_token commit]
43
+ super("#{field.label} #{message}")
44
+ end
42
45
 
43
- private_constant :RAILS_FORM_KEYS
46
+ def field_name
47
+ @field.name
48
+ end
49
+ end
44
50
 
45
51
  module InstanceMethods
46
52
  def fill(object)
47
53
  self.class.fields.each_value do |field|
48
- if Hash === object && object.key?(field.name)
54
+ if field.fill_proc?
55
+ send(:"#{field.name}=", Formeze::Block.evaluate(object, field.fill_proc))
56
+ elsif Hash === object && object.key?(field.name)
49
57
  send(:"#{field.name}=", object[field.name])
50
58
  elsif object.respond_to?(field.name)
51
59
  send(:"#{field.name}=", object.send(field.name))
@@ -59,27 +67,31 @@ module Formeze
59
67
  form_data = FormData.parse(input)
60
68
 
61
69
  self.class.fields.each_value do |field|
62
- next unless field_defined?(field)
70
+ next if field.undefined?(self)
63
71
 
64
72
  unless form_data.key?(field.key)
65
73
  next if field.multiple? || !field.key_required?
66
74
 
67
- raise KeyError, "missing form key: #{field.key}"
75
+ raise KeyError, "missing form key: #{field.key}" unless field.accept?
68
76
  end
69
77
 
70
78
  values = form_data.delete(field.key)
71
79
 
72
- if values.length > 1
73
- raise ValueError unless field.multiple?
74
- end
80
+ if values.is_a?(Array)
81
+ if values.length > 1
82
+ raise ValueError, "multiple values for #{field.key} field" unless field.multiple?
83
+ end
75
84
 
76
- field.validate_all(values, self)
85
+ field.validate_all(values, self)
86
+ else
87
+ field.validate(values, self)
88
+ end
77
89
  end
78
90
 
79
91
  if defined?(Rails)
80
- RAILS_FORM_KEYS.each do |key|
81
- form_data.delete(key)
82
- end
92
+ form_data.delete('authenticity_token')
93
+ form_data.delete('commit')
94
+ form_data.delete('utf8')
83
95
  end
84
96
 
85
97
  unless form_data.empty?
@@ -93,14 +105,10 @@ module Formeze
93
105
  return self
94
106
  end
95
107
 
96
- def add_error(field, message, default = nil)
97
- message = Formeze::Errors.translate(message, default) unless default.nil?
98
-
99
- error = ValidationError.new("#{field.label} #{message}")
108
+ def add_error(field, error)
109
+ message = Formeze::Errors.translate(error, scope: "#{self.class.name}.errors.#{field.name}")
100
110
 
101
- errors << error
102
-
103
- field_errors[field.name] << error
111
+ errors << ValidationError.new(field, message)
104
112
  end
105
113
 
106
114
  def valid?
@@ -116,11 +124,11 @@ module Formeze
116
124
  end
117
125
 
118
126
  def errors_on?(field_name)
119
- field_errors[field_name].size > 0
127
+ errors.any? { |error| error.field_name == field_name }
120
128
  end
121
129
 
122
130
  def errors_on(field_name)
123
- field_errors[field_name]
131
+ errors.select { |error| error.field_name == field_name }
124
132
  end
125
133
 
126
134
  def to_h
@@ -130,22 +138,6 @@ module Formeze
130
138
  end
131
139
 
132
140
  alias_method :to_hash, :to_h
133
-
134
- private
135
-
136
- def field_defined?(field)
137
- if field.defined_if?
138
- Formeze::Condition.evaluate(self, field.defined_if)
139
- elsif field.defined_unless?
140
- !Formeze::Condition.evaluate(self, field.defined_unless)
141
- else
142
- true
143
- end
144
- end
145
-
146
- def field_errors
147
- @field_errors ||= Hash.new { |h, k| h[k] = [] }
148
- end
149
141
  end
150
142
 
151
143
  def self.label(field_name)
@@ -154,11 +146,11 @@ module Formeze
154
146
 
155
147
  def self.scrub_methods
156
148
  @scrub_methods ||= {
157
- :strip => :strip.to_proc,
158
- :upcase => :upcase.to_proc,
159
- :downcase => :downcase.to_proc,
160
- :squeeze => proc { |string| string.squeeze(' ') },
161
- :squeeze_lines => proc { |string| string.gsub(/(\r?\n)(\r?\n)(\r?\n)+/, '\\1\\2') }
149
+ strip: :strip.to_proc,
150
+ upcase: :upcase.to_proc,
151
+ downcase: :downcase.to_proc,
152
+ squeeze: proc { |string| string.squeeze(' ') },
153
+ squeeze_lines: proc { |string| string.gsub(/(\r?\n)(\r?\n)(\r?\n)+/, '\\1\\2') }
162
154
  }
163
155
  end
164
156
 
metadata CHANGED
@@ -1,30 +1,30 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: formeze
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.0
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Craft
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-05 00:00:00.000000000 Z
11
+ date: 2024-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: cgi
14
+ name: rack
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '3'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
27
- description: Ruby gem for validating form data
26
+ version: '3'
27
+ description: Ruby gem for parsing and validating form data
28
28
  email:
29
29
  - mail@timcraft.com
30
30
  executables: []
@@ -36,7 +36,7 @@ files:
36
36
  - README.md
37
37
  - formeze.gemspec
38
38
  - lib/formeze.rb
39
- - lib/formeze/condition.rb
39
+ - lib/formeze/block.rb
40
40
  - lib/formeze/errors.rb
41
41
  - lib/formeze/field.rb
42
42
  - lib/formeze/form.rb
@@ -60,14 +60,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
60
60
  requirements:
61
61
  - - ">="
62
62
  - !ruby/object:Gem::Version
63
- version: 2.4.0
63
+ version: 3.0.0
64
64
  required_rubygems_version: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  requirements: []
70
- rubygems_version: 3.5.3
70
+ rubygems_version: 3.5.11
71
71
  signing_key:
72
72
  specification_version: 4
73
73
  summary: See description