puffs 0.1.1

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.
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