gazebo 0.1.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a7e1c03436e96b489059927089d42de8a98f07ff
4
+ data.tar.gz: f01dc87cbb489db33f283a2467a5f884992ad235
5
+ SHA512:
6
+ metadata.gz: 2ac34da08d89d7f35e323af6eda2800c72187d66d8a0207bafe65d73591b771b57549031dd5f6720c2f831cede9fc990ae8442b824d8031082cb6c6f8c373d8c
7
+ data.tar.gz: 61aff4beade614102cef92417327088d6f7abe458c30caca8e363f539b72851c344e6d9f7beb5de5d84f06cc7210dee1ab053cb2809696183efa6768a6002cae
data/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # Gazebo
2
+
3
+ A light-weight MVC framework inspired by Rails.
4
+
5
+ ## ActiveLeopard
6
+ An ORM with many of the features of Rails ActiveRecord at a fraction of the overhead.
7
+
8
+ ## ActionCondor
9
+ A Controller Base Class that is combined with a custom router and asset server to handle requests and build responses.
data/bin/gazebo ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby -U
2
+
3
+ require 'gazebo'
4
+
5
+ Gazebo.root = File.expand_path(Dir.pwd)
6
+ Gazebo::DatabaseTasks.start( ARGV )
@@ -0,0 +1,17 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
3
+ require 'active_support/inflector'
4
+ require 'erb'
5
+ require 'byebug'
6
+ require 'rack'
7
+ require 'json'
8
+
9
+ require_relative 'controller_base'
10
+ require_relative 'flash'
11
+ require_relative 'session'
12
+
13
+ class ActionCondor
14
+ end
15
+
16
+ class ActionCondor::Base < ControllerBase
17
+ end
@@ -0,0 +1,111 @@
1
+ class ControllerBase
2
+ attr_reader :req, :res, :params, :token, :flash
3
+
4
+
5
+ def self.protect_from_forgery
6
+ @@protect_from_forgery = true
7
+ end
8
+
9
+ # Setup the controller
10
+ def initialize(req, res, route_params = {})
11
+ @req = req
12
+ @res = res
13
+ @params = route_params.merge(req.params)
14
+ @@protect_from_forgery ||= false
15
+ end
16
+
17
+ def flash
18
+ @flash ||= Flash.new(req)
19
+ end
20
+
21
+ # Helper method to alias @already_built_response
22
+ def already_built_response?
23
+ @already_built_response || false
24
+ end
25
+
26
+ # Set the response status code and header
27
+ def redirect_to(url)
28
+ check_for_repeat_action!
29
+ res.status = 302
30
+ res['location'] = url
31
+
32
+ session.store_session(res)
33
+ flash.store_flash(res)
34
+ @already_built_response = true
35
+ end
36
+
37
+ # Populate the response with content.
38
+ # Set the response's content type to the given type.
39
+ # Raise an error if the developer tries to double render.
40
+ def render_content(content, content_type)
41
+ check_for_repeat_action!
42
+ res['Content-Type'] = content_type
43
+ res.write(content)
44
+
45
+ session.store_session(res)
46
+ flash.store_flash(res)
47
+ @already_built_response = true
48
+ end
49
+
50
+ #raise error if already_built_response
51
+ def check_for_repeat_action!
52
+ raise "Cannot call render/redirect twice in one action" if already_built_response?
53
+ end
54
+
55
+ # use ERB and binding to evaluate templates
56
+ # pass the rendered html to render_content
57
+ def render(template_name)
58
+ controller_name = self.class.to_s.underscore[0..-("_controller".length + 1)]
59
+ file_path = "app/views/#{controller_name}/#{template_name}.html.erb"
60
+ file_content = File.read(file_path)
61
+
62
+ application = File.read("app/views/layout/application.html.erb")
63
+ application.sub!(/__YIELD__/, file_content)
64
+
65
+ content = ERB.new(application).result(binding)
66
+ render_content(content, 'text/html')
67
+ end
68
+
69
+ # method exposing a `Session` object
70
+ def session
71
+ @session ||= Session.new(req)
72
+ end
73
+
74
+ # use this with the router to call action_name (:index, :show, :create...)
75
+ def invoke_action(name)
76
+ if protect_from_forgery? && req.request_method != 'GET'
77
+ check_authenticity_token
78
+ else
79
+ form_authenticity_token
80
+ end
81
+
82
+ send(name)
83
+ render(name) unless already_built_response?
84
+ end
85
+
86
+ def protect_from_forgery?
87
+ @@protect_from_forgery
88
+ end
89
+
90
+ def check_authenticity_token
91
+ cookie = req.cookies['authenticity_token']
92
+ unless cookie && cookie == params['authenticity_token']
93
+ raise "Invalid authenticity token"
94
+ end
95
+ end
96
+
97
+ def form_authenticity_token
98
+ @token ||= generate_authenticity_token
99
+ res.set_cookie(
100
+ 'authenticity_token',
101
+ path: '/',
102
+ value: token
103
+ )
104
+
105
+ @token
106
+ end
107
+
108
+ def generate_authenticity_token
109
+ SecureRandom.urlsafe_base64(16)
110
+ end
111
+ end
@@ -0,0 +1,31 @@
1
+ class Flash
2
+ def initialize(req)
3
+ @flash = {}
4
+
5
+ app_cookie = req.cookies['_gazebo_app_flash']
6
+
7
+ @flash_now = app_cookie ? JSON.parse(app_cookie) : {}
8
+ end
9
+
10
+ def now
11
+ @flash_now
12
+ end
13
+
14
+ def [](key)
15
+ @flash[key.to_s] || @flash_now[key.to_s] || @flash_now[key]
16
+ end
17
+
18
+ def []=(key, val)
19
+ @flash[key.to_s] = val
20
+ end
21
+
22
+ # serialize the hash into json and save in a cookie
23
+ # add to the responses cookies
24
+ def store_flash(res)
25
+ res.set_cookie(
26
+ '_gazebo_app_flash',
27
+ path: '/',
28
+ value: @flash.to_json
29
+ )
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+ class Session
2
+ # find the cookie for this app
3
+ # deserialize the cookie into a hash
4
+ attr_reader :cookie
5
+
6
+ def initialize(req)
7
+ app_cookie = req.cookies['_gazebo_app']
8
+
9
+ @cookie = app_cookie ? JSON.parse(app_cookie) : {}
10
+ end
11
+
12
+ def [](key)
13
+ @cookie[key.to_s]
14
+ end
15
+
16
+ def []=(key, val)
17
+ @cookie[key.to_s] = val
18
+ end
19
+
20
+ # serialize the hash into json and save in a cookie
21
+ # add to the responses cookies
22
+ def store_session(res)
23
+ res.set_cookie(
24
+ '_gazebo_app',
25
+ path: '/',
26
+ value: @cookie.to_json
27
+ )
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ require 'active_support/inflector'
2
+ require 'colorize'
3
+
4
+ require_relative 'query_clauses/all_clauses'
5
+ require_relative 'modules/associatable'
6
+ require_relative 'modules/validatable'
7
+ require_relative 'modules/searchable'
8
+ require_relative 'assoc_options'
9
+ require_relative 'db_connection'
10
+ require_relative 'sql_object'
11
+ require_relative 'relation'
12
+ require_relative 'errors'
13
+
14
+ class ActiveLeopard
15
+ end
16
+
17
+ class ActiveLeopard::Base < SQLObject
18
+ extend Associatable
19
+ extend Searchable
20
+ extend Validatable
21
+ end
@@ -0,0 +1,64 @@
1
+ class AssocOptions
2
+ attr_accessor(
3
+ :foreign_key,
4
+ :class_name,
5
+ :primary_key
6
+ )
7
+
8
+ def model_class
9
+ class_name.constantize
10
+ end
11
+
12
+ def table_name
13
+ model_class.table_name
14
+ end
15
+ end
16
+
17
+ class BelongsToOptions < AssocOptions
18
+ def initialize(name, options = {})
19
+ name = name.to_s.singularize
20
+
21
+ defaults = {
22
+ foreign_key: ("#{name.underscore}_id").to_sym,
23
+ class_name: name.camelcase,
24
+ primary_key: :id
25
+ }
26
+
27
+ defaults.merge(options).each do |option, opt_name|
28
+ send("#{option}=", opt_name)
29
+ end
30
+ end
31
+
32
+ def own_join_column
33
+ foreign_key
34
+ end
35
+
36
+ def other_join_column
37
+ primary_key
38
+ end
39
+ end
40
+
41
+ class HasManyOptions < AssocOptions
42
+ def initialize(name, self_class_name, options = {})
43
+ name = name.to_s.singularize
44
+ self_class_name = self_class_name.to_s.singularize
45
+
46
+ defaults = {
47
+ foreign_key: ("#{self_class_name.underscore}_id").to_sym,
48
+ class_name: name.camelcase,
49
+ primary_key: :id
50
+ }
51
+
52
+ defaults.merge(options).each do |option, opt_name|
53
+ send("#{option}=", opt_name)
54
+ end
55
+ end
56
+
57
+ def own_join_column
58
+ primary_key
59
+ end
60
+
61
+ def other_join_column
62
+ foreign_key
63
+ end
64
+ end
@@ -0,0 +1,136 @@
1
+ require 'pg'
2
+ PRINT_QUERIES = true
3
+
4
+ class DBConnection
5
+ def self.open
6
+ if ENV['DATABASE_URL']
7
+ self.open_production
8
+ else
9
+ self.open_development
10
+ end
11
+ run_migrations
12
+ end
13
+
14
+ def self.open_production
15
+ uri = URI.parse(ENV['DATABASE_URL'])
16
+
17
+ @db = PG::Connection.new(
18
+ user: uri.user,
19
+ password: uri.password,
20
+ host: uri.host,
21
+ port: uri.port,
22
+ dbname: uri.path[1..-1]
23
+ )
24
+ end
25
+
26
+ def self.open_development
27
+ begin
28
+ @db = PG::Connection.open(dbname: self.database_name)
29
+ rescue PG::ConnectionBad => e
30
+ create_database!
31
+ retry
32
+ end
33
+
34
+ @db
35
+ end
36
+
37
+ def self.ensure_migrations_table!
38
+ begin
39
+ execute("SELECT * FROM migrations")
40
+ rescue PG::UndefinedTable
41
+ execute(<<-SQL)
42
+ CREATE TABLE MIGRATIONS(
43
+ ID SERIAL PRIMARY KEY NOT NULL,
44
+ NAME CHAR(50) NOT NULL,
45
+ CREATED_AT CHAR(50) NOT NULL
46
+ )
47
+ SQL
48
+ end
49
+ end
50
+
51
+ def self.run_migrations
52
+ ensure_migrations_table!
53
+ migrations = Dir.entries("db/migrations").reject { |fname| fname.start_with?('.') }
54
+ migrations.sort_by! { |fname| Integer(fname[0..1]) }
55
+
56
+ migrations.each do |file_name|
57
+ migration_name = file_name.match(/\w+/).to_s
58
+
59
+ next if migration_name.empty? || already_run?(migration_name)
60
+
61
+ file = File.join(Gazebo::ROOT, "db/migrations", file_name)
62
+ migration_sql = File.read(file)
63
+
64
+ execute(migration_sql)
65
+
66
+ record_migration!(migration_name)
67
+ end
68
+ end
69
+
70
+ def self.record_migration!(migration_name)
71
+ time = Time.new.strftime("%Y%m%dT%H%M")
72
+ here_doc = <<-SQL
73
+ INSERT INTO
74
+ migrations (name, created_at)
75
+ VALUES
76
+ ($1, $2)
77
+ SQL
78
+
79
+ execute(here_doc, [migration_name, time])
80
+ end
81
+
82
+ def self.already_run?(migration_name)
83
+ !!execute(<<-SQL, [migration_name]).first
84
+ SELECT *
85
+ FROM migrations
86
+ WHERE name = $1
87
+ SQL
88
+ end
89
+
90
+ def self.create_database!
91
+ master_conn = PG::Connection.connect(dbname: 'postgres')
92
+ master_conn.exec("CREATE DATABASE #{database_name}")
93
+ end
94
+
95
+ def self.database_name
96
+ Gazebo::ROOT.split('/').last.gsub("-", "_") + '_development'
97
+ end
98
+
99
+ def self.instance
100
+ open if @db.nil?
101
+
102
+ @db
103
+ end
104
+
105
+ def self.execute(*args)
106
+ print_query(*args)
107
+ instance.exec(*args)
108
+ end
109
+
110
+ def self.async_exec(*args)
111
+ print_query(*args)
112
+ instance.send_query(*args)
113
+ end
114
+
115
+ def self.get_first_row(*args)
116
+ print_query(*args)
117
+ instance.exec(*args).first
118
+ end
119
+
120
+ private
121
+
122
+ def self.random_color
123
+ [:blue, :light_blue, :red, :green, :yellow].sample
124
+ end
125
+
126
+ def self.print_query(query, bind_params = [])
127
+ return unless PRINT_QUERIES
128
+
129
+ output = query.gsub(/\s+/, ' ')
130
+ unless bind_params.empty?
131
+ output += " #{bind_params.inspect}"
132
+ end
133
+
134
+ puts output.colorize(random_color)
135
+ end
136
+ end
@@ -0,0 +1,5 @@
1
+ class InvalidInput < StandardError
2
+ end
3
+
4
+ class RecordNotFound < StandardError
5
+ end
@@ -0,0 +1,86 @@
1
+ module Associatable
2
+ def belongs_to(name, options = {})
3
+ options = BelongsToOptions.new(name, options)
4
+ assoc_options[name] = options
5
+
6
+ define_method(name) do
7
+ foreign_key_val = self.send(options.foreign_key)
8
+ return nil if foreign_key_val.nil?
9
+
10
+ options
11
+ .model_class
12
+ .where(options.primary_key => foreign_key_val)
13
+ .first
14
+ end
15
+ end
16
+
17
+ def has_many(name, options = {})
18
+ options = HasManyOptions.new(name, self, options)
19
+ assoc_options[name] = options
20
+
21
+ define_method(name) do
22
+ primary_key_val = self.send(options.primary_key)
23
+
24
+ options.model_class
25
+ .where(options.foreign_key => primary_key_val)
26
+ end
27
+ end
28
+
29
+ def assoc_options
30
+ @assoc_options ||= {}
31
+ end
32
+
33
+ def has_one_through(name, through_name, source_name)
34
+ through_options = assoc_options[through_name]
35
+
36
+ define_method(name) do
37
+ source_options = through_options.model_class.assoc_options[source_name]
38
+
39
+ through_table = through_options.model_class.table_name
40
+ source_table = source_options.model_class.table_name
41
+
42
+ datum = DBConnection.execute(<<-SQL).first
43
+ SELECT
44
+ #{source_table}.*
45
+ FROM
46
+ #{through_table}
47
+ JOIN
48
+ #{source_table}
49
+ ON
50
+ #{through_table}.#{source_options.foreign_key} =
51
+ #{source_table}.#{source_options.primary_key}
52
+ WHERE
53
+ #{through_table}.#{through_options.primary_key} =
54
+ #{self.send(through_options.foreign_key)}
55
+ SQL
56
+
57
+ source_options.model_class.new(datum)
58
+ end
59
+ end
60
+
61
+ def has_many_through(name, through_name, source_name)
62
+ through_options = assoc_options[through_name]
63
+
64
+ define_method(name) do
65
+ source_options = through_options.model_class.assoc_options[source_name]
66
+ through_table = through_options.model_class.table_name
67
+ source_table = source_options.model_class.table_name
68
+
69
+ data = DBConnection.execute(<<-SQL)
70
+ SELECT
71
+ #{source_table}.*
72
+ FROM
73
+ #{through_table}
74
+ JOIN
75
+ #{source_table}
76
+ ON #{through_table}.#{source_options.primary_key} =
77
+ #{source_table}.#{source_options.foreign_key}
78
+ WHERE
79
+ #{through_table}.#{through_options.foreign_key} =
80
+ #{self.id}
81
+ SQL
82
+
83
+ data.map { |datum| source_options.model_class.new(datum) }
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,65 @@
1
+ module Searchable
2
+ def find_by(params)
3
+ where_clause = WhereClause.new([params])
4
+
5
+ search_datum = DBConnection.get_first_row(<<-SQL, where_clause.values)
6
+ SELECT
7
+ *
8
+ FROM
9
+ #{self.table_name}
10
+ #{where_clause.as_sql}
11
+ SQL
12
+
13
+ search_datum.nil? ? nil : self.new(search_datum)
14
+ end
15
+
16
+ def where(*params)
17
+ Relation.new(
18
+ {where: WhereClause.new(params)},
19
+ self
20
+ )
21
+ end
22
+
23
+ def find(id)
24
+ search_datum = DBConnection.get_first_row(<<-SQL)
25
+ SELECT
26
+ *
27
+ FROM
28
+ #{self.table_name}
29
+ WHERE
30
+ id = #{id}
31
+ SQL
32
+ raise RecordNotFound, "This record does not exist" if search_datum.nil?
33
+ self.new(search_datum)
34
+ end
35
+
36
+ def joins(association, _ = nil)
37
+ options = self.assoc_options[association]
38
+
39
+ Relation.new(
40
+ {join: JoinOptions.new(options, self.table_name)},
41
+ self
42
+ )
43
+ end
44
+
45
+ def select(*params)
46
+ Relation.new(
47
+ {select: SelectClause.new(params)},
48
+ self
49
+ )
50
+ end
51
+
52
+ def group(group_attr)
53
+ Relation.new(
54
+ {group: GroupClause.new(group_attr)},
55
+ self
56
+ )
57
+ end
58
+
59
+ def order(ordering_attr)
60
+ Relation.new(
61
+ {order: OrderClause.new(ordering_attr)},
62
+ self
63
+ )
64
+ end
65
+ end
@@ -0,0 +1,25 @@
1
+ module Validatable
2
+ def validates(attribute, options)
3
+ method_name = "validate_#{attribute}"
4
+
5
+ define_method(method_name) do
6
+ attr_val = self.send(attribute)
7
+
8
+ if options[:presence]
9
+ if attr_val.nil?
10
+ errors[attribute] << "can't be blank"
11
+ end
12
+ end
13
+
14
+ if options[:uniqueness]
15
+ matching_obj = self.class.find_by(attribute => attr_val)
16
+
17
+ unless matching_obj.nil? || matching_obj.id == self.id
18
+ errors[attribute] << "must be unique"
19
+ end
20
+ end
21
+ end
22
+
23
+ self.validations << method_name
24
+ end
25
+ end
@@ -0,0 +1,7 @@
1
+ require_relative 'join_clause'
2
+ require_relative 'where_clause'
3
+ require_relative 'limit_clause'
4
+ require_relative 'select_clause'
5
+ require_relative 'from_clause'
6
+ require_relative 'group_clause'
7
+ require_relative 'order_clause'
@@ -0,0 +1,11 @@
1
+ class FromClause
2
+ attr_reader :table_name
3
+
4
+ def initialize(table_name)
5
+ @table_name = table_name
6
+ end
7
+
8
+ def as_sql
9
+ "FROM #{table_name}"
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ class GroupClause
2
+ attr_accessor :grouping_attr
3
+
4
+ def initialize(grouping_attr = nil)
5
+ @grouping_attr = grouping_attr
6
+ end
7
+
8
+ def as_sql
9
+ return "" if grouping_attr.nil?
10
+ "GROUP BY #{grouping_attr.to_s}"
11
+ end
12
+ end
@@ -0,0 +1,46 @@
1
+ require_relative '../assoc_options'
2
+
3
+ class JoinClause
4
+ def initialize(assoc_options, source_table)
5
+ unless assoc_options
6
+ raise InvalidInput, "Argument must be an association(type: symbol)"
7
+ end
8
+
9
+ @assoc_options = assoc_options
10
+ @source_table = source_table
11
+ end
12
+
13
+ def other_table
14
+ assoc_options.table_name
15
+ end
16
+
17
+ def on_clause
18
+ "#{source_table}.#{assoc_options.own_join_column}" +
19
+ " = " + "#{other_table}.#{assoc_options.other_join_column}"
20
+ end
21
+
22
+ def as_sql
23
+ "JOIN #{other_table} ON #{on_clause} "
24
+ end
25
+
26
+ attr_reader :assoc_options, :source_table
27
+ end
28
+
29
+ class JoinOptions
30
+ attr_reader :clauses
31
+
32
+ def initialize(assoc_options = nil, source_table = nil)
33
+ @clauses = []
34
+ if assoc_options && source_table
35
+ @clauses << JoinClause.new(assoc_options, source_table)
36
+ end
37
+ end
38
+
39
+ def as_sql
40
+ clauses.map(&:as_sql).join(" \n ")
41
+ end
42
+
43
+ def append(assoc_options, source_table)
44
+ clauses << JoinClause.new(assoc_options, source_table)
45
+ end
46
+ end