puffs 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.byebug_history +4 -0
  4. data/.gitignore +11 -0
  5. data/.rspec +2 -0
  6. data/.travis.yml +4 -0
  7. data/Gemfile +9 -0
  8. data/Gemfile.lock +52 -0
  9. data/LICENSE.txt +21 -0
  10. data/Rakefile +6 -0
  11. data/app/controllers/cats_controller.rb +26 -0
  12. data/app/controllers/house_controller.rb +3 -0
  13. data/app/controllers/humans_controller.rb +3 -0
  14. data/app/models/cat.rb +7 -0
  15. data/app/models/house.rb +7 -0
  16. data/app/models/human.rb +8 -0
  17. data/app/views/cats_controller/index.html.erb +5 -0
  18. data/app/views/cats_controller/new.html.erb +11 -0
  19. data/app/views/cats_controller/show.html.erb +1 -0
  20. data/app/views/dogs_controller/index.html.erb +8 -0
  21. data/app/views/dogs_controller/new.html.erb +17 -0
  22. data/app/views/dogs_controller/show.html.erb +1 -0
  23. data/app/views/my_controller/counting_show.html.erb +1 -0
  24. data/app/views/my_controller/show.html.erb +1 -0
  25. data/bin/puffs +68 -0
  26. data/bin/rake +37 -0
  27. data/config/routes.rb +9 -0
  28. data/db/migrate/201600202411059024_create_cats.sql +5 -0
  29. data/db/migrate/201600202411059044_create_houses.sql +4 -0
  30. data/db/migrate/201600202411059044_create_humans.sql +6 -0
  31. data/db/seeds.rb +26 -0
  32. data/exit +1 -0
  33. data/lib/controller_base.rb +74 -0
  34. data/lib/db_connection.rb +112 -0
  35. data/lib/puffs.rb +7 -0
  36. data/lib/relation.rb +206 -0
  37. data/lib/router.rb +77 -0
  38. data/lib/server_connection.rb +19 -0
  39. data/lib/session.rb +28 -0
  40. data/lib/sql_object/associatable.rb +97 -0
  41. data/lib/sql_object/sql_object.rb +156 -0
  42. data/lib/version.rb +3 -0
  43. data/puffs.gemspec +33 -0
  44. metadata +132 -0
