cynic 0.0.3.3 → 0.0.3.4

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
  SHA1:
3
- metadata.gz: f06ba77863844a1b79da02973a972d9521c45939
4
- data.tar.gz: f3cdd7b18a0f6c2a37716c4b4b7b8564505d5cc6
3
+ metadata.gz: 4eba83ffc70d526a9bd125bd744a45d6d1523646
4
+ data.tar.gz: 08016075298c30877a1002fa864af77e21f1b309
5
5
  SHA512:
6
- metadata.gz: e6a1475e4715bcafbb5c0b2630046661dc2171d8872891f4142002b874f3741ae04260fb55d69e979bd0d008c2ced2d44ae8982201d9ad2946c20a566ad5138d
7
- data.tar.gz: 618272197924429b08bcb7d48153ae00a57392768452f686232166107191345f47642866b3cb9e17dbc20339bd0ca8dba7c4d7f3c019da6a66d4d204bf5ca9ec
6
+ metadata.gz: 4e8fb783d5827d5d765637b2e1d62532f0508c40795590f632c23f5d756c2aa0668d92fd89f7fef3af74588a2dfc3bf0ae940fbc8c689b6027211e5ae1c76a11
7
+ data.tar.gz: 30defcf46ad77ab6bfca77b7647ba9e40cbed8f6443d7e9e8f244b6415a09857f18dcc2a3532fee01351414ea7d96235c30da95cd48d2c180d0bfa908d0b4d8d
data/bin/cynic CHANGED
@@ -4,16 +4,16 @@ require "cynic"
4
4
 
5
5
  module Cynic
6
6
  class Generator
7
- attr_accessor :name, :options, :app_name
7
+ attr_accessor :name, :options, :app_name, :dir
8
8
 
9
9
  def initialize(args)
10
- @name, @options = args[1], args[2..-1]
10
+ @dir, @name, @options = args[1], args[1].gsub("-", "_"), args[2..-1]
11
11
  @app_name = @name.gsub(/(?<=[_-]|^|\s)(\w)/){$1.upcase}.gsub(/(?:[_-]|\s)(\w)/,'\1')
12
12
  end
13
13
 
14
14
 
15
15
  def make_file(name, &block)
16
- File.open("#{@name}/#{name}", "w") do |file|
16
+ File.open("#{@dir}/#{name}", "w") do |file|
17
17
  block.call(file)
18
18
  end
19
19
  end
@@ -34,7 +34,12 @@ end
34
34
  file.write <<-HEREDOC
35
35
  source 'https://rubygems.org'
36
36
 
37
+ gem "rake"
38
+
37
39
  gem "cynic", "#{Cynic::VERSION}"
40
+ gem "activerecord"
41
+ gem "pg"
42
+
38
43
  HEREDOC
39
44
  end
40
45
 
@@ -44,8 +49,11 @@ gem "cynic", "#{Cynic::VERSION}"
44
49
  make_file("config/application.rb") do |file|
45
50
  file.write <<-HEREDOC
46
51
  require 'cynic'
52
+ require 'active_record'
47
53
  Dir.glob(["./app/**/*.rb"]).each {|file| require file }
48
54
 
55
+ ActiveRecord::Base.establish_connection(YAML::load(File.open('config/database.yml'))[ENV["RACK_ENV"]])
56
+
49
57
  module #{@app_name}
50
58
  class Application < Cynic::App
51
59
  # Your code here
@@ -56,7 +64,6 @@ end
56
64
  end
57
65
 
58
66
  def create_config
59
- Dir.mkdir([self.name, "config"].join("/"))
60
67
  make_file("config.ru") do |file|
61
68
  file.write <<-HEREDOC
62
69
  require './config/application'
@@ -77,7 +84,6 @@ end
77
84
  end
78
85
 
79
86
  def create_layout
80
- Dir.mkdir([self.name, "app", "views", "layouts"].join("/"))
81
87
  make_file("app/views/layouts/application.html.erb") do |file|
