formeze 4.3.0 → 5.0.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 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