gourami 2.0.0 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0cc34cc23b4323b885cb63b0c82cd98921e0467fac34a97fcc584550f85de29d
4
- data.tar.gz: '0737019b3dcb2cc40567287a94f4a3bd9dc13ef3b32a28e0e140b0745cc5a29e'
3
+ metadata.gz: 79b225001fdeb796c9a6224beb9c7c792f37ae0ae7f48be33a2a4ca27f938979
4
+ data.tar.gz: 9fa37364de4f3c646f51f864798029564629a012e16b85877822629d579650a4
5
5
  SHA512:
6
- metadata.gz: fb5e34109e4c24fd1394e8888acee9d2a0ff99542fb9aadd9072f6e81ca9c7cb65f05d65ba7dfb505ea3ba520f803a9eceeaebd2b0fc2260bae740ce7ea60246
7
- data.tar.gz: 55a3dffab397480607dfa1bb0edbee3b5d17a52c859e855423aec56b349a1a15542e47238b45c3edb5740d3c3680f0e363767da40e0edf7f93835c9f884f1b72
6
+ metadata.gz: 3b421bf22515e7380abf2a247e74e057ba26eed52d29dc7b2a8f2a13a0f2f72608f0ae6a2c1b0ca5ea4138a4b756b73cb8d9cf234f0bb416237c63c866b06f59
7
+ data.tar.gz: f7dd343730a9503d267f3c39ebb7b769eb62347283a69e5848f0a8e7766b1dcb2da5707eb0b71251eb0f39bdddce47d0fcee212493e811a06618bdbc20be2618
@@ -0,0 +1,5 @@
1
+ {
2
+ "cSpell.words": [
3
+ "uids"
4
+ ]
5
+ }
data/gourami.gemspec CHANGED
@@ -23,6 +23,7 @@ Gem::Specification.new do |spec|
23
23
 
24
24
  spec.add_development_dependency "activesupport", ">= 5.1.7"
25
25
  spec.add_development_dependency "filewatcher", "~> 1.1.0"
26
+ spec.add_development_dependency "loofah", ">= 2.24.0"
26
27
  spec.add_development_dependency "minitest", "~> 5.0"
27
28
  spec.add_development_dependency "pry", "~>0.10"
28
29
  spec.add_development_dependency "rake", "~> 10.0"
@@ -11,7 +11,12 @@ module Gourami
11
11
  # @return [*]
12
12
  def setter_filter(attribute_name, value, options)
13
13
  type = options[:type]
14
- value = send(:"coerce_#{type}", value, options) if type
14
+
15
+ if type
16
+ raise ":coerce_#{type} does not exist. Did you forget to add a plugin for :coerce_#{type}?" unless respond_to?(:"coerce_#{type}")
17
+
18
+ value = send(:"coerce_#{type}", value, options)
19
+ end
15
20
 
16
21
  super(attribute_name, value, options)
17
22
  end
@@ -72,7 +72,7 @@ module Gourami
72
72
  # validate_presence(:trim_end_time) # validates `attributes[:social_broadcasts]["facebook_page-41"][:trim_end_time]`
73
73
  # end
74
74
  # end
75
- def with_resource(resource_namespace, resource_uid, offset: nil, &_block)
75
+ def with_resource(resource_namespace, resource_uid = nil, offset: nil, &_block)
76
76
  @resource_namespace = resource_namespace
77
77
  @resource_uid = resource_uid
78
78
  @offset = offset
@@ -86,8 +86,10 @@ module Gourami
86
86
  # If a resource namespace is active (within with_resource block), find the resource using the namespace and uid.
87
87
  # Otherwise, return the form object.
88
88
  def current_resource
89
- if @resource_namespace
89
+ if @resource_namespace && @resource_uid
90
90
  send(@resource_namespace)[@resource_uid]
91
+ elsif @resource_namespace
92
+ send(@resource_namespace)
91
93
  else
92
94
  super
93
95
  end
@@ -156,13 +158,21 @@ module Gourami
156
158
  end
157
159
 
158
160
  # TODO: YARD
159
- def resource_has_errors?(resource_namespace, resource_uid)
160
- resource_errors[resource_namespace][resource_uid.to_s].values.map(&:flatten).any?
161
+ def resource_has_errors?(resource_namespace, resource_uid = nil)
162
+ resource_errors[resource_namespace][resource_uid&.to_s].values.map(&:flatten).any?
161
163
  end
162
164
 
163
165
  # TODO: YARD
164
- def resource_attribute_has_errors?(resource_namespace, resource_uid, attribute_name)
165
- resource_errors[resource_namespace][resource_uid.to_s][attribute_name].any?
166
+ def resource_attribute_has_errors?(resource_namespace, resource_uid_or_attribute_name, attribute_name = nil)
167
+ if attribute_name.nil?
168
+ # 2 arguments: resource_namespace, attribute_name
169
+ attribute_name = resource_uid_or_attribute_name
170
+ resource_uid = nil
171
+ else
172
+ # 3 arguments: resource_namespace, resource_uid, attribute_name
173
+ resource_uid = resource_uid_or_attribute_name
174
+ end
175
+ resource_errors[resource_namespace][resource_uid&.to_s][attribute_name].any?
166
176
  end
167
177
 
168
178
  # Append an error to the given attribute for a resource.
@@ -174,7 +184,7 @@ module Gourami
174
184
  # @param error [Symbol, String]
175
185
  # The error identifier.
176
186
  def append_resource_error(resource_namespace, resource_uid, attribute_name, error)