82
88
  file.write <<-HEREDOC
83
89
  <!DOCTYPE html>
@@ -92,27 +98,169 @@ end
92
98
  HEREDOC
93
99
  end
94
100
  end
101
+
102
+ def create_database_placeholder
103
+ make_file("config/database.yml") do |file|
104
+ file.write <<-HEREDOC
105
+ development:
106
+ adapter: 'postgresql'
107
+ database: '#{self.name}_development'
108
+ encoding: unicode
109
+ pool: 5
110
+
111
+ test:
112
+ adapter: 'postgresql'
113
+ database: '#{self.name}_test'
114
+ encoding: unicode
115
+ pool: 5
116
+
117
+ production:
118
+ adapter: 'postgresql'
119
+ database: '#{self.name}_production'
120
+ encoding: unicode
121
+ pool: 5
122
+
123
+ HEREDOC
124
+ end
125
+ end
126
+
127
+ def create_rake_tasks
128
+ make_file("Rakefile") do |file|
129
+ file.write <<-HEREDOC
130
+ require 'yaml'
131
+ require 'active_record'
132
+ Dir["config/tasks/*.rake"].sort.each { |ext| load ext }
133
+ HEREDOC
134
+ end
135
+
136
+ make_file("config/tasks/migrations.rake") do |file|
137
+ file.write <<-HEREDOC
138
+ task :environment do
139
+ env = ENV["RACK_ENV"] ? ENV["RACK_ENV"] : "development"
140
+ ActiveRecord::Base.establish_connection(YAML::load(File.open('config/database.yml'))[env])
141
+ end
142
+
143
+ namespace :db do
144
+ def connect(conf)
145
+ if conf["adapter"] == 'postgresql'
146
+ ActiveRecord::Base.establish_connection(conf.merge('database' => 'postgres'))
147
+ else
148
+ ActiveRecord::Base.establish_connection(conf.merge('database' => nil))
149
+ end
150
+ end
151
+
152
+ desc "Create the database defined in config/database.yml for the current RACK_ENV"
153
+ task :create do
154
+ env = ENV["RACK_ENV"] ? ENV["RACK_ENV"] : "development"
155
+ config = YAML::load(File.open('config/database.yml'))[env]
156
+ connect(config)
157
+ ActiveRecord::Base.connection.create_database(config['database'])
158
+ end
159
+
160
+ namespace :create do
161
+ desc "Create all the local databases defined in config/database.yml"
162
+ task :all do
163
+ YAML::load(File.open('config/database.yml')).each_value do |config|
164
+ next unless config['database']
165
+ unless @config
166
+ connect(config)
167
+ @config = 1
168
+ end
169
+ ActiveRecord::Base.connection.create_database(config['database'])
170
+ end
171
+ end
172
+ end
173
+
174
+ desc "Drops the database for the current RACK_ENV"
175
+ task :drop do
176
+ env = ENV["RACK_ENV"] ? ENV["RACK_ENV"] : "development"
177
+ config = YAML::load(File.open('config/database.yml'))[env]
178
+ connect(config)
179
+ ActiveRecord::Base.connection.drop_database config['database']
180
+ end
181
+
182
+ namespace :drop do
183
+ desc "Drops all the local databases defined in config/database.yml"
184
+ task :all do
185
+ YAML::load(File.open('config/database.yml')).each_value do |config|
186
+ next unless config['database']
187
+ unless @config
188
+ connect(config)
189
+ @config = 1
190
+ end
191
+ ActiveRecord::Base.connection.drop_database config['database']
192
+ end
193
+ end
194
+ end
195
+
196
+ desc "Migrate the database through scripts in db/migrate"
197
+ task(:migrate => :environment) do
198
+ ActiveRecord::Migrator.migrate('db/migrate', ENV["VERSION"] ? ENV["VERSION"].to_i : nil )
199
+ Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
200
+ end
201
+
202
+ namespace :migrate do
203
+ desc 'Runs the "down" for a given migration VERSION'
204
+ task(:down => :environment) do
205
+ ActiveRecord::Migrator.down('db/migrate', ENV["VERSION"] ? ENV["VERSION"].to_i : nil )
206
+ Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
207
+ end
208
+
209
+ desc 'Runs the "up" for a given migration VERSION'
210
+ task(:up => :environment) do
211
+ ActiveRecord::Migrator.up('db/migrate', ENV["VERSION"] ? ENV["VERSION"].to_i : nil )
212
+ Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
213
+ end
214
+
215
+ desc "Rollbacks the database one migration and re migrate up"
216
+ task(:redo => :environment) do
217
+ ActiveRecord::Migrator.rollback('db/migrate', 1 )
218
+ ActiveRecord::Migrator.up('db/migrate', nil )
219
+ Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
220
+ end
221
+ end
222
+
223
+ namespace :schema do
224
+ task :dump => :environment do
225
+ require 'active_record/schema_dumper'
226
+ File.open(ENV['SCHEMA'] || "db/schema.rb", "w") do |file|
227
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
228
+ end
229
+ end
230
+ end
231
+ end
232
+ HEREDOC
233
+ end
234
+ end
235
+
236
+ def create_schema
237
+ make_file("db/schema.rb") do |file|
238
+ file.write <<-HEREDOC
239
+
240
+ HEREDOC
241
+ end
242
+ end
95
243
  end
