gourami 0.5.0 → 1.3.1

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: 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: []