camping 3.0.2 → 3.1.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: 6aa52530d539466610a0ae7b505a72176f38b2ba12975f776447dcfb4e6d6934
4
- data.tar.gz: 31c521e39db07c10472bff1d6e56bec41b55052bf0e40b3a8165e1266873815b
3
+ metadata.gz: e1aa24f1bf87479fdf76924e8a0ac6a72b308eebce898efad48e2a23879b5f45
4
+ data.tar.gz: 6779a418c8f44829aa14cfdb5633925a8479ab91c8d1bff0640b7dd7f6de5634
5
5
  SHA512:
6
- metadata.gz: 200a6af8d4e245e7bf1fac8bb5965ec9ec2a049fc2b3965b9129e5fcc296f251de7ec5fd83656bde13dac6e2869692f52dd5d707e5718e84a498da1f742f7d07
7
- data.tar.gz: b80af113694c5303ab1c955876dd6e6e0eb78d0324fa2b4d16f9361b8c8114e0af1cfdffd6761d452a22a3ee5c8febc679078ed50978a2d8a1385806d6da208b
6
+ metadata.gz: d68c77354dce8e339a0cd5371d31a99f775380f176bbac8edab4e36c5cff3d07224bad49f918c34b546f7bf57fa553b2bbc38945d82a0c482de7cf3b0292939e
7
+ data.tar.gz: 23fd9016b59844b38a9331a3cac4505d398cf350a27e310551d0a7f69eb4e950dad74ed487e53b71c7528e0bafb1310abe003a2dd68241ad5a73fce822dfaf03
data/README.md CHANGED
@@ -64,27 +64,38 @@ end
64
64
  ## Installation
65
65
 
66
66
  Interested yet? Luckily it's quite easy to install Camping. We'll be using
67
- a tool called RubyGems, so if you don't have that installed yet, go grab it!
68
- Once that's sorted out, open up a Terminal or Command Line and enter:
67
+ a tool called RubyGems, and Bundler, so if you don't have that installed
68
+ yet, go grab it! Once that's sorted out, open up a Terminal or Command
69
+ Line and enter:
69
70
 
70
71
  ```
71
72
  gem install camping
72
73
  ```
73
74
 
75
+ Also make certain to have Bundler installed:
76
+
77
+ ```
78
+ gem install bundler
79
+ ```
80
+
74
81
  ~~Even better, install the Camping Omnibus, a full package of recommended libs:~~ Camping Omnibus will return for summer vacation.
75
82
 
83
+ Now make a new directory filled with your camp essentials using the `camping new` command:
84
+
76
85
  ```
77
- gem install camping-omnibus
86
+ camping new Donuts # You can replace Donuts with whatever but CamelCased.
78
87
  ```
79
88
 