177
- resource_errors[resource_namespace][resource_uid.to_s][attribute_name] << error
187
+ resource_errors[resource_namespace][resource_uid&.to_s][attribute_name] << error
178
188
  end
179
189
 
180
190
  # Determine if current form instance is valid by running the validations
data/lib/gourami/form.rb CHANGED
@@ -88,5 +88,35 @@ module Gourami
88
88
  include Gourami::Validations
89
89
  include Gourami::Coercer
90
90
 
91
+ # Load and apply a plugin.
92
+ # Inspired by the plugin system from the Sequel gem. https://janko.io/the-plugin-system-of-sequel-and-roda/
93
+ #
94
+ # @param name [Symbol, Module] a name to look up under Gourami::Plugins
95
+ # (e.g. :sanitize loads gourami/plugins/sanitize and resolves
96
+ # Gourami::Plugins::Sanitize), or a Module to apply directly.
97
+ def self.plugin(name, *args, &block)
98
+ plugin_module = load_plugin(name)
99
+
100
+ plugin_module.apply(self, *args, &block) if plugin_module.respond_to?(:apply)
101
+
102
+ extend(plugin_module::ClassMethods) if plugin_module.const_defined?(:ClassMethods)
103
+ include(plugin_module::InstanceMethods) if plugin_module.const_defined?(:InstanceMethods)
104
+
105
+ plugin_module.configure(self, *args, &block) if plugin_module.respond_to?(:configure)
106
+ end
107
+
108
+ def self.load_plugin(name)
109
+ return name if name.is_a?(Module)
110
+
111
+ const_name = name.to_s.split("_").map { |part| part[0].upcase + part[1..-1] }.join
112
+
113
+ unless Gourami::Plugins.const_defined?(const_name)
114
+ require "gourami/plugins/#{name}"
115
+ end
116
+
117
+ Gourami::Plugins.const_get(const_name)
118
+ end
119
+ private_class_method :load_plugin
120
+
91
121
  end
92
122
  end
@@ -0,0 +1,37 @@
1
+ # Optional plugin adding a :sanitized_string attribute type that strips dangerous HTML
2
+ # using loofah. Not loaded by default - opt in with `plugin :sanitize`, and add
3
+ # "loofah" to your own application's Gemfile/gemspec.
4
+ #
5
+ # @example
6
+ # class MyForm < Gourami::Form
7
+ # plugin :sanitize
8
+ #
9
+ # attribute :bio, :type => :sanitized_string
10
+ # end
11
+ module Gourami
12
+ module Plugins
13
+ module Sanitize
14
+ # Runs on every `plugin :sanitize` call; require is idempotent so this is safe to repeat.
15
+ def self.apply(_model, *)
16
+ require "loofah"
17
+ end
18
+
19
+ module InstanceMethods
20
+ # @param value [Object]
21
+ # @param options [Hash] forwarded to #coerce_string
22
+ # @return [String, nil]
23
+ def coerce_sanitized_string(value, options = {})
24
+ value = coerce_string(value, options)
25
+ return value if value.nil?
26
+
27
+ # Unescape first so entity-encoded tags (e.g. "&lt;script&gt;") are
28
+ # recognized as real HTML by Loofah instead of passing through as text.
29
+ unescaped_content = CGI.unescapeHTML(value)
30
+ sanitized_content = Loofah.html4_fragment(unescaped_content).scrub!(:prune).to_s
31
+ # Unescape again since loofah would turn a & back into &amp;
32
+ CGI.unescapeHTML(sanitized_content).strip
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,6 @@
1
+ module Gourami
2
+ # Namespace that plugins live under, e.g. Gourami::Plugins::Sanitize is
3
+ # loaded and applied via `plugin :sanitize`.
4
+ module Plugins
5
+ end
6
+ end
@@ -1,3 +1,3 @@
1
1
  module Gourami
2
- VERSION = "2.0.0".freeze
2
+ VERSION = "2.2.0".freeze
3
3
  end
data/lib/gourami.rb CHANGED
@@ -15,5 +15,6 @@ require "gourami/extensions/changes"
15
15
  require "gourami/extensions/resources"
16
16
  require "gourami/form"
17
17
  require "gourami/formatting_constants"
18
+ require "gourami/plugins"
18
19
  require "gourami/validations"
19
20
  require "gourami/version"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gourami
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - TSMMark
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-05-22 00:00:00.000000000 Z
11
+ date: 2026-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: 1.1.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: loofah
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 2.24.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 2.24.0
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: minitest
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -91,6 +105,7 @@ files:
91
105
  - ".github/workflows/ci.yml"
92
106
  - ".gitignore"
93
107
  - ".ruby-version"
108
+ - ".vscode/settings.json"
94
109
  - CODE_OF_CONDUCT.md
95
110
  - Gemfile
96
111
  - LICENSE.txt
@@ -110,6 +125,8 @@ files:
110
125
  - lib/gourami/form.rb
111
126
  - lib/gourami/formatting_constants.rb
112
127
  - lib/gourami/not_watching_changes_error.rb
128
+ - lib/gourami/plugins.rb
129
+ - lib/gourami/plugins/sanitize.rb
113
130
  - lib/gourami/required_attribute_error.rb
114
131
  - lib/gourami/validation_error.rb
115
132
  - lib/gourami/validations.rb
@@ -134,7 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
134
151
  - !ruby/object:Gem::Version
135
152
  version: '0'
136
153
  requirements: []
137
- rubygems_version: 3.4.12
154
+ rubygems_version: 3.4.21
138
155
  signing_key:
139
156
  specification_version: 4
140
157
  summary: Keep your Routes, Controllers and Models thin.