96
244
  end
97
245
 
98
246
  case ARGV[0]
99
247
  when "new"
100
248
  generator = Cynic::Generator.new(ARGV)
101
- Dir.mkdir(generator.name)
102
- Dir.mkdir([generator.name, "app"].join("/"))
103
- Dir.mkdir([generator.name, "app", "controllers"].join("/"))
104
- Dir.mkdir([generator.name, "app", "views"].join("/"))
105
- Dir.mkdir([generator.name, "app", "models"].join("/"))
106
- Dir.mkdir([generator.name, "public"].join("/"))
107
- Dir.mkdir([generator.name, "public", "stylesheets"].join("/"))
108
- Dir.mkdir([generator.name, "public", "javascripts"].join("/"))
109
- Dir.mkdir([generator.name, "public", "images"].join("/"))
249
+
250
+ Dir.mkdir(generator.dir)
251
+ %w(db app app/controllers app/views app/views/layouts app/models public public/stylesheets public/javascripts public/images config config/tasks).each do |dir|
252
+ Dir.mkdir([generator.dir, dir].join("/"))
253
+ end
254
+
110
255
  generator.create_config
111
256
  generator.create_application
112
257
  generator.create_controller
113
258
  generator.create_gemfile
114
259
  generator.create_routes
115
260
  generator.create_layout
261
+ generator.create_database_placeholder
262
+ generator.create_rake_tasks
263
+ generator.create_schema
116
264
  when "server"
117
265
  `shotgun -p 4545 -s puma`
118
266
  end
data/lib/cynic/app.rb CHANGED
@@ -21,8 +21,12 @@ module Cynic
21
21
  send_response
22
22
  end
23
23
 
24
+ def request
25
+ @request ||= Rack::Request.new(@env)
26
+ end
27
+
24
28
  def routing_to_request
25
- routing.go_to @env["REQUEST_METHOD"].downcase.to_sym, @env["REQUEST_PATH"]
29
+ routing.go_to request.request_method.downcase.to_sym, @env["REQUEST_PATH"]
26
30
  end
27
31
 
28
32
  def send_response
@@ -34,16 +38,26 @@ module Cynic
34
38
  end
35
39
 
36
40
  def execute_controller_actions
37
- object, method = self.routing_to_request
38
- object.response(method)
41
+ route = self.routing_to_request
42
+ if route.object.respond_to? :request=
43
+ route.params.each do |k,v|
44
+ request.update_param(k, v)
45
+ end
46
+ route.object.request = request
47
+ end
48
+ route.object.response(route.method)
39
49
  end
40
50
 
41
51
  def status
42
52
  200
43
53
  end
44
54
 
