raamen 0.1.0 → 0.1.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
  SHA1:
3
- metadata.gz: 8a824183af8e3398198698e5005affd11902c660
4
- data.tar.gz: a6fa9471d103bdfff64c07b2ba44f655fbf40f40
3
+ metadata.gz: 31023fc5e442be1d7d1f7346a40bb53355847fcb
4
+ data.tar.gz: f4fe204f88692a72324501ab003627b4c37b43af
5
5
  SHA512:
6
- metadata.gz: 26905102435808cb9c096506578f6ce226ae56f891154d90ebd007f62870a0a88460973e5f9edb9d5287511fea3a8538ab5297642299254871d840a14dc65703
7
- data.tar.gz: abfaaa5d58acb5524711a44794bdb403acfbb15485670e74ad94d3ed1e223df05aee054881e5cdbb979c8569ec63e1b8efafdcc915623494694b138d28e1f779
6
+ metadata.gz: 3b37d65b71beafcab9e9ce8e25c8b10a0f1523c7ba2441c44d2a0354a105866ba20627ec5adf918c1a592dd04c3b62bbe8a40df8d3daa85e90747dc1c685356f
7
+ data.tar.gz: 03b2e0b4b29254475fd02eeceee34237296fc71cad2a6af1cbc0bfaa7252d7af1aabc68b624d7d3bae7577b87d59eebca337295d6d9ef536e9c6075823a1a5d7
@@ -1,4 +1,9 @@
1
- require "raamen/version"
1
+ require_relative "raamen/version.rb"
2
+ require_relative "raamen/sql_object"
3
+ require_relative "raamen/controller_base"
4
+ require_relative "raamen/router"
5
+ require_relative "raamen/static"
6
+ require_relative "raamen/show_exceptions"
2
7
 
3
8
  module Raamen
4
9
  # Your code goes here...
