optimism 0.3.3 → 0.4.2

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: 86be504b270b8c5ec388d09a1bdceaf7a415becb0847e911cd0a4f31ed756145
4
- data.tar.gz: 351510dbb07f54523d3bf16e2c9412f1c9c9edd8423dfa371713d49988936765
3
+ metadata.gz: 8966741ac69e6f62196d1ea78a71a30dda97fa56e6618fb844a829a5d8d93008
4
+ data.tar.gz: 5f7e64c28e697af5dd3a782549abf6a4872870f7bab9833868ed196a251bc806
5
5
  SHA512:
6
- metadata.gz: 89dd0cfe1095d4d1a63233195e2d9c69ab04298995406f3e035880cfd81d531a9b0407204ad875867340f0a3e03e5d5ac8bb286dd90dd47bd0b3e895d209cc6c
7
- data.tar.gz: 11721e72984cbeeae273a9a5096a4b743b21415b2bb5ef543cb00cb0059841b1522b6cf9ff6b22aeebb8fb3324026833b64399aee6f38749e63e0bd2944c9016
6
+ metadata.gz: 936c07c2d033fc9d0f932cb31ef073a62a919cc53c635b14b896520d387c4ba8beaa94348e3b67a42786aca948fd0f3760a80530d90f874099db7d177d61bd47
7
+ data.tar.gz: 8526dc93dd499644f9ebcf63ea3bb3fd24ff672a8ea7579518900800a2738b40cfefb3b96e9de228cb5e97ed718f05c6064871579487c49229e9288309d63a1a
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- optimism (0.3.1)
4
+ optimism (0.3.5)
5
5
  cable_ready (>= 4)
6
6
  rack (~> 2.0)
7
7
  rails (>= 6, >= 5.2)
@@ -9,75 +9,79 @@ PATH
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- actioncable (6.0.3.1)
13
- actionpack (= 6.0.3.1)
12
+ actioncable (6.1.0)
13
+ actionpack (= 6.1.0)
14
+ activesupport (= 6.1.0)
14
15
  nio4r (~> 2.0)
15
16
  websocket-driver (>= 0.6.1)
16
- actionmailbox (6.0.3.1)
17
- actionpack (= 6.0.3.1)
18
- activejob (= 6.0.3.1)
19
- activerecord (= 6.0.3.1)
20
- activestorage (= 6.0.3.1)
21
- activesupport (= 6.0.3.1)
17
+ actionmailbox (6.1.0)
18
+ actionpack (= 6.1.0)
19
+ activejob (= 6.1.0)
20
+ activerecord (= 6.1.0)
21
+ activestorage (= 6.1.0)
22
+ activesupport (= 6.1.0)
22
23
  mail (>= 2.7.1)
23
- actionmailer (6.0.3.1)
24
- actionpack (= 6.0.3.1)
25
- actionview (= 6.0.3.1)
26
- activejob (= 6.0.3.1)
24
+ actionmailer (6.1.0)
25
+ actionpack (= 6.1.0)
26
+ actionview (= 6.1.0)
27
+ activejob (= 6.1.0)
28
+ activesupport (= 6.1.0)
27
29
  mail (~> 2.5, >= 2.5.4)
28
30
  rails-dom-testing (~> 2.0)
29
- actionpack (6.0.3.1)
30
- actionview (= 6.0.3.1)
31
- activesupport (= 6.0.3.1)
32
- rack (~> 2.0, >= 2.0.8)
31
+ actionpack (6.1.0)
32
+ actionview (= 6.1.0)
33
+ activesupport (= 6.1.0)
34
+ rack (~> 2.0, >= 2.0.9)
33
35
  rack-test (>= 0.6.3)
34
36
  rails-dom-testing (~> 2.0)
35
37
  rails-html-sanitizer (~> 1.0, >= 1.2.0)
36
- actiontext (6.0.3.1)
37
- actionpack (= 6.0.3.1)
38
- activerecord (= 6.0.3.1)
39
- activestorage (= 6.0.3.1)
40
- activesupport (= 6.0.3.1)
38
+ actiontext (6.1.0)
39
+ actionpack (= 6.1.0)
40
+ activerecord (= 6.1.0)
41
+ activestorage (= 6.1.0)
42
+ activesupport (= 6.1.0)
41
43
  nokogiri (>= 1.8.5)