55
+ def content_type
56
+ request.content_type || "text/html"
57
+ end
58
+
45
59
  def headers
46
- {"CONTENT-TYPE" => "text/html"}
60
+ {"CONTENT-TYPE" => content_type}
47
61
  end
48
62
 
49
63
  end
@@ -1,5 +1,8 @@
1
1
  module Cynic
2
2
  class Configuration
3
- attr_accessor :environment
3
+ attr_accessor :environment, :database
4
+
5
+ def initialize(options={})
6
+ end
4
7
  end
5
8
  end
@@ -1,5 +1,6 @@
1
1
  module Cynic
2
2
  class Controller
3
+ attr_accessor :request
3
4
  class << self
4
5
  attr_accessor :before_actions
5
6
  def before_actions
@@ -30,6 +31,10 @@ module Cynic
30
31
  send method
31
32
  end
32
33
 
34
+ def params
35
+ request.params
36
+ end
37
+
33
38
  private
34
39
 
35
40
  # The +name+ split and joined into a string seperated by "/"'s
@@ -0,0 +1,32 @@
1
+ module Cynic
2
+ class RouteOption
3
+ def initialize(hash={})
4
+ @hash = hash
5
+ end
6
+
7
+ def [](route)
8
+ key, regex = regex_for_key(route)
9
+ object, method = @hash[key]
10
+ Cynic::Route.new(object, method, params(route)) if object
11
+ end
12
+
13
+ def params(route)
14
+ key, regex = regex_for_key(route)
15
+ return nil if regex.nil?
16
+ matched_route = route.match(regex)
17
+ Hash[matched_route.names.map(&:to_sym).zip(matched_route.captures)]
18
+ end
19
+
20
+ def regex_for_key(key)
21
+ regexps.find {|k, regex| key.match regex }
22
+ end
23
+
24
+ def regexps
25
+ @hash.keys.map {|key| [key, Regexp.new("^" + key.gsub(/:(.\w+)/, '(?<\1>\w+)') + "$")] }
26
+ end
27
+
28
+ def method_missing(method, *args)
29
+ @hash.send(method, *args)
30
+ end
31
+ end
32
+ end
data/lib/cynic/routing.rb CHANGED
@@ -1,9 +1,27 @@
1
1
  module Cynic
2
+ class Route
3
+ attr_accessor :params, :object, :method
4
+ def initialize(object, method, params={})
5
+ @object = object
6
+ @method = method
7
+ @params = params
8
+ end
9
+
10
+ def method_missing(method, *args)
11
+ [@object, @method, @params].send(method, *args)
12
+ end
13
+
14
+ end
2
15
  class Routing
3
16
  class Error < StandardError;end
4
17
 
5
18
  def initialize()
6
- @routings = {get: {}, post: {}, patch: {}, delete: {}}
19
+ @routings = {
20
+ get: Cynic::RouteOption.new,
21
+ post: Cynic::RouteOption.new,
22
+ patch: Cynic::RouteOption.new,
23
+ delete: Cynic::RouteOption.new
24
+ }
7
25
  end
8
26
 
9
27
  def define(&block)
@@ -12,8 +30,9 @@ module Cynic
12
30
  end
13
31
 
14
32
  def go_to(request_method, request_path)
15
- if action(request_method, request_path)
16
- action(request_method, request_path)
33
+ route = action(request_method, request_path)
34
+ if route
35
+ route
17
36
  else
18
37
  raise Error, "undefined routing #{request_method.upcase} '#{request_path}'"
19
38
  end
@@ -26,5 +45,9 @@ module Cynic
26
45
  def get(path, options={})
27
46
  @routings[:get][path] = options[:to]
28
47
  end
48
+
49
+ def post(path, options={})
50
+ @routings[:post][path] = options[:to]
51
+ end
29
52
  end
30
53
  end
data/lib/cynic/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Cynic
2
- VERSION = "0.0.3.3"
2
+ VERSION = "0.0.3.4"
3
3
  end
