gourami 0.5.0 → 1.3.1

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: 98ec9c2d757ec710e65dd5aa0662530f0f92363b72d56edce9fecebe85d0d800
4
- data.tar.gz: 80842660cd5e2ed28a244414ef8a8f8f091d111d8fd2ba614bcec52dc0efe807
3
+ metadata.gz: 471278c6d8c858485bdec0d124139f3ccd4251eedbad1573059dcf4519ec9b0a
4
+ data.tar.gz: 2e62de42a3eb36e05cd247bed62ccb7f7bfecb796318a1b0c5dfeb79ce6b28bf
5
5
  SHA512:
6
- metadata.gz: 94897c3b427a7528020c6df8b657c193e88488b52610bf853d2e3fffda962ba1e0ba642ba7c72e2c9bb6ecf234518372910f733bca453d72a2b72c4ee469cb55
7
- data.tar.gz: 12d3a6215c2fc337aa03af6d0818b916879aebe77cb96a21aed65d47ca7385a68eb096cc81dc22ff39ca3e187260ffd0a0f55060a4fb38db948eeb5a673a0e87
6
+ metadata.gz: '09d6304054c99cbf3587005dfc38c8fef568dff2339cfc1ae41642bacbf4c80da1a91c54bc435b90253ac01d7c72927f516b945838db698d33df7250bb686499'
7
+ data.tar.gz: e33e55074af37d7776329feb8a491687ab43dea237d5ce96cd4dd9ff1d80f6a580b43b6015021c69a9444d48f788bcd1bd16c512f07da4f6377f3a19d0682ea8
@@ -0,0 +1 @@
1
+ 2.5.3
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Gourami
2
2
 