42
- actionview (6.0.3.1)
43
- activesupport (= 6.0.3.1)
44
+ actionview (6.1.0)
45
+ activesupport (= 6.1.0)
44
46
  builder (~> 3.1)
45
47
  erubi (~> 1.4)
46
48
  rails-dom-testing (~> 2.0)
47
49
  rails-html-sanitizer (~> 1.1, >= 1.2.0)
48
- activejob (6.0.3.1)
49
- activesupport (= 6.0.3.1)
50
+ activejob (6.1.0)
51
+ activesupport (= 6.1.0)
50
52
  globalid (>= 0.3.6)
51
- activemodel (6.0.3.1)
52
- activesupport (= 6.0.3.1)
53
- activerecord (6.0.3.1)
54
- activemodel (= 6.0.3.1)
55
- activesupport (= 6.0.3.1)
56
- activestorage (6.0.3.1)
57
- actionpack (= 6.0.3.1)
58
- activejob (= 6.0.3.1)
59
- activerecord (= 6.0.3.1)
53
+ activemodel (6.1.0)
54
+ activesupport (= 6.1.0)
55
+ activerecord (6.1.0)
56
+ activemodel (= 6.1.0)
57
+ activesupport (= 6.1.0)
58
+ activestorage (6.1.0)
59
+ actionpack (= 6.1.0)
60
+ activejob (= 6.1.0)
61
+ activerecord (= 6.1.0)
62
+ activesupport (= 6.1.0)
60
63
  marcel (~> 0.3.1)
61
- activesupport (6.0.3.1)
64
+ mimemagic (~> 0.3.2)
65
+ activesupport (6.1.0)
62
66
  concurrent-ruby (~> 1.0, >= 1.0.2)
63
- i18n (>= 0.7, < 2)
64
- minitest (~> 5.1)
65
- tzinfo (~> 1.1)
66
- zeitwerk (~> 2.2, >= 2.2.2)
67
+ i18n (>= 1.6, < 2)
68
+ minitest (>= 5.1)
69
+ tzinfo (~> 2.0)
70
+ zeitwerk (~> 2.3)
67
71
  ast (2.4.0)
68
72
  builder (3.2.4)
69
- cable_ready (4.1.2)
73
+ cable_ready (4.4.5)
70
74
  rails (>= 5.2)
71
75
  coderay (1.1.2)
72
- concurrent-ruby (1.1.6)
76
+ concurrent-ruby (1.1.7)
73
77
  crass (1.0.6)
74
- erubi (1.9.0)
78
+ erubi (1.10.0)
75
79
  globalid (0.4.2)
76
80
  activesupport (>= 4.2.0)
77
- i18n (1.8.2)
81
+ i18n (1.8.5)
78
82
  concurrent-ruby (~> 1.0)
79
83
  jaro_winkler (1.5.4)
80
- loofah (2.5.0)
84
+ loofah (2.8.0)
81
85
  crass (~> 1.0.2)
82
86
  nokogiri (>= 1.5.9)
83
87
  mail (2.7.1)
@@ -88,9 +92,9 @@ GEM
88
92
  mimemagic (0.3.5)
89
93
  mini_mime (1.0.2)
90
94
  mini_portile2 (2.4.0)
91
- minitest (5.14.1)
92
- nio4r (2.5.2)
93
- nokogiri (1.10.9)
95
+ minitest (5.14.2)
96
+ nio4r (2.5.4)
97
+ nokogiri (1.10.10-x64-mingw32)
94
98
  mini_portile2 (~> 2.4.0)
95
99
  parallel (1.19.1)
96
100
  parser (2.7.0.2)
@@ -100,35 +104,35 @@ GEM
100
104
  method_source (~> 0.9.0)
101
105
  pry-nav (0.3.0)
102
106
  pry (>= 0.9.10, < 0.13.0)
103
- rack (2.2.2)
107
+ rack (2.2.3)
104
108
  rack-test (1.1.0)