data/lib/cynic.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'rack'
1
2
  require "cynic/version"
2
3
  require 'cynic/configuration'
3
4
 
@@ -21,3 +22,4 @@ require 'cynic/response'
21
22
  require 'cynic/routing'
22
23
  require 'cynic/controller'
23
24
  require 'cynic/renderer'
25
+ require 'cynic/route_option'
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Cynic::App do
4
- let(:env) { {"REQUEST_METHOD" => "GET", "REQUEST_PATH" => "/cynic"} }
4
+ let(:env) { {"REQUEST_METHOD" => "GET", "REQUEST_PATH" => "/cynic", "rack.input" => "wtf", "CONTENT_TYPE" => "text/json"} }
5
5
  let(:cynic) { Cynic.application }
6
6
  before { require "support/routes" }
7
7
 
@@ -38,6 +38,10 @@ describe Cynic::App do
38
38
  it "has a response object with a body" do
39
39
  expect(cynic.call(env)[2].body).to eq "This is erb hello"
40
40
  end
41
+
42
+ it "has a response of json" do
43
+ expect(cynic.call(env)[1]["CONTENT-TYPE"]).to eq "text/json"
44
+ end
41
45
  end
42
46
 
43
47
  end
@@ -14,6 +14,17 @@ end
14
14
  describe Cynic::Controller do
15
15
  let(:controller) { CynicController.new }
16
16
  before { CynicController.instance_variable_set(:@before_actions, nil)}
17
+
18
+ describe "#request" do
19
+ before {
20
+ request = double("Rack::Request")
21
+ request.stub(:params) { {id: 1} }
22
+ controller.request = request
23
+ }
24
+ it "has params" do
25
+ expect(controller.params).to eq({id: 1})
26
+ end
27
+ end
17
28
  describe "#render" do
18
29
  it "finds a file" do
19
30
  Cynic::Renderer.any_instance.stub(:layout_file).and_return("<%= yield %>")
@@ -0,0 +1,25 @@
1
+ require "spec_helper"
2
+
3
+ describe Cynic::RouteOption do
4
+ describe "regex find" do
5
+ let(:hash) { Cynic::RouteOption.new({"/params/:user_id" => ["blah", :method]}) }
6
+ it "finds value" do
7
+ expect(hash["/params/1"].object).to eq "blah"
8
+ end
9
+
10
+ it "returns the params" do
11
+ expect(hash.params("/params/1")[:user_id]).to eq "1"
12
+ end
13
+
14
+ context "when no match" do
15
+ it "returns nil" do
16
+ expect(hash["/no-route"]).to eq nil
17
+ end
18
+
19
+ it "returns nil" do
20
+ expect(hash.params("/no-route")).to eq nil
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -5,11 +5,7 @@ describe Cynic::Routing do
5
5
  let(:routing) { defined_routing }
6
6
 
7
7
  it "calls the method to be returned" do
8
- expect(routing.go_to(:get, "/")).to eq [String, :new]
9
- end
10
-
11
- it "calls the method to be returned" do
12
- expect(routing.go_to(:get, "/blog").last).to eq :rand
8
+ expect(routing.go_to(:get, "/").method).to eq :new
13
9
  end
14
10
 
15
11
  it "returns the index" do
@@ -21,4 +17,25 @@ describe Cynic::Routing do
21
17
  expect { routing.go_to(:get, "/im-lost") }.to raise_error Cynic::Routing::Error, "undefined routing GET '/im-lost'"
22
18
  end
23
19
 
20
+ describe "POST" do
21
+
22
+ it "can set a post" do
23
+ route = Cynic::Routing.new
24
+ route.post "/blah", to: [String, :new]
25
+ expect(route.go_to(:post, "/blah").first).to eq String
26
+ end
27
+
28
+ it "can render text" do
29
+ expect(routing.go_to(:post, "/blah").first).to be_an_instance_of Cynic::Controller
30
+ end
31
+ end
32
+
33
+ describe "#params" do
34
+
35
+ it "recognizes the path with a named parameter" do
36
+ expect(routing.go_to(:get, "/params/1").params).to eq({id: "1"})
37
+ end
38
+
39
+ end
40
+
24
41
  end