@@ -0,0 +1,90 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
3
+ require 'erb'
4
+ require_relative 'session'
5
+ require_relative 'flash'
6
+
7
+ module Raamen
8
+ class ControllerBase
9
+ attr_reader :req, :res, :params, :session, :flash
10
+ attr_accessor :already_built_response, :authenticity_token
11
+
12
+ def initialize(req, res, route_params = {})
13
+ @req = req
14
+ @res = res
15
+ @params = route_params.merge(req.params)
16
+ @session = Session.new(req)
17
+ @flash = Flash.new(req)
18
+ @already_built_response = false
19
+ @authenticity_token = generate_authenticity_token
20
+ @@protect_from_forgery ||= false
21
+ end
22
+
23
+ def already_built_response?
24
+ self.already_built_response
25
+ end
26
+
27
+ def redirect_to(url)
28
+ raise "double render" if already_built_response?
29
+ self.res["location"] = url
30
+ self.res.status = 302
31
+ self.session.store_session(res)
32
+ self.flash.store_flash(res)
33
+ self.already_built_response = true
34
+ end
35
+
36
+ def render_content(content, content_type)
37
+ raise "double render" if already_built_response?
38
+ self.res["Content-Type"] = content_type
39
+ self.res.write(content)
40
+ self.session.store_session(res)
41
+ self.flash.store_flash(res)
42
+ self.already_built_response = true
43
+ end
44
+
45
+ def render(template_name)
46
+ template_path = File.join(
47
+ File.dirname(__FILE__),
48
+ "..",
49
+ "views",
50
+ "#{self.class.name.underscore}",
51
+ "#{template_name}.html.erb"
52
+ )
53
+ template_content = File.read(template_path)
54
+ render_content(ERB.new(template_content).result(binding), "text/html")
55
+ end
56
+
57
+ def invoke_action(name)
58
+ if @@protect_from_forgery && self.req.request_method != "GET"
59
+ check_authenticity_token
60
+ end
61
+ self.send(name)
62
+ render(name) unless already_built_response?
63
+ end
64
+
65
+ def form_authenticity_token
66
+ self.res.set_cookie(
67
+ "authenticity_token",
68
+ {path: "/", value: self.authenticity_token}
69
+ )
70
+ self.authenticity_token
71
+ end
72
+
73
+ def self.protect_from_forgery
74
+ @@protect_from_forgery = true
75
+ end
76
+
77
+ private
78
+
79
+ def generate_authenticity_token
80
+ SecureRandom.urlsafe_base64(16)
81
+ end
82
+
83
+ def check_authenticity_token
84
+ cookie = self.req.cookies["authenticity_token"]
85
+ unless cookie && cookie == params["authenticity_token"]
86
+ raise "Invalid authenticity token"
87
+ end
88
+ end
89
+ end
90
+ end
Binary file
@@ -0,0 +1,43 @@
1
+ CREATE TABLE cats (
2
+ id INTEGER PRIMARY KEY,
3
+ name VARCHAR(255) NOT NULL,
4
+ owner_id INTEGER,
5
+
6
+ FOREIGN KEY(owner_id) REFERENCES human(id)
7
+ );
8
+
9
+ CREATE TABLE humans (
10
+ id INTEGER PRIMARY KEY,
11
+ fname VARCHAR(255) NOT NULL,
12
+ lname VARCHAR(255) NOT NULL,
13
+ house_id INTEGER,
14
+
15
+ FOREIGN KEY(house_id) REFERENCES human(id)
16
+ );
17
+
18
+ CREATE TABLE houses (
19
+ id INTEGER PRIMARY KEY,
20
+ address VARCHAR(255) NOT NULL
21
+ );
22
+
23
+ INSERT INTO
24
+ houses (id, address)
25
+ VALUES
26
+ (1, "26th and Guerrero"), (2, "Dolores and Market");
27
+
28
+ INSERT INTO
29
+ humans (id, fname, lname, house_id)
30
+ VALUES
31
+ (1, "Devon", "Watts", 1),
32
+ (2, "Matt", "Rubens", 1),
33
+ (3, "Ned", "Ruggeri", 2),
34
+ (4, "Catless", "Human", NULL);
35
+
36
+ INSERT INTO
37
+ cats (id, name, owner_id)
38
+ VALUES
39
+ (1, "Breakfast", 1),
40
+ (2, "Earl", 2),
41
+ (3, "Haskell", 3),
42
+ (4, "Markov", 3),
43
+ (5, "Stray Cat", NULL);
@@ -0,0 +1,62 @@
1
+ require 'sqlite3'
2
+
3
+ module Raamen
4
+ PRINT_QUERIES = ENV['PRINT_QUERIES'] == 'true'
5
+ # https://tomafro.net/2010/01/tip-relative-paths-with-file-expand-path
6
+ ROOT_FOLDER = File.join(File.dirname(__FILE__), '..')
7
+ SQL_FILE = File.join(ROOT_FOLDER, 'db', 'sqlite3.sql')
8
+ DB_FILE = File.join(ROOT_FOLDER, 'db', 'sqlite3.db')
9
+
10
+ class DBConnection
11
+ def self.open(db_file_name)
12
+ @db = SQLite3::Database.new(db_file_name)
13
+ @db.results_as_hash = true
14
+ @db.type_translation = true
15
+
16
+ @db
17
+ end
18
+
19
+ def self.reset
20
+ commands = [
21
+ "rm '#{DB_FILE}'",
22
+ "cat '#{SQL_FILE}' | sqlite3 '#{DB_FILE}'"
23
+ ]
24
+
25
+ commands.each { |command| `#{command}` }
26
+ DBConnection.open(DB_FILE)
27
+ end
28
+
29
+ def self.instance
30
+ reset if @db.nil?
31
+
32
+ @db
33
+ end
34
+
35
+ def self.execute(*args)
36
+ print_query(*args)
37
+ instance.execute(*args)
38
+ end
39
+
40
+ def self.execute2(*args)
41
+ print_query(*args)
42
+ instance.execute2(*args)
43
+ end
44
+
45
+ def self.last_insert_row_id
46
+ instance.last_insert_row_id
47
+ end
48
+
49
+ private
50
+
51
+ def self.print_query(query, *interpolation_args)
52
+ return unless PRINT_QUERIES
53
+
54
+ puts '--------------------'
55
+ puts query
56
+ unless interpolation_args.empty?
57
+ puts "interpolate: #{interpolation_args.inspect}"
58
+ end
59
+ puts '--------------------'
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,41 @@
1
+ require 'json'
2
+
3
+ module Raamen
4
+ class Flash
5
+ attr_reader :flash, :now
6
+
7
+ def initialize(req)
8
+ flash = req.cookies["_rails_lite_app_flash"]
9
+ @now = flash ? Now.new(JSON.parse(flash)) : Now.new({})
10
+ @flash = {}
11
+ end
12
+
13
+ def [](key)
14
+ self.now[key.to_sym] || self.flash[key.to_sym]
15
+ end
16
+
17
+ def []=(key, val)
18
+ self.flash[key.to_sym] = val
19
+ end
20
+
21
+ def store_flash(res)
22
+ res.set_cookie("_rails_lite_app_flash", {path: "/", value: self.flash.to_json})
23
+ end
24
+ end
25
+
26
+ class Now
27
+ attr_reader :now
28
+
29
+ def initialize(now)
30
+ @now = Hash[now.map{ |k,v| [k.to_sym, v] }]
31
+ end
32
+
33
+ def [](key)
34
+ self.now[key.to_sym]
35
+ end
36
+
37
+ def []=(key, val)
38
+ self.now[key.to_sym] = val
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,62 @@
1
+ module Raamen
2
+ class Route
3
+ attr_reader :pattern, :http_method, :controller_class, :action_name
4
+
5
+ def initialize(pattern, http_method, controller_class, action_name)
6
+ @pattern = pattern
7
+ @http_method = http_method
8
+ @controller_class = controller_class
9
+ @action_name = action_name
10
+ end
11
+
12
+ def matches?(req)
13
+ req.path =~ self.pattern &&
14
+ req.request_method == self.http_method.to_s.upcase
15
+ end
16
+
17
+ def run(req, res)
18
+ match_data = self.pattern.match(req.path)
19
+ route_params = Hash[match_data.names.zip(match_data.captures)]
20
+ self.controller_class.new(req, res, route_params)
21
+ .invoke_action(self.action_name)
22
+ end
23
+ end
24
+
25
+ class Router
26
+ attr_reader :routes
27
+
28
+ def initialize
29
+ @routes = []
30
+ end
31
+
32
+ def add_route(pattern, method, controller_class, action_name)
33
+ self.routes.push(Route.new(pattern, method, controller_class, action_name))
34
+ end
35
+
36
+ def draw(&proc)
37
+ self.instance_eval(&proc)
38
+ end
39
+
40
+ [:get, :post, :put, :delete].each do |http_method|
41
+ define_method(http_method) do |pattern, controller_class, action_name|
42
+ add_route(pattern, http_method, controller_class, action_name)
43
+ end
44
+ end
45
+
46
+ def match(req)
47
+ self.routes.each do |route|
48
+ return route if route.matches?(req)
49
+ end
50
+ nil
51
+ end
52
+
53
+ def run(req, res)
54
+ matching_route = match(req)
55
+ unless matching_route
56
+ res.status = 404
57
+ return
58
+ end
59
+ matching_route.run(req, res)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,25 @@
1
+ require 'json'
2
+
3
+ module Raamen
4
+ class Session
5
+ attr_reader :cookies
6
+
7
+ def initialize(req)
8
+ cookies = req.cookies["_rails_lite_app"]
9
+ cookies = Hash[JSON.parse(cookies).map{ |k,v| [k.to_sym, v] }] if cookies
10
+ @cookies = cookies || {}
11
+ end
12
+
13
+ def [](key)
14
+ self.cookies[key.to_sym]
15
+ end
16
+
17
+ def []=(key, val)
18
+ self.cookies[key.to_sym] = val
19
+ end
20
+
21
+ def store_session(res)
22
+ res.set_cookie("_rails_lite_app", {path: "/", value: self.cookies.to_json})
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,66 @@
1
+ require 'erb'
2
+
3
+ module Raamen
4
+ class ShowExceptions
5
+ attr_reader :app
6
+
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ begin
13
+ self.app.call(env)
14
+ rescue Exception => e
15
+ render_exception(e)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def render_exception(e)
22
+ template_path = File.join(
23
+ File.dirname(__FILE__),
24
+ "templates",
25
+ "rescue.html.erb")
26
+ template_content = File.read(template_path)
27
+ content = ERB.new(template_content).result(binding)
28
+
29
+ res = Rack::Response.new
30
+ res.status = 500
31
+ res["Content-Type"] = "text/html"
32
+ res.write(content)
33
+ res.finish
34
+ end
35
+
36
+ def stack_trace_top(e)
37
+ e.backtrace[0].split(':')
38
+ end
39
+
40
+ def source_line_num(e)
41
+ stack_trace_top(e)[1].to_i
42
+ end
43
+
44
+ def error_source_file(e)
45
+ stack_trace_top(e)[0]
46
+ end
47
+
48
+ def extract_source(file)
49
+ source_file = File.open(file, 'r')
50
+ source_file.readlines
51
+ end
52
+
53
+ def format_source(source_lines, source_line_num)
54
+ start = [0, source_line_num - 3].max
55
+ lines = source_lines[start..(start + 5)]
56
+ Hash[*(start + 1..(lines.count + start)).zip(lines).flatten]
57
+ end
58
+
59
+ def extract_formatted_source(e)
60
+ source_file_name = error_source_file(e)
61
+ source_line_num = source_line_num(e)
62
+ source_lines = extract_source(source_file_name)
63
+ format_source(source_lines, source_line_num)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,120 @@
1
+ require_relative 'db_connection'
2
+ require_relative 'sql_object_modules/searchable'
3
+ require_relative 'sql_object_modules/associatable'
4
+ require 'active_support/inflector'
5
+
6
+ module Raamen
7
+ class SQLObject
8
+ extend Searchable
9
+ extend Associatable
10
+
11
+ def self.columns
12
+ @columns ||= DBConnection.execute2(<<-SQL).first.map(&:to_sym)
13
+ SELECT
14
+ *
15
+ FROM
16
+ #{self.table_name}
17
+ LIMIT
18
+ 0
19
+ SQL
20
+ end
21
+
22
+ def self.finalize!
23
+ self.columns.each do |col|
24
+ define_method(col) do
25
+ attributes[col]
26
+ end
27
+
28
+ define_method("#{col}=") do |value|
29
+ attributes[col] = value
30
+ end
31
+ end
32
+ end
33
+
34
+ def self.table_name=(table_name)
35
+ @table_name = table_name
36
+ end
37
+
38
+ def self.table_name
39
+ @table_name || self.name.tableize
40
+ end
41
+
42
+ def self.all
43
+ parse_all(results = DBConnection.execute(<<-SQL))
44
+ SELECT
45
+ *
46
+ FROM
47
+ #{self.table_name}
48
+ SQL
49
+ end
50
+
51
+ def self.parse_all(results)
52
+ results.map do |result|
53
+ self.new(result)
54
+ end
55
+ end
56
+
57
+ def self.find(id)
58
+ parse_all(DBConnection.execute(<<-SQL, id)).first
59
+ SELECT
60
+ *
61
+ FROM
62
+ #{self.table_name}
63
+ WHERE
64
+ id = ?
65
+ SQL
66
+ end
67
+
68
+ def initialize(params = {})
69
+ params.each do |col, value|
70
+ col = col.to_sym
71
+ raise "unknown attribute '#{col}'" unless self.class.columns.include?(col)
72
+ self.send("#{col}=", value)
73
+ end
74
+ end
75
+
76
+ def attributes
77
+ @attributes ||= {}
78
+ end
79
+
80
+ def attribute_values
81
+ self.class.columns.map do |col|
82
+ self.send(col)
83
+ end
84
+ end
85
+
86
+ def insert
87
+ columns = self.class.columns.drop(1)
88
+ col_names = columns.map(&:to_sym).join(", ")
89
+ question_marks = (["?"] * columns.count).join(", ")
90
+
91
+ DBConnection.execute(<<-SQL, attribute_values.drop(1))
92
+ INSERT INTO
93
+ #{self.class.table_name} (#{col_names})
94
+ VALUES
95
+ (#{question_marks})
96
+ SQL
97
+
98
+ self.id = DBConnection.last_insert_row_id
99
+ end
100
+
101
+ def update
102
+ set_line = self.class.columns.map do |col|
103
+ "#{col}= ?"
104
+ end.join(", ")
105
+
106
+ DBConnection.execute(<<-SQL, *attribute_values, id)
107
+ UPDATE
108
+ #{self.class.table_name}
109
+ SET
110
+ #{set_line}
111
+ WHERE
112
+ id = ?
113
+ SQL
114
+ end
115
+
116
+ def save
117
+ id.nil? ? insert : update
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,90 @@
1
+ module Raamen
2
+ class AssocOptions
3
+ attr_accessor(
4
+ :foreign_key,
5
+ :class_name,
6
+ :primary_key
7
+ )
8
+
9
+ def model_class
10
+ self.class_name.constantize
11
+ end
12
+
13
+ def table_name
14
+ model_class.table_name
15
+ end
16
+ end
17
+
18
+ class BelongsToOptions < AssocOptions
19
+ def initialize(name, options = {})
20
+ self.class_name = options[:class_name] || name.to_s.camelcase
21
+ self.foreign_key = options[:foreign_key] || (name.to_s + "_id").to_sym
22
+ self.primary_key = options[:primary_key] || :id
23
+ end
24
+ end
25
+
26
+ class HasManyOptions < AssocOptions
27
+ def initialize(name, self_class_name, options = {})
28
+ self.class_name = options[:class_name] || name.to_s.singularize.camelcase
29
+ self.foreign_key = options[:foreign_key] || (self_class_name.downcase + "_id").to_sym
30
+ self.primary_key = options[:primary_key] || :id
31
+ end
32
+ end
33
+
34
+ module Associatable
35
+ def belongs_to(name, options = {})
36
+ self.assoc_options[name] = BelongsToOptions.new(name, options)
37
+
38
+ define_method(name) do
39
+ options = self.class.assoc_options[name]
40
+ key_val = self.send(options.foreign_key)
41
+ options.model_class.where(options.primary_key => key_val).first
42
+ end
43
+ end
44
+
45
+ def has_many(name, options = {})
46
+ self.assoc_options[name] = HasManyOptions.new(name, self.name, options)
47
+
48
+ define_method(name) do
49
+ options = self.class.assoc_options[name]
50
+ key_val = self.send(options.primary_key)
51
+ options.model_class.where(options.foreign_key => key_val)
52
+ end
53
+ end
54
+
55
+ def has_one_through(name, through_name, source_name)
56
+ define_method(name) do
57
+ through_options = self.class.assoc_options[through_name]
58
+ source_options = through_options.model_class.assoc_options[source_name]
59
+
60
+ through_table = through_options.table_name
61
+ through_pk = through_options.primary_key
62
+ through_fk = through_options.foreign_key
63
+
64
+ source_table = source_options.table_name
65
+ source_pk = source_options.primary_key
66
+ source_fk = source_options.foreign_key
67
+
68
+ key_val = self.send(through_fk)
69
+ results = DBConnection.execute(<<-SQL, key_val)
70
+ SELECT
71
+ #{source_table}.*
72
+ FROM
73
+ #{through_table}
74
+ JOIN
75
+ #{source_table}
76
+ ON
77
+ #{through_table}.#{source_fk} = #{source_table}.#{source_pk}
78
+ WHERE
79
+ #{through_table}.#{through_pk} = ?
80
+ SQL
81
+
82
+ source_options.model_class.parse_all(results).first
83
+ end
84
+ end
85
+
86
+ def assoc_options
87
+ @assoc_options ||= {}
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,18 @@
1
+ module Raamen
2
+ module Searchable
3
+ def where(params)
4
+ where_line = params.keys.map do |col|
5
+ "#{col}= ?"
6
+ end.join(" AND ")
7
+
8
+ parse_all(DBConnection.execute(<<-SQL, *params.values))
9
+ SELECT
10
+ *
11
+ FROM
12
+ #{self.table_name}
13
+ where
14
+ #{where_line}
15
+ SQL
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,59 @@
1
+ module Raamen
2
+ class Static
3
+ attr_reader :app, :root, :file_server
4
+
5
+ def initialize(app)
6
+ @app = app
7
+ @root = :public
8
+ @file_server = FileServer.new(self.root)
9
+ end
10
+
11
+ def call(env)
12
+ req = Rack::Request.new(env)
13
+ path = req.path
14
+
15
+ if path.include?("/#{self.root}")
16
+ res = self.file_server.call(env)
17
+ else
18
+ res = self.app.call(env)
19
+ end
20
+
21
+ res
22
+ end
23
+ end
24
+
25
+ class FileServer
26
+ MIME_TYPES = {
27
+ ".txt" => "text/plain",
28
+ ".jpg" => "image/jpeg",
29
+ ".zip" => "application/zip"
30
+ }
31
+
32
+ def initialize(root)
33
+ @root = root
34
+ end
35
+
36
+ def call(env)
37
+ req = Rack::Request.new(env)
38
+ res = Rack::Response.new
39
+ file_path = File.join(
40
+ File.dirname(__FILE__),
41
+ "..",
42
+ req.path
43
+ )
44
+
45
+ if File.exist?(file_path)
46
+ extension = File.extname(file_path)
47
+ content_type = MIME_TYPES[extension]
48
+ file_content = File.read(file_path)
49
+ res["Content-Type"] = content_type
50
+ res.write(file_content)
51
+ else
52
+ res.status = 404
53
+ res.write("File not found")
54
+ end
55
+
56
+ res
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,52 @@
1
+ <head>
2
+ <style>
3
+ .header {
4
+ color: red;
5
+ }
6
+
7
+ .source-view {
8
+ border: 10px solid lightgray;
9
+ width: 800px;
10
+ }
11
+
12
+ .line {
13
+ white-space: pre-wrap;
14
+ }
15
+
16
+ .line-num {
17
+ }
18
+
19
+ .line.error {
20
+ color: red;
21
+ }
22
+ </style>
23
+ </head>
24
+
25
+ <body>
26
+ <h2 class='header'><%=e.class%>: <%=e.message%></h2>
27
+
28
+ <h4>Extracted source (around line <b><%=source_line_num(e)%></b>):</h4>
29
+
30
+ <div class='source-view'>
31
+ <table cellpadding="0" cellspacing="0">
32
+ <% extract_formatted_source(e).each do |line_num, line| %>
33
+ <tr>
34
+ <td>
35
+ <pre class='line-num'> <%=line_num %> </pre>
36
+ </td>
37
+ <td>
38
+ <pre class='line
39
+ <%= 'error' if line_num == source_line_num(e)%>'><%= line %></pre>
40
+ </td>
41
+ </tr>
42
+ <% end %>
43
+ </table>
44
+ </div>
45
+ <h5><%= File.expand_path(error_source_file(e)) %></h5>
46
+
47
+ <h3>Stack trace</h3>
48
+ <% e.backtrace.each do |stack_line| %>
49
+ <%= stack_line %>
50
+ <br>
51
+ <% end %>
52
+ </body>
@@ -1,3 +1,3 @@
1
1
  module Raamen
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: raamen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuan Gao
@@ -70,6 +70,19 @@ files:
70
70
  - bin/console
71
71
  - bin/setup
72
72
  - lib/raamen.rb
73
+ - lib/raamen/controller_base.rb
74
+ - lib/raamen/db/sqlite3.db
75
+ - lib/raamen/db/sqlite3.sql
76
+ - lib/raamen/db_connection.rb
77
+ - lib/raamen/flash.rb
78
+ - lib/raamen/router.rb
79
+ - lib/raamen/session.rb
80
+ - lib/raamen/show_exceptions.rb
81
+ - lib/raamen/sql_object.rb
82
+ - lib/raamen/sql_object_modules/associatable.rb
83
+ - lib/raamen/sql_object_modules/searchable.rb
84
+ - lib/raamen/static.rb
85
+ - lib/raamen/templates/rescue.html.erb
73
86
  - lib/raamen/version.rb
74
87
  - raamen.gemspec
75
88
  homepage: https://github.com/yuangaonyc/raamen