data/lib/relation.rb ADDED
@@ -0,0 +1,206 @@
1
+ require_relative './../lib/db_connection'
2
+ require_relative 'sql_object/sql_object'
3
+
4
+ class SQLRelation
5
+ def self.build_association(base, included, method_name)
6
+ base.included_relations << included
7
+
8
+ assoc_options = base.klass.assoc_options[method_name]
9
+ has_many = assoc_options.class == HasManyOptions
10
+
11
+ if has_many
12
+ i_send = assoc_options.foreign_key
13
+ b_send = assoc_options.primary_key
14
+ else
15
+ i_send = assoc_options.primary_key
16
+ b_send = assoc_options.foreign_key
17
+ end
18
+
19
+ match = proc do
20
+ selection = included.select do |i_sql_obj|
21
+ i_sql_obj.send(i_send) == self.send(b_send)
22
+ end
23
+
24
+ associated = has_many ? selection : selection.first
25
+
26
+ #After we find our values iteratively, we overwrite the method again
27
+ #to the result values to reduce future lookup time to O(1).
28
+ new_match = proc { associated }
29
+ SQLObject.define_singleton_method_by_proc(
30
+ self, method_name, new_match)
31
+
32
+ associated
33
+ end
34
+
35
+ #we overwrite the association method for each SQLObject in the
36
+ #collection so that it points to our cached relation and doesn't fire a query.
37
+ base.collection.each do |b_sql_obj|
38
+ SQLObject.define_singleton_method_by_proc(
39
+ b_sql_obj, method_name, match)
40
+ end
41
+ end
42
+
43
+ attr_reader :klass, :collection, :loaded, :sql_count, :sql_limit
44
+ attr_accessor :included_relations
45
+
46
+ def initialize(options)
47
+ defaults =
48
+ {
49
+ klass: nil,
50
+ loaded: false,
51
+ collection: []
52
+ }
53
+
54
+ @klass = options[:klass]
55
+ @collection = options[:collection] || defaults[:collection]
56
+ @loaded = options[:loaded] || defaults[:loaded]
57
+ end
58
+
59
+ def <<(item)
60
+ if item.class == klass
61
+ @collection << item
62
+ end
63
+ end
64
+
65
+ def count
66
+ @sql_count = true
67
+ load
68
+ end
69
+
70
+ def included_relations
71
+ @included_relations ||= []
72
+ end
73
+
74
+ def includes(klass)
75
+ includes_params << klass
76
+ self
77
+ end
78
+
79
+ def includes_params
80
+ @includes_params ||= []
81
+ end
82
+
83
+ def limit(n)
84
+ @sql_limit = n
85
+ self
86
+ end
87
+
88
+ def load
89
+ if !loaded
90
+ puts "LOADING #{table_name}"
91
+ results = DBConnection.execute(<<-SQL, sql_params[:values])
92
+ SELECT
93
+ #{sql_count ? "COUNT(*)" : self.table_name.to_s + ".*"}
94
+ FROM
95
+ #{self.table_name}
96
+ #{sql_params[:where]}
97
+ #{sql_params[:params]}
98
+ #{order_by_string}
99
+ #{"LIMIT #{sql_limit}" if sql_limit};
100
+ SQL
101
+
102
+ results = sql_count ? results.first.values.first : parse_all(results)
103
+ end
104
+
105
+ results = results || self
106
+
107
+ unless includes_params.empty?
108
+ results = load_includes(results)
109
+ end
110
+
111
+ results
112
+ end
113
+
114
+ def load_includes(relation)
115
+ includes_params.each do |param|
116
+ if relation.klass.has_association?(param)
117
+ puts "LOADING #{param.to_s}"
118
+ assoc = klass.assoc_options[param]
119
+ f_k = assoc.foreign_key
120
+ p_k = assoc.primary_key
121
+ includes_table = assoc.table_name.to_s
122
+ in_ids = relation.collection.map do |sqlobject|
123
+ sqlobject.id
124
+ end.join(", ")
125
+
126
+ has_many = assoc.class == HasManyOptions
127
+
128
+ results = DBConnection.execute(<<-SQL)
129
+ SELECT
130
+ #{includes_table}.*
131
+ FROM
132
+ #{includes_table}
133
+ WHERE
134
+ #{includes_table}.#{has_many ? f_k : p_k}
135
+ IN
136
+ (#{in_ids});
137
+ SQL
138
+ included = assoc.model_class.parse_all(results)
139
+ SQLRelation.build_association(relation, included, param)
140
+ end
141
+ end
142
+
143
+ relation
144
+ end
145
+
146
+ def method_missing(method, *args, &block)
147
+ self.to_a.send(method, *args, &block)
148
+ end
149
+
150
+ def order(params)
151
+ if params.is_a?(Hash)
152
+ order_params_hash.merge!(params)
153
+ else
154
+ order_params_hash.merge!(params => :asc)
155
+ end
156
+ self
157
+ end
158
+
159
+ def order_params_hash
160
+ @order_params_hash ||= {}
161
+ end
162
+
163
+ def order_by_string
164
+ hash_string = order_params_hash.map do |column, asc_desc|
165
+ "#{column} #{asc_desc.to_s.upcase}"
166
+ end.join(", ")
167
+
168
+ hash_string.empty? ? "" : "ORDER BY #{hash_string}"
169
+ end
170
+
171
+ def parse_all(attributes)
172
+ klass.parse_all(attributes).where(where_params_hash).includes(includes_params)
173
+ end
174
+
175
+ def sql_params
176
+ params, values = [], []
177
+
178
+ i = 1
179
+ where_params_hash.map do |attribute, value|
180
+ params << "#{attribute} = $#{i}"
181
+ values << value
182
+ i += 1
183
+ end
184
+
185
+ { params: params.join(" AND "),
186
+ where: params.empty? ? nil : "WHERE",
187
+ values: values }
188
+ end
189
+
190
+ def table_name
191
+ klass.table_name
192
+ end
193
+
194
+ def to_a
195
+ self.load.collection
196
+ end
197
+
198
+ def where_params_hash
199
+ @where_params_hash ||= {}
200
+ end
201
+
202
+ def where(params)
203
+ where_params_hash.merge!(params)
204
+ self
205
+ end
206
+ end
data/lib/router.rb ADDED
@@ -0,0 +1,77 @@
1
+ project_root = File.dirname(File.absolute_path(__FILE__))
2
+ Dir.glob(project_root + '/../app/controllers/*.rb') { |file| require file }
3
+
4
+ class Route
5
+ attr_reader :pattern, :http_method, :controller_class, :action_name
6
+
7
+ def initialize(pattern, http_method, controller_class, action_name)
8
+ @pattern = pattern
9
+ @http_method = http_method
10
+ @controller_class = controller_class
11
+ @action_name = action_name
12
+ end
13
+
14
+ # checks if pattern matches path and method matches request method
15
+ def matches?(req)
16
+ pattern =~ req.path && req.request_method == http_method.to_s.upcase
17
+ end
18
+
19
+ # use pattern to pull out route params (save for later?)
20
+ # instantiate controller and call controller action
21
+ def run(req, res)
22
+ matches = pattern.match(req.path)
23
+ route_params = {}
24
+
25
+ matches.names.each do |name|
26
+ route_params[name] = matches[name]
27
+ end
28
+
29
+ controller_class.new(req, res, route_params).invoke_action(action_name)
30
+ end
31
+ end
32
+
33
+ class Router
34
+ attr_reader :routes
35
+
36
+ def initialize
37
+ @routes = []
38
+ end
39
+
40
+ # simply adds a new route to the list of routes
41
+ def add_route(pattern, method, controller_class, action_name)
42
+ routes << Route.new(pattern, method, controller_class, action_name)
43
+ end
44
+
45
+ # evaluate the proc in the context of the instance
46
+ # for syntactic sugar :)
47
+ def draw(&proc)
48
+ instance_eval(&proc)
49
+ end
50
+
51
+ # make each of these methods that
52
+ # when called add route
53
+ [:get, :post, :put, :delete].each do |http_method|
54
+ define_method(http_method) do |pattern, controller_class, action_name|
55
+ add_route(pattern, http_method, controller_class, action_name)
56
+ end
57
+ end
58
+
59
+ # should return the route that matches this request
60
+ def match(req)
61
+ routes.each do |route|
62
+ return route if route.matches?(req)
63
+ end
64
+ nil
65
+ end
66
+
67
+ # either throw 404 or call run on a matched route
68
+ def run(req, res)
69
+ route = match(req)
70
+ if route
71
+ route.run(req, res)
72
+ else
73
+ res.status = 404
74
+ res.body = ["Sorry, Charlie. That page doesn't exist."]
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,19 @@
1
+ require 'rack'
2
+ require_relative 'router'
3
+ require_relative '../config/routes'
4
+
5
+ class ServerConnection
6
+ def self.start
7
+ app = Proc.new do |env|
8
+ req = Rack::Request.new(env)
9
+ res = Rack::Response.new
10
+ ROUTER.run(req, res)
11
+ res.finish
12
+ end
13
+
14
+ Rack::Server.start(
15
+ app: app,
16
+ Port: 3000
17
+ )
18
+ end
19
+ end
data/lib/session.rb ADDED
@@ -0,0 +1,28 @@
1
+ require 'json'
2
+
3
+ class Session
4
+ # find the cookie for this app
5
+ # deserialize the cookie into a hash
6
+ def initialize(req)
7
+ cookie = req.cookies['_rails_lite_app']
8
+ if cookie
9
+ @session_data = JSON.parse(cookie)
10
+ else
11
+ @session_data = {}
12
+ end
13
+ end
14
+
15
+ def [](key)
16
+ @session_data[key]
17
+ end
18
+
19
+ def []=(key, val)
20
+ @session_data[key] = val
21
+ end
22
+
23
+ # serialize the hash into json and save in a cookie
24
+ # add to the responses cookies
25
+ def store_session(res)
26
+ res.set_cookie("_rails_lite_app", { path: '/', value: @session_data.to_json })
27
+ end
28
+ end
@@ -0,0 +1,97 @@
1
+ require 'active_support/inflector'
2
+
3
+ class AssocOptions
4
+ attr_accessor(
5
+ :foreign_key,
6
+ :class_name,
7
+ :primary_key
8
+ )
9
+
10
+ def model_class
11
+ class_name.constantize
12
+ end
13
+
14
+ def table_name
15
+ model_class.table_name
16
+ end
17
+ end
18
+
19
+ class BelongsToOptions < AssocOptions
20
+ def initialize(name, options = {})
21
+ @primary_key = options[:primary_key] || :id
22
+ @foreign_key = options[:foreign_key] || "#{name}_id".to_sym
23
+ @class_name = options[:class_name] || name.to_s.capitalize
24
+ end
25
+ end
26
+
27
+ class HasManyOptions < AssocOptions
28
+ def initialize(name, self_class_name, options = {})
29
+ @primary_key = options[:primary_key] || :id
30
+ @foreign_key = options[:foreign_key] || "#{self_class_name.to_s.underscore}_id".to_sym
31
+ @class_name = options[:class_name] || name.to_s.singularize.camelcase
32
+ end
33
+ end
34
+
35
+ module Associatable
36
+ def belongs_to(name, options = {})
37
+ options = BelongsToOptions.new(name, options)
38
+ assoc_options[name] = options
39
+
40
+ define_method(name) do
41
+ foreign_key_value = self.send(options.foreign_key)
42
+ return nil if foreign_key_value.nil?
43
+
44
+ options.model_class
45
+ .where(options.primary_key => foreign_key_value)
46
+ .first
47
+ end
48
+ end
49
+
50
+ def has_many(name, options = {})
51
+ options = HasManyOptions.new(name, self.to_s, options)
52
+ assoc_options[name] = options
53
+
54
+ define_method(name) do
55
+ target_key_value = self.send(options.primary_key)
56
+ return nil if target_key_value.nil?
57
+ options.model_class
58
+ .where(options.foreign_key => target_key_value)
59
+ .to_a
60
+ end
61
+ end
62
+
63
+ def assoc_options
64
+ @assoc_options ||= {}
65
+ end
66
+
67
+ def has_one_through(name, through_name, source_name)
68
+ through_options = assoc_options[through_name]
69
+
70
+ define_method(name) do
71
+ source_options =
72
+ through_options.model_class.assoc_options[source_name]
73
+ through_pk = through_options.primary_key
74
+ key_val = self.send(through_options.foreign_key)
75
+
76
+ source_options.model_class.includes(through_options.model_class)
77
+ .where(through_pk => key_val).first
78
+ end
79
+ end
80
+
81
+ def has_many_through(name, through_name, source_name)
82
+ through_options = assoc_options[through_name]
83
+ define_method(name) do
84
+ through_fk = through_options.foreign_key
85
+ through_class = through_options.model_class
86
+ key_val = self.send(through_options.primary_key)
87
+
88
+ #2 queries, we could reduce to 1 by writing SQLRelation::join.
89
+ through_class.where(through_fk => key_val)
90
+ .includes(source_name)
91
+ .load
92
+ .included_relations
93
+ .first
94
+ .to_a
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,156 @@
1
+ require_relative '../../lib/db_connection'
2
+ require_relative 'associatable'
3
+ require_relative '../relation'
4
+ require 'active_support/inflector'
5
+
6
+ class SQLObject
7
+ extend Associatable
8
+
9
+ RELATION_METHODS = [
10
+ :limit, :includes, :where, :order
11
+ ]
12
+
13
+ RELATION_METHODS.each do |method|
14
+ define_singleton_method(method) do |arg|
15
+ SQLRelation.new(klass: self).send(method, arg)
16
+ end
17
+ end
18
+
19
+ def self.all
20
+ where({})
21
+ end
22
+
23
+ def self.columns
24
+ DBConnection.columns(table_name)
25
+ end
26
+
27
+ def self.count
28
+ all.count
29
+ end
30
+
31
+ def self.define_singleton_method_by_proc(obj, name, block)
32
+ metaclass = class << obj; self; end
33
+ metaclass.send(:define_method, name, block)
34
+ end
35
+
36
+ def self.destroy_all!
37
+ self.all.each do |entry|
38
+ entry.destroy!
39
+ end
40
+ end
41
+
42
+ def self.finalize!
43
+ self.columns.each do |column|
44
+ define_method(column) do
45
+ attributes[column]
46
+ end
47
+
48
+ define_method("#{column}=") do |new_value|
49
+ attributes[column] = new_value
50
+ end
51
+ end
52
+ end
53
+
54
+ def self.find(id)
55
+ where(id: id).first
56
+ end
57
+
58
+ def self.first
59
+ all.limit(1).first
60
+ end
61
+
62
+ def self.has_association?(association)
63
+ assoc_options.keys.include?(association)
64
+ end
65
+
66
+ def self.last
67
+ all.order(id: :desc).limit(1).first
68
+ end
69
+
70
+ def self.parse_all(results)
71
+ relation = SQLRelation.new(klass: self, loaded: true)
72
+ results.each do |result|
73
+ relation << self.new(result)
74
+ end
75
+
76
+ relation
77
+ end
78
+
79
+ def self.table_name=(table_name)
80
+ @table_name = table_name
81
+ end
82
+
83
+ def self.table_name
84
+ @table_name ||= self.to_s.downcase.tableize
85
+ end
86
+
87
+ def initialize(params = {})
88
+ params.each do |attr_name, value|
89
+ unless self.class.columns.include?(attr_name.to_sym)
90
+ raise "unknown attribute '#{attr_name}'"
91
+ end
92
+
93
+ self.send("#{attr_name}=", value)
94
+ end
95
+ end
96
+
97
+ def attributes
98
+ @attributes ||= {}
99
+ end
100
+
101
+ def attribute_values
102
+ self.class.columns.map do |column|
103
+ self.send(column)
104
+ end
105
+ end
106
+
107
+ def destroy!
108
+ if self.class.find(id)
109
+ DBConnection.execute(<<-SQL)
110
+ DELETE
111
+ FROM
112
+ #{self.class.table_name}
113
+ WHERE
114
+ id = #{id}
115
+ SQL
116
+ return self
117
+ end
118
+ end
119
+
120
+ def insert
121
+ columns = self.class.columns.reject { |col| col == :id }
122
+ column_values = columns.map {|attr_name| send(attr_name)}
123
+ column_names = columns.join(", ")
124
+ bind_params = (1..columns.length).map {|n| "$#{n}"}.join(", ")
125
+ result = DBConnection.execute(<<-SQL, column_values)
126
+ INSERT INTO
127
+ #{self.class.table_name} (#{column_names})
128
+ VALUES
129
+ (#{bind_params})
130
+ RETURNING id;
131
+ SQL
132
+ self.id = result.first['id']
133
+ self
134
+ end
135
+
136
+ def save
137
+ self.class.find(id) ? update : insert
138
+ end
139
+
140
+ def update
141
+ set_line = self.class.columns.map do |column|
142
+ #bobby tables says hi!
143
+ "#{column} = \'#{self.send(column)}\'"
144
+ end.join(", ")
145
+
146
+ DBConnection.execute(<<-SQL)
147
+ UPDATE
148
+ #{self.class.table_name}
149
+ SET
150
+ #{set_line}
151
+ WHERE
152
+ id = #{id}
153
+ SQL
154
+ self
155
+ end
156
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Puffs
2
+ VERSION = "0.1.1"
3
+ end
data/puffs.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "puffs"
9
+ spec.version = Puffs::VERSION
10
+ spec.authors = ["Zachary Moroni"]
11
+ spec.email = ["zachary.moroni@gmail.com"]
12
+
13
+ spec.summary = %q{A simple ORM and MVC inspired by Rails.}
14
+ spec.description = %q{Coming Soon...}
15
+ spec.homepage = "http://github.com/snackzone"
16
+ spec.license = "MIT"
17
+
18
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
19
+ # delete this section to allow pushing this gem to any host.
20
+ if spec.respond_to?(:metadata)
21
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
22
+ else
23
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
24
+ end
25
+
26
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
27
+ spec.bindir = "bin"
28
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
29
+
30
+ spec.add_development_dependency "bundler", "~> 1.11"
31
+ spec.add_development_dependency "rake", "~> 10.0"
32
+ spec.add_development_dependency "rspec", "~> 3.0"
33
+ end