3
+ [![Codeship Status for Vydia/gourami](https://app.codeship.com/projects/316bc070-f431-0136-4713-52c1ec7c066f/status?branch=master)](https://app.codeship.com/projects/320673)
4
+
3
5
  Keep your Routes, Controllers and Models thin with Plain Old Ruby Objects (PORO).
4
6
 
5
7
  ## Installation
@@ -12,7 +14,7 @@ gem 'gourami'
12
14
 
13
15
  And then execute:
14
16
 
15
- $ bundle
17
+ $ bundle install
16
18
 
17
19
  Or install it yourself as:
18
20
 
@@ -20,10 +22,10 @@ Or install it yourself as:
20
22
 
21
23
  ## Usage
22
24
 
23
- ### A Typical Gourami::Form will
25
+ ### A Typical `Gourami::Form` will
24
26
 
25
- - Define some attributes
26
- - Validate user input
27
+ - Define attributes (inputs & outputs)
28
+ - Validate input
27
29
  - Perform an action
28
30
 
29
31
  ```ruby
@@ -173,6 +175,56 @@ class UpdateFishBowl < CreateFishBowl
173
175
  end
174
176
  ```
175
177
 
178
+ #### Configure default attribute options
179
+
180
+ The following examples will result in all `:string` attributes getting the options `:strip` and `:upcase` set to `true`.
181
+
182
+ Set global defaults:
183
+
184
+ ```ruby
185
+ Gourami::Form.set_default_attribute_options(:string, upcase: true)
186
+
187
+ # Make sure to define CreateFishBowl and other forms AFTER setting default options.
188
+ class CreateFishBowl < Gourami::Form
189
+ attribute(:name, type: :string)
190
+ end
191
+
192
+ form = CreateFishBowl.new(name: "Snake Gyllenhaal")
193
+ form.name # => "SNAKE GYLLENHAAL"
194
+ ```
195
+
196
+ Instead of global defaults, you can also apply defaults to certain form classes.
197
+
198
+ Just as `attributes` are inherited by subclasses, so are `default_attribute_options`.
199
+
200
+ Set local defaults:
201
+
202
+ ```ruby
203
+ class ScreamingForm < Gourami::Form
204
+ set_default_attribute_options(:string, upcase: true)
205
+ end
206
+
207
+ class CreateScreamingFish < ScreamingForm
208
+ attribute(:name, type: :string)
209
+ end
210
+
211
+ class UpdateScreamingFish < CreateScreamingFish; end
212
+
213
+ create_form = CreateScreamingFish.new(name: "Snake Gyllenhaal")
214
+ create_form.name # => "SNAKE GYLLENHAAL"
215
+
216
+ update_form = UpdateScreamingFish.new(name: "Snake Gyllenhaal")
217
+ update_form.name # => "SNAKE GYLLENHAAL"
218
+
219
+ # Other Gourami::Forms are unaffected
220
+ class RegularForm < Gourami::Form
221
+ attribute(:name, type: :string)
222
+ end
223
+
224
+ regular_form = RegularForm.new(name: "Snake Gyllenhaal")
225
+ regular_form.name # => "Snake Gyllenhaal"
226
+ ```
227
+
176
228
  #### Extensions / Plugins
177
229
 
178
230
  ##### Gourami::Extensions::Changes
@@ -250,9 +302,13 @@ end
250
302
 
251
303
  ## Development
252
304
 
253
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
305
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests, or `rake test:watch` to automatically rerun the tests when you make code changes. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
306
+
307
+ To install this gem onto your local machine, run `bundle exec rake install`.
308
+
309
+ To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
254
310
 
255
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
311
+ To add another gem owner to gourami gem `gem owner --add john.smith@example.com gourami`
256
312
 
257
313
  ## Contributing
258
314
 
data/Rakefile CHANGED
@@ -8,4 +8,18 @@ Rake::TestTask.new(:test) do |t|
8
8
  end
9
9
 
10
10
  task :spec => :test
11
+ namespace :test do
12
+ task :watch do |t, args|
13
+ require "filewatcher"
14
+
15
+ watcher = Filewatcher.new(["spec/", "lib/"], :every => true, :spinner => true, :immediate => true)
16
+ watcher.watch do |filename, event|
17
+ begin
18
+ Rake::Task[:test].execute(args)
19
+ rescue StandardError => error
20
+ puts "Error: #{error.message}"
21
+ end
22
+ end
23
+ end
24
+ end
11
25
  task :default => :test
@@ -21,8 +21,9 @@ Gem::Specification.new do |spec|
21
21
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
22
  spec.require_paths = ["lib"]
23
23
 
24
+ spec.add_development_dependency "activesupport", ">= 5.1.7"
25
+ spec.add_development_dependency "filewatcher", "~> 1.1.0"
26
+ spec.add_development_dependency "minitest", "~> 5.0"
24
27
  spec.add_development_dependency "pry", "~>0.10"
25
- spec.add_development_dependency "bundler", "~> 1.13"
26
28
  spec.add_development_dependency "rake", "~> 10.0"
27
- spec.add_development_dependency "minitest", "~> 5.0"
28
29
  end
@@ -2,6 +2,7 @@ module Gourami
2
2
  end
3
3
 
4
4
  require "gourami/error"
5
+ require "gourami/attribute_name_conflict_error"
5
6
  require "gourami/configuration_error"
6
7
  require "gourami/not_watching_changes_error"
7
8
  require "gourami/required_attribute_error"
@@ -0,0 +1,4 @@
1
+ module Gourami
2
+ class AttributeNameConflictError < Gourami::Error
3
+ end
4
+ end
@@ -2,12 +2,14 @@ module Gourami
2
2
  module Attributes
3
3
 
4
4
  module ClassMethods
5
+
5
6
  # Copy parent attributes to inheriting class.
6
7
  #
7
8
  # @param klass [Class]
8
9
  def inherited(klass)
9
10
  super(klass)
10
11
  klass.instance_variable_set(:@attributes, attributes.dup)
12
+ klass.instance_variable_set(:@default_attribute_options, default_attribute_options.dup)
11
13
  end
12
14
 
13
15
  # Define an attribute for the form.
@@ -20,14 +22,21 @@ module Gourami
20
22
  # @block default_block
21
23
  # If provided, the block will be applied to options as the :default
22
24
  def attribute(name, options = {}, &default_block)
25
+ base = self
23
26
  options = options.dup
24
27
  options[:default] = default_block if block_given?
25
28
 
29
+ options_with_defaults = merge_default_attribute_options(options)
30
+
26
31
  mixin = Module.new do |mixin|
27
- unless options[:skip_reader]
32
+ unless options_with_defaults[:skip_reader]
33
+ if !base.attributes.key?(name) && base.instance_methods.include?(name) && !options_with_defaults[:override_reader]
34
+ raise AttributeNameConflictError, "#{name} is already a method. To use the existing method, use `:skip_reader => true` option. To override the existing method, use `:override_reader => true` option."
35
+ end
36
+
28
37
  mixin.send(:define_method, :"#{name}") do
29
38
  value = instance_variable_get(:"@#{name}")
30
- default = options[:default]
39
+ default = options_with_defaults[:default]
31
40
 
32
41
  if value.nil? && default
33
42
  default.respond_to?(:call) ? instance_exec(&default) : default
@@ -45,7 +54,7 @@ module Gourami
45
54
 
46
55
  # Define internal setter.
47
56
  mixin.send(:define_method, :"_#{name}=") do |value|
48
- instance_variable_set(:"@#{name}", setter_filter(name, value, options))
57
+ instance_variable_set(:"@#{name}", setter_filter(name, value, self.class.merge_default_attribute_options(options)))
49
58
  end
50
59
  mixin.send(:private, :"_#{name}=")
51
60
 
@@ -84,6 +93,24 @@ module Gourami
84
93
  def attributes
85
94
  @attributes ||= {}
86
95
  end
96
+
97
+ # Useful if you want, for example, all type: :string attributes to use
98
+ # strip: true to remove whitespace padding.
99
+ def set_default_attribute_options(attr_type, options)
100
+ default_attribute_options[attr_type] = options
101
+ end
102
+
103
+ def default_attribute_options
104
+ @default_attribute_options ||= {}
105
+ end
106
+
107
+ def merge_default_attribute_options(options)
108
+ if options[:type]
109
+ default_attribute_options.fetch(options[:type], {}).merge(options)
110
+ else
111
+ options
112
+ end
113
+ end
87
114
  end
88
115
 
89
116
  # Extend ClassMethods into including class.
@@ -11,8 +11,7 @@ module Gourami
11
11
  # @return [*]
12
12
  def setter_filter(attribute_name, value, options)
13
13
  type = options[:type]
14
- coercer_method_name = :"coerce_#{type}"
15
- value = send(coercer_method_name, value, options) if type
14
+ value = send(:"coerce_#{type}", value, options) if type
16
15
 
17
16
  super(attribute_name, value, options)
18
17
  end
@@ -34,6 +33,9 @@ module Gourami
34
33
  end
35
34
 
36
35
  value = value.to_s.dup.force_encoding(Encoding::UTF_8)
36
+
37
+ # TODO: Instead of providing unconfigurable defaults like this, use
38
+ # set_default_attribute_options at the gem level or consumer level.
37
39
  value.strip! if options.fetch(:strip, true)
38
40
  value.upcase! if options.fetch(:upcase, false)
39
41
 
@@ -85,6 +87,9 @@ module Gourami
85
87
  element_type_options = {}
86
88
  end
87
89
 
90
+ element_type_options[:type] = element_type
91
+ element_type_options = self.class.merge_default_attribute_options(element_type_options) if self.class.respond_to?(:merge_default_attribute_options)
92
+
88
93
  coercer_method_name = :"coerce_#{element_type}"
89
94
 
90
95
  value.map do |array_element|
@@ -120,21 +125,41 @@ module Gourami
120
125
  # The type of the hash keys to coerce, no coersion if value is nil.
121
126
  # @option options :value_type [Symbol, Callable] (nil)
122
127
  # The type of the hash values to coerce, no coersion if value is nil.
128
+ # @option options :indifferent_access [Boolean] (false)
129
+ # When true, the resulting Hash will be an ActiveSupport::HashWithIndifferentAccess
123
130
  #
124
- # @return [Hash]
131
+ # @return [Hash, ActiveSupport::HashWithIndifferentAccess]
125
132
  # The coerced Hash.
126
133
  def coerce_hash(value, options = {})
134
+ return if options[:allow_nil] && value.nil?
135
+
127
136
  hash_key_type = options[:key_type]
128
137
  hash_value_type = options[:value_type]
129
138
 
130
- return {} unless value.is_a?(Hash) || (defined?(Sequel::Postgres::JSONHash) && value.is_a?(Sequel::Postgres::JSONHash))
139
+ hash_class = options[:indifferent_access] ? ActiveSupport::HashWithIndifferentAccess : Hash
140
+ hash = hash_class.new
141
+
142
+ return hash unless value.is_a?(Hash) || (defined?(Sequel::Postgres::JSONHash) && value.is_a?(Sequel::Postgres::JSONHash))
131
143
 
132
- value.each_with_object({}) do |(key, value), coerced_hash|
144
+ value.each_with_object(hash) do |(key, value), coerced_hash|
133
145
  key_type = hash_key_type.respond_to?(:call) ? hash_key_type.call(key, value) : hash_key_type
134
- key = send("coerce_#{key_type}", key) if key_type
146
+ key = send(:"coerce_#{key_type}", key) if key_type
135
147
 
136
148
  value_type = hash_value_type.respond_to?(:call) ? hash_value_type.call(key, value) : hash_value_type
137
- value = send("coerce_#{value_type}", value) if value_type
149
+
150
+ # TODO: Refactor shared logic here and coerce_array to a method like `type, options = resolve_coercer_type_and_options`
151
+ if value_type.is_a?(Hash)
152
+ value_type_options = value_type
153
+ value_type = value_type[:type]
154
+ else
155
+ value_type_options = {}
156
+ end
157
+
158
+ value_type_options[:type] = value_type
159
+ value_type_options = self.class.merge_default_attribute_options(value_type_options) if self.class.respond_to?(:merge_default_attribute_options)
160
+
161
+ value = send(:"coerce_#{value_type}", value, value_type_options) if value_type
162
+
138
163
  coerced_hash[key] = value
139
164
  end
140
165
  end
@@ -91,6 +91,28 @@ module Gourami
91
91
  resource_errors.values.flat_map(&:values).map(&:values).flatten.any?
92
92
  end
93
93
 
94
+ # Replace the existing resource errors with the provided errors Hash.
95
+ #
96
+ # @param new_resource_errors [Hash<Symbol, Hash<Symbol, Hash<Symbol, Array>>>]
97
+ #
98
+ # @return [Hash<Symbol, Hash<Symbol, Hash<Symbol, Array>>>]
99
+ def clear_and_set_resource_errors(new_resource_errors)
100
+ new_resource_errors = new_resource_errors.dup
101
+ resource_errors.clear
102
+ resource_errors.merge!(new_resource_errors)
103
+
104
+ resource_errors
105
+ end
106
+
107
+ def handle_validation_error(error)
108
+ super(error)
109
+ clear_and_set_resource_errors(error.resource_errors) unless error.resource_errors.nil?
110
+ end
111
+
112
+ def raise_validate_errors
113
+ raise ValidationError.new(errors, resource_errors)
114
+ end
115
+
94
116
  end
95
117
  end
96
118
  end
@@ -9,20 +9,39 @@ module Gourami
9
9
  end
10
10
  end
11
11
 
12
+ def self.stringify_resource_errors(resource_errors)
13
+ [].tap do |array|
14
+ resource_errors.each do |resource_namespace, resource_namespace_errors|
15
+ resource_namespace_errors.each do |resource_uid, resource_uid_errors|
16
+ resource_uid_errors.each do |attribute_name, error|
17
+ array.push("#{resource_namespace}:#{resource_uid}:#{attribute_name}: #{error}")
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
12
24
  # !@attribute [r] errors
13
25
  # @return [Hash<Symbol, Array>]
14
26
  attr_reader :errors
15
27
 
28
+ # !@attribute [r] resource_errors
29
+ # @return [Hash<Symbol, Hash<Symbol, Hash<Symbol, Array>>>]
30
+ attr_reader :resource_errors
31
+
16
32
  # Initialize the Gourami::ValidationError.
17
33
  #
18
34
  # @param errors [Hash<Symbol, Array>]
19
- def initialize(errors)
35
+ # @param resource_errors [Hash<Symbol, Hash<Symbol, Hash<Symbol, Array>>>]
36
+ def initialize(errors, resource_errors = {})
37
+ @resource_errors = resource_errors
20
38
  @errors = errors
39
+
21
40
  super(message)
22
41
  end
23
42
 
24
43
  def message
25
- @message ||= "Validation failed with errors: #{stringify_errors.join("\n")}"
44
+ @message ||= stringify_all_errors
26
45
  end
27
46
 
28
47
  private
@@ -31,5 +50,16 @@ module Gourami
31
50
  ValidationError.stringify_errors(errors)
32
51
  end
33
52
 
53
+ def stringify_resource_errors
54
+ ValidationError.stringify_resource_errors(resource_errors)
55
+ end
56
+
57
+ def stringify_all_errors
58
+ messages = []
59
+ messages << "Validation failed with errors: #{stringify_errors.join("\n")}" unless errors.nil? || errors.empty?
60
+ messages << "Validation failed with resource errors: #{stringify_resource_errors.join("\n")}" unless resource_errors.nil? || resource_errors.empty?
61
+ messages.join("\n")
62
+ end
63
+
34
64
  end
35
65
  end
@@ -15,11 +15,16 @@ module Gourami
15
15
  # @raise [Gourami::ValidationError]
16
16
  def perform!
17
17
  if valid?
18
- returned = perform
18
+ begin
19
+ returned = perform
20
+ rescue Gourami::ValidationError => error
21
+ handle_validation_error(error)
22
+ raise
23
+ end
19
24
  end
20
25
 
21
26
  if any_errors?
22
- raise ValidationError.new(errors)
27
+ raise_validate_errors
23
28
  end
24
29
 
25
30
  returned
@@ -46,9 +51,9 @@ module Gourami
46
51
 
47
52
  # Replace the existing errors with the provided errors Hash.
48
53
  #
49
- # @param new_errors [Hash<Symbol, nil>, Array<Symbol, String>]
54
+ # @param new_errors Hash<Symbol, Array>
50
55
  #
51
- # @return [Hash<Symbol, nil>, Array<Symbol, String>]
56
+ # @return Hash<Symbol, Array>
52
57
  def clear_and_set_errors(new_errors)
53
58
  new_errors = new_errors.dup
54
59
  errors.clear
@@ -57,6 +62,14 @@ module Gourami
57
62
  errors
58
63
  end
59
64
 
65
+ def raise_validate_errors
66
+ raise ValidationError.new(errors)
67
+ end
68
+
69
+ def handle_validation_error(error)
70
+ clear_and_set_errors(error.errors) unless error.errors.nil?
71
+ end
72
+
60
73
  # Return true if there given attribute has any errors.
61
74
  def attribute_has_errors?(attribute_name)
62
75
  errors[attribute_name.to_sym].any?
@@ -1,3 +1,3 @@
1
1
  module Gourami
2
- VERSION = "0.5.0".freeze
2
+ VERSION = "1.3.1".freeze
3
3
  end
metadata CHANGED
@@ -1,71 +1,85 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gourami
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - TSMMark
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-10-30 00:00:00.000000000 Z
11
+ date: 2020-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: pry
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 5.1.7
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 5.1.7
27
+ - !ruby/object:Gem::Dependency
28
+ name: filewatcher
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
31
  - - "~>"
18
32
  - !ruby/object:Gem::Version
19
- version: '0.10'
33
+ version: 1.1.0
20
34
  type: :development
21
35
  prerelease: false
22
36
  version_requirements: !ruby/object:Gem::Requirement
23
37
  requirements:
24
38
  - - "~>"
25
39
  - !ruby/object:Gem::Version
26
- version: '0.10'
40
+ version: 1.1.0
27
41
  - !ruby/object:Gem::Dependency
28
- name: bundler
42
+ name: minitest
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - "~>"
32
46
  - !ruby/object:Gem::Version
33
- version: '1.13'
47
+ version: '5.0'
34
48
  type: :development
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
52
  - - "~>"
39
53
  - !ruby/object:Gem::Version
40
- version: '1.13'
54
+ version: '5.0'
41
55
  - !ruby/object:Gem::Dependency
42
- name: rake
56
+ name: pry
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - "~>"
46
60
  - !ruby/object:Gem::Version
47
- version: '10.0'
61
+ version: '0.10'
48
62
  type: :development
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
- version: '10.0'
68
+ version: '0.10'
55
69
  - !ruby/object:Gem::Dependency
56
- name: minitest
70
+ name: rake
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
73
  - - "~>"
60
74
  - !ruby/object:Gem::Version
61
- version: '5.0'
75
+ version: '10.0'
62
76
  type: :development
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
- version: '5.0'
82
+ version: '10.0'
69
83
  description: Create Plain Old Ruby Objects that take attributes, validate them, and
70
84
  perform an action.
71
85
  email:
@@ -75,6 +89,7 @@ extensions: []
75
89
  extra_rdoc_files: []
76
90
  files:
77
91
  - ".gitignore"
92
+ - ".ruby-version"
78
93
  - ".travis.yml"
79
94
  - CODE_OF_CONDUCT.md
80
95
  - Gemfile
@@ -85,6 +100,7 @@ files:
85
100
  - bin/setup
86
101
  - gourami.gemspec
87
102
  - lib/gourami.rb
103
+ - lib/gourami/attribute_name_conflict_error.rb
88
104
  - lib/gourami/attributes.rb
89
105
  - lib/gourami/coercer.rb
90
106
  - lib/gourami/configuration_error.rb
@@ -103,7 +119,7 @@ homepage: http://github.com/Vydia/gourami
103
119
  licenses:
104
120
  - MIT
105
121
  metadata: {}
106
- post_install_message:
122
+ post_install_message:
107
123
  rdoc_options: []
108
124
  require_paths:
109
125
  - lib
@@ -118,9 +134,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
118
134
  - !ruby/object:Gem::Version
119
135
  version: '0'
120
136
  requirements: []
121
- rubyforge_project:
122
- rubygems_version: 2.7.7
123
- signing_key:
137
+ rubygems_version: 3.0.4
138
+ signing_key:
124
139
  specification_version: 4
125
140
  summary: Keep your Routes, Controllers and Models thin.
126
141
  test_files: []