80
- If not, you should be aware of that Camping itself only depends on
81
- [Rack](https://github.com/rack/rack), and if you're going to use the views you also
82
- need to install **[markaby](https://github.com/markaby/markaby)**, and if you're going to use the database you need
83
- **activerecord** as well.
89
+ Move to your new directory, then use bundler to install all of your camp's dependencies:
84
90
 
85
91
  ```
86
- gem install markaby
87
- gem install activerecord
92
+ cd donuts; bundle install
93
+ ```
94
+
95
+ You can now run camping using the `camping` command. We recommend running camping in development mode locally. Make certain to prefix the camping command with `bundle exec` to run your app with the gems you've installed just for your camp:
96
+
97
+ ```
98
+ bundle exec camping -e development
88
99
  ```
89
100
 
90
101
  ## Learning
@@ -107,6 +118,10 @@ like chatting with us, you should join [#camping @ irc.freenode.net](http://java
107
118
 
108
119
  Tests should be run using bundler and rake: `bundle exec rake`.
109
120
 
121
+ ## Minting Releases
122
+
123
+ We use Ruby Gems to distribute versions of Camping.
124
+
110
125
  ## Authors
111
126
 
112
127
  Camping was originally crafted by [why the lucky stiff](http://en.wikipedia.org/wiki/Why_the_lucky_stiff),
data/Rakefile CHANGED
@@ -70,6 +70,18 @@ Rake::TestTask.new(:test) do |t|
70
70
  t.test_files = FileList['test/app_*.rb', 'test/gear/gear_*.rb']
71
71
  end
72
72
 
73
+ ## Reloader Tests
74
+ Rake::TestTask.new(:reloader) do |t|
75
+ t.libs << "test"
76
+ t.test_files = FileList['test/reload_*.rb']
77
+ end
78
+
79
+ ## Reloader Tests
80
+ Rake::TestTask.new(:configreloader) do |t|
81
+ t.libs << "test"
82
+ t.test_files = FileList['test/config_*.rb']
83
+ end
84
+
73
85
  ## Diff
74
86
  desc "Compare camping and camping-unabridged"
75
87
  task :diff do
@@ -97,7 +109,7 @@ end
97
109
  error = false
98
110
 
99
111
  ## Check
100
- task :check => ["test", "check:valid", "check:equal", "check:size", "check:lines", "check:exit"]
112
+ task :check => ["test", "reloader", "configreloader", "check:valid", "check:equal", "check:size", "check:lines", "check:exit"]
101
113
  namespace :check do
102
114
 
103
115
  desc "Check source code validity"
@@ -0,0 +1,14 @@
1
+ # Campguide is a small helper to map common errors in a Camping project to a quick resolution.
2
+ module Campguide
3
+ Errors = {"wrong number of arguments (given 3, expected 0)" => "ArgumentError. This is sometimes caused when you try to send a request to a controller when a camping app hasn't made camp yet. make certain to call Camping.make_camp to set up your apps."}
4
+
5
+ class << self
6
+
7
+ # accepts string error messages and tries to match them with better explanations of the error.
8
+ def make_sense(error_message)
9
+ message = Errors[error_message]
10
+ puts message ? message : error_message
11
+ end
12
+
13
+ end
14
+ end
@@ -237,9 +237,79 @@ task :test => 'test:all'
237
237
  namespace 'test' do
238
238
  Rake::TestTask.new('all') do |t|
239
239
  t.libs << 'test'
240
- t.test_files = FileList['test/nuts_*.rb']
240
+ t.test_files = FileList['test/test_*.rb']
241
241
  end
242
242
  end
243
+ TXT
244
+ end
245
+
246
+ # writes a test_helper
247
+ def make_test_helper
248
+ write 'test/test_helper.r', <<-TXT
249
+ # Test Helper
250
+ $:.unshift File.dirname(__FILE__) + '/../'
251
+
252
+ begin
253
+ require 'rubygems'
254
+ rescue LoadError
255
+ end
256
+
257
+ require 'minitest/autorun'
258
+ require 'rack/test'
259
+ require 'minitest/reporters'
260
+ require 'minitest/hooks'
261
+ Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new(:color => true)]
262
+
263
+ # Default TestCase with some helpers.
264
+ # The same stuff we use for Camping itself.
265
+ class TestCase < MiniTest::Test
266
+ include Rack::Test::Methods
267
+ include Minitest::Hooks
268
+
269
+ def self.inherited(mod)
270
+ mod.app = Object.const_get(mod.to_s[/\w+/])
271
+ super
272
+ end
273
+
274
+ class << self
275
+ attr_accessor :app
276
+ end
277
+
278
+ def setup
279
+ super
280
+ Camping.make_camp
281
+ end
282
+
283
+ def body() last_response.body end
284
+ def app() self.class.app end
285
+
286
+ def response_body() last_response.to_a end
287
+
288
+ def assert_reverse
289
+ begin
290
+ yield
291
+ rescue Exception
292
+ else
293
+ assert false, "Block didn't fail"
294
+ end
295
+ end
296
+
297
+ def assert_body(str, message="")
298
+ case str
299
+ when Regexp
300
+ assert_match(str, last_response.body.strip, message)
301
+ else
302
+ assert_equal(str.to_s, last_response.body.strip, message)
303
+ end
304
+ end
305
+
306
+ def assert_status(code, message="")
307
+ assert_equal(code, last_response.status, message)
308
+ end
309
+
310
+ def test_silly; end
311
+ end
312
+
243
313
  TXT
244
314
  end
245
315
 
@@ -274,6 +344,7 @@ group :test do
274
344
  gem 'minitest', '~> 5.0'
275
345
  gem 'minitest-reporters'
276
346
  gem 'rack-test'
347
+ gem 'minitest-hooks'
277
348
  end
278
349
 
279
350
  GEM
@@ -287,6 +358,8 @@ Camping is really fun and I hope you enjoy it.
287
358
 
288
359
  Start camping by running: `camping` in the root directory.
289
360
 
361
+ To start Camping in development mode run: `camping -e development
362
+
290
363
  READ
291
364
  end
292
365
 
@@ -360,47 +433,55 @@ RUBY
360
433
  end
361
434
 
362
435
  class Commands
436
+ class << self
363
437
 
364
- # A helper method to spit out Routes for an application
365
- def self.routes(theApp = Camping, silent = false)
366
- routes = Camping::CommandsHelpers::RoutesParser.parse theApp
367
- routes.display unless silent == true
368
- return routes
369
- end
438
+ # A helper method to spit out Routes for an application
439
+ def routes(theApp = Camping, silent = false)
440
+ routes = Camping::CommandsHelpers::RoutesParser.parse theApp
441
+ routes.display unless silent == true
442
+ return routes
443
+ end
370
444
 
371
- def self.new_cmd(app_name=:Camp)
372
-
373
- # Normalize the app_name
374
- Camping::CommandsHelpers.app_name_from_input(app_name) => {app_name:, snake_name:, camel_name:}
375
-
376
- # make a directory then move there.
377
- # _original_dir = Dir.pwd
378
- Dir.mkdir("#{snake_name}") unless Dir.exist?("#{snake_name}")
379
- Dir.chdir("#{snake_name}")
380
-
381
- # generate a new camping app in a directory named after it:
382
- Generators::make_camp_file(camel_name)
383
- Generators::make_gitignore()
384
- Generators::make_rakefile()
385
- Generators::make_ruby_version()
386
- Generators::make_configkdl()
387
- Generators::make_gemfile()
388
- Generators::make_readme()
389
- Generators::make_public_folder()
390
- Generators::make_test_folder()
391
-
392
- # optionally add omnibus support
393
- # add src/ folder
394
- # add lib/ folder
395
- # add views/ folder
396
-
397
- # optionally add a local database too, through guidebook
398
- # add db/ folder
399
- # add db/migrate folder
400
- # add db/config.kdl
401
- # append migrations stuff to Rakefile.
402
- `ls`
403
- end
445
+ def new_cmd(app_name=:Camp)
446
+
447
+ # Normalize the app_name
448
+ Camping::CommandsHelpers.app_name_from_input(app_name) => {app_name:, snake_name:, camel_name:}
449
+
450
+ # make a directory then move there.
451
+ # _original_dir = Dir.pwd
452
+ Dir.mkdir("#{snake_name}") unless Dir.exist?("#{snake_name}")
453
+ Dir.chdir("#{snake_name}")
454
+
455
+ # generate a new camping app in a directory named after it:
456
+ Generators::make_camp_file(camel_name)
457
+ Generators::make_gitignore()
458
+ Generators::make_rakefile()
459
+ Generators::make_ruby_version()
460
+ Generators::make_configkdl()
461
+ Generators::make_gemfile()
462
+ Generators::make_readme()
463
+ Generators::make_public_folder()
464
+ Generators::make_test_folder()
465
+ Generators::make_test_helper()
466
+
467
+ # optionally add omnibus support
468
+ # add src/ folder
469
+ # add lib/ folder
470
+ # add views/ folder
471
+
472
+ # optionally add a local database too, through guidebook
473
+ # add db/ folder
474
+ # add db/migrate folder
475
+ # add db/config.kdl
476
+ # append migrations stuff to Rakefile.
477
+ `ls`
478
+ end
479
+
480
+ # TODO: Create this generator
481
+ # generates the apps folder from apps found in camp.rb or config.ru
482
+ # def generate_apps_folder
483
+ # end
404
484
 
485
+ end
405
486
  end
406
487
  end
@@ -1,48 +1,48 @@
1
1
  # This gear is originally from filtering_camping gem by judofyr, and techarc.
2
2
  module Gear
3
- module Filters
4
- module ClassMethods
5
- def filters
6
- @_filters ||= {:before => [], :after => []}
7
- end
3
+ module Filters
4
+ module ClassMethods
5
+ def filters
6
+ @_filters ||= {:before => [], :after => []}
7
+ end
8
8
 
9
- def before(actions, &blk)
10
- actions = [actions] unless actions.is_a?(Array)
11
- actions.each do |action|
12
- filters[:before] << [action, blk]
13
- end
14
- end
9
+ def before(actions, &blk)
10
+ actions = [actions] unless actions.is_a?(Array)
11
+ actions.each do |action|
12
+ filters[:before] << [action, blk]
13
+ end
14
+ end
15
15
 
16
- def after(actions, &blk)
17
- actions = [actions] unless actions.is_a?(Array)
18
- actions.each do |action|
19
- filters[:after] << [action, blk]
20
- end
21
- end
22
- end
16
+ def after(actions, &blk)
17
+ actions = [actions] unless actions.is_a?(Array)
18
+ actions.each do |action|
19
+ filters[:after] << [action, blk]
20
+ end
21
+ end
22
+ end
23
23
 
24
- def self.included(mod)
25
- mod.extend(ClassMethods)
26
- end
24
+ def self.included(mod)
25
+ mod.extend(ClassMethods)
26
+ end
27
27
 
28
- def self.setup(app, *a, &block) end
28
+ def self.setup(app, *a, &block) end
29
29
 
30
- def run_filters(type)
31
- o = self.class.to_s.split("::")
32
- filters = Object.const_get(o.first).filters
33
- filters[type].each do |filter|
34
- if (filter[0].is_a?(Symbol) && (filter[0] == o.last.to_sym || filter[0] == :all)) ||
35
- (filter[0].is_a?(String) && /^#{filter[0]}\/?$/ =~ @env.REQUEST_URI)
36
- self.instance_eval(&filter[1])
37
- end
38
- end
39
- end
30
+ def run_filters(type)
31
+ o = self.class.to_s.split("::")
32
+ filters = Object.const_get(o.first).filters
33
+ filters[type].each do |filter|
34
+ if (filter[0].is_a?(Symbol) && (filter[0] == o.last.to_sym || filter[0] == :all)) ||
35
+ (filter[0].is_a?(String) && /^#{filter[0]}\/?$/ =~ @env.REQUEST_URI)
36
+ self.instance_eval(&filter[1])
37
+ end
38
+ end
39
+ end
40
40
 
41
- def service(*a)
42
- run_filters(:before)
43
- super(*a)
44
- run_filters(:after)
45
- self
46
- end
47
- end
41
+ def service(*a)
42
+ run_filters(:before)
43
+ super(*a)
44
+ run_filters(:after)
45
+ self
46
+ end
47
+ end
48
48
  end
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- # lib/camping/config
3
2
  # load and parse settings.
4
3
 
5
4
  begin
@@ -1,3 +1,6 @@
1
+ require 'zeitwerk'
2
+ require 'listen'
3
+
1
4
  module Camping
2
5
  # == The Camping Reloader
3
6
  #
@@ -31,18 +34,39 @@ module Camping
31
34
  # Blog and Wiki, but they will update themselves if yourapp.rb changes.
32
35
  #
33
36
  # You can also give Reloader more than one script.
34
- class Reloader
37
+ class Loader
35
38
  attr_reader :file
39
+ Loaders = []
36
40
 
37
- def initialize(file, &blk)
41
+ def initialize(file=nil, &blk)
38
42
  @file = file
39
43
  @mtime = Time.at(0)
40
44
  @requires = []
41
45
  @apps = {}
42
46
  @callback = blk
43
-
47
+ @root = Dir.pwd
48
+ @file = @root + '/camp.rb' if @file == nil
49
+ @zeit = Zeitwerk::Loader.new
50
+ Loaders << @zeit
51
+
52
+ # setup Zeit for this reloader
53
+ setup_zeit(@zeit)
54
+
55
+ # setup recursive listener on the apps and lib directories from the source script.
56
+ @listener = Listen.to("#{@root}/apps", "#{@root}/lib", "#{@root}") do |modified, added, removed|
57
+ @mtime = Time.now
58
+ reload!
59
+ end
60
+ start
44
61
  end
45
62
 
63
+ # pass through methods to the Listener.
64
+ # for testing purposes.
65
+ def processing_events?;@listener.processing? end
66
+ def stop;@listener.stop end
67
+ def pause;@listener.pause end
68
+ def start;@listener.start end
69
+
46
70
  def name
47
71
  @name ||= begin
48
72
  base = @file.dup
@@ -52,29 +76,29 @@ module Camping
52
76
  end
53
77
  end
54
78
 
55
- # Loads the apps available in this script. Use <tt>apps</tt> to get
56
- # the loaded apps.
57
- def load_apps(old_apps)
79
+ # remove_constants called inside this.
80
+ def load_everything()
58
81
  all_requires = $LOADED_FEATURES.dup
59
82
  all_apps = Camping::Apps.dup
60
83
 
61
84
  load_file
85
+ reload_directory("#{@root}/apps")
86
+ reload_directory("#{@root}/lib")
87
+ Camping.make_camp
62
88
  ensure
63
89
  @requires = []
64
- dirs = []
65
90
  new_apps = Camping::Apps - all_apps
66
91
 
67
92
  @apps = new_apps.inject({}) do |hash, app|
68
93
  if file = app.options[:__FILE__]
69
94
  full = File.expand_path(file)
70
95
  @requires << [file, full]
71
- dirs << full.sub(/\.[^.]+$/, '')
72
96
  end
73
97
 
74
98
  key = app.name.to_sym
75
99
  hash[key] = app
76
100
 
77
- if !old_apps.include?(key)
101
+ apps.each do |app|
78
102
  @callback.call(app) if @callback
79
103
  app.create if app.respond_to?(:create)
80
104
  end
@@ -84,12 +108,13 @@ module Camping
84
108
 
85
109
  ($LOADED_FEATURES - all_requires).each do |req|
86
110
  full = full_path(req)
87
- @requires << [req, full] if dirs.any? { |x| full.index(x) == 0 }
111
+ @requires << [req, full] # if dirs.any? { |x| full.index(x) == 0 }
88
112
  end
89
113
 
90
114
  @mtime = mtime
91
115
 
92
116
  self
117
+
93
118
  end
94
119
 
95
120
  # load_file
@@ -102,13 +127,15 @@ module Camping
102
127
  else
103
128
  load(@file)
104
129
  end
130
+ @requires << [@file, File.expand_path(@file)]
105
131
  end
106
132
 
107
- # Removes all the apps defined in this script.
108
- def remove_apps
109
- @requires.each do |(path, full)|
110
- $LOADED_FEATURES.delete(path)
111
- end
133
+ # removes all constants recursively included using this script as a root.
134
+ # so everything in /apps, and /lib in relation from this script.
135
+ def remove_constants
136
+ @requires.each do |(path, full)|
137
+ $LOADED_FEATURES.delete(path)
138
+ end
112
139
 
113
140
  @apps.each do |name, app|
114
141
  Camping::Apps.delete(app)
@@ -116,6 +143,7 @@ module Camping
116
143
  end.dup
117
144
  ensure
118
145
  @apps.clear
146
+ @requires.clear
119
147
  end
120
148
 
121
149
  # Reloads the file if needed. No harm is done by calling this multiple
@@ -125,8 +153,10 @@ module Camping
125
153
  reload!
126
154
  end
127
155
 
156
+ # Force a reload.
128
157
  def reload!
129
- load_apps(remove_apps)
158
+ remove_constants
159
+ load_everything
130
160
  end
131
161
 
132
162
  # Checks if both scripts watches the same file.
@@ -144,6 +174,46 @@ module Camping
144
174
 
145
175
  private
146
176
 
177
+ # sets up Zeit autoloading for the script locations.
178
+ def setup_zeit(loader)
179
+ loader.push_dir("#{@root}/apps") if can_add_directory "#{@root}/apps"
180
+ loader.push_dir("#{@root}/lib") if can_add_directory "#{@root}/lib"
181
+ loader.enable_reloading if ENV['environment'] == 'development'
182
+ loader.setup
183
+ end
184
+
185
+ # verifies that we can add a directory to the loader.
186
+ # used for testing to prevent multiple loaders from watching the same directory.
187
+ def can_add_directory(directory)
188
+ if Dir.exist?("#{@root}/apps")
189
+ Loaders.each do |loader|
190
+ return false if loader.dirs.include? directory
191
+ end
192
+ true
193
+ else
194
+ false
195
+ end
196
+ end
197
+
198
+ # Splits the descendent files and folders found in a given directory for eager loading and recursion.
199
+ def folders_and_files_in(directory)
200
+ directory = directory + "/*" # unless directory
201
+ [Dir.glob(directory).select {|f| !File.directory? f },
202
+ Dir.glob(directory).select {|f| File.directory? f }]
203
+ end
204
+
205
+ # Reloads a directory recursively. loading more shallow files before deeper files.
206
+ def reload_directory(directory)
207
+ files, folders = folders_and_files_in(directory)
208
+ files.each {|file|
209
+ @requires << [file, File.expand_path(file)]
210
+ load file
211
+ }
212
+ folders.each {|folder|
213
+ reload_directory folder
214
+ }
215
+ end
216
+
147
217
  def mtime
148
218
  @requires.map do |(path, full)|
149
219
  File.mtime(full)
@@ -161,4 +231,7 @@ module Camping
161
231
  end
162
232
  end
163
233
  end
234
+
235
+ Reloader = Loader
236
+
164
237
  end
data/lib/camping/loads.rb CHANGED
@@ -8,8 +8,8 @@ require 'bundler/setup'
8
8
 
9
9
  # internal dependencies
10
10
  require 'camping/tools'
11
+ require 'camping/campguide'
11
12
  require 'camping/gear/filters'
12
13
  require 'camping/gear/nancy'
13
14
  require 'camping/gear/inspection'
14
15
  require 'camping/gear/kuddly'
15
-
@@ -3,7 +3,7 @@ require 'erb'
3
3
  require 'rack'
4
4
  require 'rackup'
5
5
  require 'camping/version'
6
- require 'camping/reloader'
6
+ require 'camping/loader'
7
7
  require 'camping/commands'
8
8
 
9
9
  # == The Camping Server (for development)
@@ -128,9 +128,10 @@ module Camping
128
128
  exit
129
129
  end
130
130
 
131
+ @reloader.reload!
132
+ r = @reloader
133
+
131
134
  if options[:routes] == true
132
- @reloader.reload!
133
- r = @reloader
134
135
  eval("self", TOPLEVEL_BINDING).meta_def(:reload!) { r.reload!; nil }
135
136
  ARGV.clear
136
137
  Camping::Commands.routes
@@ -139,16 +140,11 @@ module Camping
139
140
 
140
141
  if options[:server] == "console"
141
142
  puts "** Starting console"
142
- @reloader.reload!
143
- r = @reloader
144
143
  eval("self", TOPLEVEL_BINDING).meta_def(:reload!) { r.reload!; nil }
145
144
  ARGV.clear
146
145
  IRB.start
147
146
  exit
148
147
  else
149
- @reloader.reload!
150
- r = @reloader
151
- Camping.make_camp
152
148
  name = server.name[/\w+$/]
153
149
  puts "** Starting #{name} on #{options[:Host]}:#{options[:Port]}"
154
150
  super
@@ -215,7 +211,6 @@ module Camping
215
211
  def call(env)
216
212
  if ENV['environment'] == 'development'
217
213
  @reloader.reload
218
- Camping.make_camp
219
214
  end
220
215
 
221
216
  # our switch statement iterates through possible app outcomes, no apps
@@ -1,5 +1,5 @@
1
1
  require 'rack/session'
2
- class InsecureSecret < Exception #:nodoc: all
2
+ class InsecureSecretError < Exception #:nodoc: all
3
3
  end
4
4
  module Camping
5
5
  # == Getting Started
@@ -31,7 +31,7 @@ module Camping
31
31
  def self.included(app)
32
32
  key = "#{app}.state".downcase
33
33
  secret = app.options[:secret] || ['camping-secret',__FILE__, File.mtime('Rakefile')].join(":")*2
34
- raise InsecureSecret, "You're Session Secret is too short. Minimum length is 64." if secret.length < 64
34
+ raise InsecureSecretError, "Your Session Secret is too short. Minimum length is 64." if secret.length < 64
35
35
  app.use Rack::Session::Cookie, :key => key, :secrets => secret
36
36
  end
37
37
  end
@@ -1,5 +1,5 @@
1
1
  module Camping
2
- VERSION = "3.0.2"
2
+ VERSION = "3.1.2"
3
3
  def self.version
4
4
  VERSION
5
5
  end