@@ -1,5 +1,5 @@
1
1
  Cynic.application.routing.define do |map|
2
2
  map.get "/", to: [String, :new]
3
3
  map.get "/blog", to: [self, :rand]
4
- map.get "/cynic", to: [Cynic::Controller.new, :index]
4
+ map.get "/cynic", to: [Cynic::Controller.new, :index]
5
5
  end
@@ -2,7 +2,9 @@ def defined_routing(app=nil)
2
2
  routing = app.nil? ? Cynic::Routing.new : app.routing
3
3
  routing.define do |map|
4
4
  map.get "/", to: [String, :new]
5
- map.get "/blog", to: [self, :rand]
5
+ map.get "/blog", to: [String, :new]
6
6
  map.get "/cynic", to: [Cynic::Controller.new, :index]
7
+ map.post "/blah", to: [Cynic::Controller.new, :index]
8
+ map.get "/params/:id", to: [Cynic::Controller.new, :index]
7
9
  end
8
10
  end
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "cynic", "0.0.3.3"
@@ -0,0 +1,3 @@
1
+ class ApplicationController < Cynic::Controller
2
+ # Application wide methods and before_actions go here
3
+ end
@@ -0,0 +1,9 @@
1
+ <!DOCTYPE html>
2
+ <head>
3
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
4
+ <title></title>
5
+ </head>
6
+
7
+ <body>
8
+ <%= yield %>
9
+ </body>
@@ -0,0 +1,8 @@
1
+ require 'cynic'
2
+ Dir.glob(["./app/**/*.rb"]).each {|file| require file }
3
+
4
+ module UserProvider
5
+ class Application < Cynic::App
6
+ # Your code here
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ Cynic.application.routing.define do |map|
2
+ # map.get "/", to: [HomeController.new, :index]
3
+ end
@@ -0,0 +1,3 @@
1
+ require './config/application'
2
+ user_provider = UserProvider::Application.initialize!
3
+ run user_provider
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cynic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3.3
4
+ version: 0.0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bookis Smuin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-02-17 00:00:00.000000000 Z
11
+ date: 2014-02-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -74,17 +74,25 @@ files:
74
74
  - lib/cynic/controller.rb
75
75
  - lib/cynic/renderer.rb
76
76
  - lib/cynic/response.rb
77
+ - lib/cynic/route_option.rb
77
78
  - lib/cynic/routing.rb
78
79
  - lib/cynic/version.rb
79
80
  - spec/lib/cynic/app_spec.rb
80
81
  - spec/lib/cynic/controller_spec.rb
81
82
  - spec/lib/cynic/renderer_spec.rb
82
83
  - spec/lib/cynic/response_spec.rb
84
+ - spec/lib/cynic/route_option_spec.rb
83
85
  - spec/lib/cynic/routing_spec.rb
84
86
  - spec/lib/cynic_spec.rb
85
87
  - spec/spec_helper.rb
86
88
  - spec/support/routes.rb
87
89
  - spec/support/routing.rb
90
+ - user-provider/Gemfile
91
+ - user-provider/app/controllers/application_controller.rb
92
+ - user-provider/app/views/layouts/application.html.erb
93
+ - user-provider/config.ru
94
+ - user-provider/config/application.rb
95
+ - user-provider/config/routes.rb
88
96
  homepage:
89
97
  licenses:
90
98
  - MIT
@@ -114,6 +122,7 @@ test_files:
114
122
  - spec/lib/cynic/controller_spec.rb
115
123
  - spec/lib/cynic/renderer_spec.rb
116
124
  - spec/lib/cynic/response_spec.rb
125
+ - spec/lib/cynic/route_option_spec.rb
117
126
  - spec/lib/cynic/routing_spec.rb
118
127
  - spec/lib/cynic_spec.rb
119
128
  - spec/spec_helper.rb