105
109
  rack (>= 1.0, < 3)
106
- rails (6.0.3.1)
107
- actioncable (= 6.0.3.1)
108
- actionmailbox (= 6.0.3.1)
109
- actionmailer (= 6.0.3.1)
110
- actionpack (= 6.0.3.1)
111
- actiontext (= 6.0.3.1)
112
- actionview (= 6.0.3.1)
113
- activejob (= 6.0.3.1)
114
- activemodel (= 6.0.3.1)
115
- activerecord (= 6.0.3.1)
116
- activestorage (= 6.0.3.1)
117
- activesupport (= 6.0.3.1)
118
- bundler (>= 1.3.0)
119
- railties (= 6.0.3.1)
110
+ rails (6.1.0)
111
+ actioncable (= 6.1.0)
112
+ actionmailbox (= 6.1.0)
113
+ actionmailer (= 6.1.0)
114
+ actionpack (= 6.1.0)
115
+ actiontext (= 6.1.0)
116
+ actionview (= 6.1.0)
117
+ activejob (= 6.1.0)
118
+ activemodel (= 6.1.0)
119
+ activerecord (= 6.1.0)
120
+ activestorage (= 6.1.0)
121
+ activesupport (= 6.1.0)
122
+ bundler (>= 1.15.0)
123
+ railties (= 6.1.0)
120
124
  sprockets-rails (>= 2.0.0)
121
125
  rails-dom-testing (2.0.3)
122
126
  activesupport (>= 4.2.0)
123
127
  nokogiri (>= 1.6)
124
128
  rails-html-sanitizer (1.3.0)
125
129
  loofah (~> 2.3)
126
- railties (6.0.3.1)
127
- actionpack (= 6.0.3.1)
128
- activesupport (= 6.0.3.1)
130
+ railties (6.1.0)
131
+ actionpack (= 6.1.0)
132
+ activesupport (= 6.1.0)
129
133
  method_source
130
134
  rake (>= 0.8.7)
131
- thor (>= 0.20.3, < 2.0)
135
+ thor (~> 1.0)
132
136
  rainbow (3.0.0)
133
137
  rake (13.0.1)
134
138
  rubocop (0.79.0)
@@ -141,10 +145,10 @@ GEM
141
145
  rubocop-performance (1.5.2)
142
146
  rubocop (>= 0.71.0)
143
147
  ruby-progressbar (1.10.1)
144
- sprockets (4.0.0)
148
+ sprockets (4.0.2)
145
149
  concurrent-ruby (~> 1.0)
146
150
  rack (> 1, < 3)
147
- sprockets-rails (3.2.1)
151
+ sprockets-rails (3.2.2)
148
152
  actionpack (>= 4.0)
149
153
  activesupport (>= 4.0)
150
154
  sprockets (>= 3.0.0)
@@ -154,17 +158,17 @@ GEM
154
158
  standardrb (1.0.0)
155
159
  standard
156
160
  thor (1.0.1)
157
- thread_safe (0.3.6)
158
- tzinfo (1.2.7)
159
- thread_safe (~> 0.1)
161
+ tzinfo (2.0.4)
162
+ concurrent-ruby (~> 1.0)
160
163
  unicode-display_width (1.6.1)
161
- websocket-driver (0.7.1)
164
+ websocket-driver (0.7.3)
162
165
  websocket-extensions (>= 0.1.0)
163
166
  websocket-extensions (0.1.5)
164
- zeitwerk (2.3.0)
167
+ zeitwerk (2.4.2)
165
168
 
166
169
  PLATFORMS
167
170
  ruby
171
+ x64-mingw32
168
172
 
169
173
  DEPENDENCIES
170
174
  bundler (~> 2.0)
data/README.md CHANGED
@@ -4,6 +4,10 @@
4
4
 
5
5
  The missing drop-in solution for realtime remote form validation in Rails.
6
6
 
7
+ {% hint style="success" %}
8
+ Optimism v0.4.1 now supports Ruby 3.0.0, thanks to [Julian Rubisch](https://twitter.com/julian_rubisch) and Rails 6.1, thanks to [Josh LeBlanc](https://github.com/joshleblanc). 🎉
9
+ {% endhint %}
10
+
7
11
  ## Why have optimism?
8
12
 
9
13
  [Optimism](https://github.com/leastbad/optimism) is an MIT-licensed [Ruby on Rails](https://rubyonrails.org/) gem that makes it easy to give your users instant constructive feedback if they enter invalid data into your application. Instead of dumping a list of errors at the top of your interface, Optimism provides specific instructions directly beside or below individual input elements.
data/lib/optimism.rb CHANGED
@@ -3,22 +3,19 @@ require "optimism/version"
3
3
  require "optimism/railtie" if defined?(Rails)
4
4
 
5
5
  module Optimism
6
- include CableReady::Broadcaster
7
- class << self
8
- mattr_accessor :channel, :form_class, :error_class, :disable_submit, :suffix, :emit_events, :add_css, :inject_inline, :container_selector, :error_selector, :form_selector, :submit_selector
9
- self.channel = ->(context) { "OptimismChannel" }
10
- self.form_class = "invalid"
11
- self.error_class = "error"
12
- self.disable_submit = false
13
- self.suffix = ""
14
- self.emit_events = false
15
- self.add_css = true
16
- self.inject_inline = true
17
- self.container_selector = "#RESOURCE_ATTRIBUTE_container"
18
- self.error_selector = "#RESOURCE_ATTRIBUTE_error"
19
- self.form_selector = "#RESOURCE_form"
20
- self.submit_selector = "#RESOURCE_submit"
21
- end
6
+ mattr_accessor :channel_proc, :form_class, :error_class, :disable_submit, :suffix, :emit_events, :add_css, :inject_inline, :container_selector, :error_selector, :form_selector, :submit_selector
7
+ self.channel_proc = ->(context) { "OptimismChannel" }
8
+ self.form_class = "invalid"
9
+ self.error_class = "error"
10
+ self.disable_submit = false
11
+ self.suffix = ""
12
+ self.emit_events = false
13
+ self.add_css = true
14
+ self.inject_inline = true
15
+ self.container_selector = "#RESOURCE_ATTRIBUTE_container"
16
+ self.error_selector = "#RESOURCE_ATTRIBUTE_error"
17
+ self.form_selector = "#RESOURCE_form"
18
+ self.submit_selector = "#RESOURCE_submit"
22
19
 
23
20
  def self.configure(&block)
24
21
  yield self
@@ -26,7 +23,7 @@ module Optimism
26
23
 
27
24
  def broadcast_errors(model, attributes)
28
25
  return unless model&.errors&.messages
29
- resource = model.class.to_s.underscore
26
+ resource = ActiveModel::Naming.param_key(model)
30
27
  form_selector, submit_selector = Optimism.form_selector.sub("RESOURCE", resource), Optimism.submit_selector.sub("RESOURCE", resource)
31
28
  attributes = case attributes
32
29
  when ActionController::Parameters, Hash, ActiveSupport::HashWithIndifferentAccess
@@ -41,15 +38,15 @@ module Optimism
41
38
  model.valid? if model.errors.empty?
42
39
  process_resource(model, attributes, [resource])
43
40
  if model.errors.any?
44
- cable_ready[Optimism.channel[self]].dispatch_event(name: "optimism:form:invalid", detail: {resource: resource}) if Optimism.emit_events
45
- cable_ready[Optimism.channel[self]].add_css_class(selector: form_selector, name: Optimism.form_class) if Optimism.form_class.present?
46
- cable_ready[Optimism.channel[self]].set_attribute(selector: submit_selector, name: "disabled") if Optimism.disable_submit
41
+ CableReady::Channels.instance[Optimism.channel_proc[self]].dispatch_event(name: "optimism:form:invalid", detail: {resource: resource}) if Optimism.emit_events
42
+ CableReady::Channels.instance[Optimism.channel_proc[self]].add_css_class(selector: form_selector, name: Optimism.form_class) if Optimism.form_class.present?
43
+ CableReady::Channels.instance[Optimism.channel_proc[self]].set_attribute(selector: submit_selector, name: "disabled") if Optimism.disable_submit
47
44
  else
48
- cable_ready[Optimism.channel[self]].dispatch_event(name: "optimism:form:valid", detail: {resource: resource}) if Optimism.emit_events
49
- cable_ready[Optimism.channel[self]].remove_css_class(selector: form_selector, name: Optimism.form_class) if Optimism.form_class.present?
50
- cable_ready[Optimism.channel[self]].remove_attribute(selector: submit_selector, name: "disabled") if Optimism.disable_submit
45
+ CableReady::Channels.instance[Optimism.channel_proc[self]].dispatch_event(name: "optimism:form:valid", detail: {resource: resource}) if Optimism.emit_events
46
+ CableReady::Channels.instance[Optimism.channel_proc[self]].remove_css_class(selector: form_selector, name: Optimism.form_class) if Optimism.form_class.present?
47
+ CableReady::Channels.instance[Optimism.channel_proc[self]].remove_attribute(selector: submit_selector, name: "disabled") if Optimism.disable_submit
51
48
  end
52
- cable_ready.broadcast
49
+ CableReady::Channels.instance.broadcast
53
50
  head :ok if defined?(head)
54
51
  end
55
52
 
@@ -81,21 +78,31 @@ module Optimism
81
78
  container_selector, error_selector = Optimism.container_selector.sub("RESOURCE", resource).sub("ATTRIBUTE", attribute), Optimism.error_selector.sub("RESOURCE", resource).sub("ATTRIBUTE", attribute)
82
79
  if model.errors.any? && model.errors.messages.map(&:first).include?(attribute.to_sym)
83
80
  message = "#{model.errors.full_message(attribute.to_sym, model.errors.messages[attribute.to_sym].first)}#{Optimism.suffix}"
84
- cable_ready[Optimism.channel[self]].dispatch_event(name: "optimism:attribute:invalid", detail: {resource: resource, attribute: attribute, text: message}) if Optimism.emit_events
85
- cable_ready[Optimism.channel[self]].add_css_class(selector: container_selector, name: Optimism.error_class) if Optimism.add_css
86
- cable_ready[Optimism.channel[self]].text_content(selector: error_selector, text: message) if Optimism.inject_inline
81
+ CableReady::Channels.instance[Optimism.channel_proc[self]].dispatch_event(name: "optimism:attribute:invalid", detail: {resource: resource, attribute: attribute, text: message}) if Optimism.emit_events
82
+ CableReady::Channels.instance[Optimism.channel_proc[self]].add_css_class(selector: container_selector, name: Optimism.error_class) if Optimism.add_css
83
+ CableReady::Channels.instance[Optimism.channel_proc[self]].text_content(selector: error_selector, text: message) if Optimism.inject_inline
87
84
  else
88
- cable_ready[Optimism.channel[self]].dispatch_event(name: "optimism:attribute:valid", detail: {resource: resource, attribute: attribute}) if Optimism.emit_events
89
- cable_ready[Optimism.channel[self]].remove_css_class(selector: container_selector, name: Optimism.error_class) if Optimism.add_css
90
- cable_ready[Optimism.channel[self]].text_content(selector: error_selector, text: "") if Optimism.inject_inline
85
+ CableReady::Channels.instance[Optimism.channel_proc[self]].dispatch_event(name: "optimism:attribute:valid", detail: {resource: resource, attribute: attribute}) if Optimism.emit_events
86
+ CableReady::Channels.instance[Optimism.channel_proc[self]].remove_css_class(selector: container_selector, name: Optimism.error_class) if Optimism.add_css
87
+ CableReady::Channels.instance[Optimism.channel_proc[self]].text_content(selector: error_selector, text: "") if Optimism.inject_inline
91
88
  end
92
89
  end
93
90
  end
94
91
 
95
92
  module ActionView::Helpers
93
+ module FormHelper
94
+ def error_for(object_name, attribute, **options)
95
+ tag.span **options.merge(id: error_id_for(object_name, attribute))
96
+ end
97
+
98
+ def error_id_for(object_name, attribute)
99
+ Optimism.error_selector.sub("RESOURCE", object_name.to_s.delete("]").tr("[", "_")).sub("ATTRIBUTE", attribute.to_s)[1..-1]
100
+ end
101
+ end
102
+
96
103
  class FormBuilder
97
104
  def container_for(attribute, **options, &block)
98
- @template.tag.div @template.capture(&block), options.merge!(id: container_id_for(attribute)) if block_given?
105
+ @template.tag.div @template.capture(&block), **options.merge(id: container_id_for(attribute)) if block_given?
99
106
  end
100
107
 
101
108
  def container_id_for(attribute)
@@ -103,11 +110,7 @@ module ActionView::Helpers
103
110
  end
104
111
 
105
112
  def error_for(attribute, **options)
106
- @template.tag.span options.merge! id: error_id_for(attribute)
107
- end
108
-
109
- def error_id_for(attribute)
110
- Optimism.error_selector.sub("RESOURCE", object_name.to_s.delete("]").tr("[", "_")).sub("ATTRIBUTE", attribute.to_s)[1..-1]
113
+ @template.error_for(object_name, attribute, **options)
111
114
  end
112
115
  end
113
116
  end
@@ -1,3 +1,3 @@
1
1
  module Optimism
2
- VERSION = "0.3.3"
2
+ VERSION = "0.4.2"
3
3
  end
data/optimism.gemspec CHANGED
@@ -26,6 +26,6 @@ Gem::Specification.new do |spec|
26
26
  spec.add_development_dependency "pry-nav", "~> 0.3.0"
27
27
  spec.add_development_dependency "standardrb", "~> 1.0.0"
28
28
  spec.add_dependency "rack", "~> 2.0"
29
- spec.add_dependency "rails", [">= 5.2", ">= 6"]
30
- spec.add_dependency "cable_ready", ">= 4"
29
+ spec.add_dependency "rails", [">= 5.2"]
30
+ spec.add_dependency "cable_ready", ">= 4.4.6"
31
31
  end
data/reference.md CHANGED
@@ -93,7 +93,7 @@ Optimism is configurable via an optional initializer file. As with all initializ
93
93
  {% code title="config/initializers/optimism.rb" %}
94
94
  ```ruby
95
95
  Optimism.configure do |config|
96
- config.channel = ->(context) { "OptimismChannel" }
96
+ config.channel_proc = ->(context) { "OptimismChannel" }
97
97
  config.form_class = "invalid"
98
98
  config.error_class = "error"
99
99
  config.disable_submit = false
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: optimism
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - leastbad
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-07 00:00:00.000000000 Z
11
+ date: 2021-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -101,9 +101,6 @@ dependencies:
101
101
  - - ">="
102
102
  - !ruby/object:Gem::Version
103
103
  version: '5.2'
104
- - - ">="
105
- - !ruby/object:Gem::Version
106
- version: '6'
107
104
  type: :runtime
108
105
  prerelease: false
109
106
  version_requirements: !ruby/object:Gem::Requirement
@@ -111,23 +108,20 @@ dependencies:
111
108
  - - ">="
112
109
  - !ruby/object:Gem::Version
113
110
  version: '5.2'
114
- - - ">="
115
- - !ruby/object:Gem::Version
116
- version: '6'
117
111
  - !ruby/object:Gem::Dependency
118
112
  name: cable_ready
119
113
  requirement: !ruby/object:Gem::Requirement
120
114
  requirements:
121
115
  - - ">="
122
116
  - !ruby/object:Gem::Version
123
- version: '4'
117
+ version: 4.4.6
124
118
  type: :runtime
125
119
  prerelease: false
126
120
  version_requirements: !ruby/object:Gem::Requirement
127
121
  requirements:
128
122
  - - ">="
129
123
  - !ruby/object:Gem::Version
130
- version: '4'
124
+ version: 4.4.6
131
125
  description: Realtime remote form input validations delivered via websockets
132
126
  email:
133
127
  - hello@leastbad.com
@@ -189,7 +183,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
189
183
  - !ruby/object:Gem::Version
190
184
  version: '0'
191
185
  requirements: []
192
- rubygems_version: 3.0.6
186
+ rubygems_version: 3.1.4
193
187
  signing_key:
194
188
  specification_version: 4
195
189
  summary: Drop-in Rails form validations