feet 0.0.9 → 0.0.91

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
  SHA256:
3
- metadata.gz: a0c3e1fc4a9a9d0a621b09f655b3bb412c8244749215fefbbb70c51f3e6740c5
4
- data.tar.gz: 589ddacf2c109fc9839adc44f4e1fbd41afc7e7e28cdf9907ad081fa58dd4e60
3
+ metadata.gz: 07d443fe7489fa46e75573a051cbaea50d5ed5fc4e48cfa37c5d3dc9722bc7c1
4
+ data.tar.gz: af25d58693e016b1cde6ed04bc0dce7456c38713851a79b482f40f74fd78d18e
5
5
  SHA512:
6
- metadata.gz: 07ac4430315e10541fca8b034ad8e7e31f2b6bb49c640963d5442aa30dc7ab7535e8abeec83f4e15c9894d426221d004de9be983a0f56813acd127f21ea2fa42
7
- data.tar.gz: aeebc7f4721d7c465b7471cc15e0912be86d2465a0468599cd7b80f09aeac25b894f6c16dd3abcb6a2a34c3448e45c484f3d3fb9035338b5dd7dc72c2f32670e
6
+ metadata.gz: 22ec8f8cb0f7636e9382fa428c7df6a43f6e07f936309105e4a28b329d08c6bf66f9b952a9fc121c8422b8406fa521b88065cae5d2d113bbf8caea5f7cba7a2e
7
+ data.tar.gz: fa8f9fe9fc8685542a2d1d8b42063cf76a0f38fd20b0bfd88cd76ce5ad4572ffc34f9162e830faa8aedc400dec9d5f37b48cdde906fa95c3885202e8eb0a3d23
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.0.1] - 2022-10-10
4
+
5
+ - Initial release
data/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # Ruby on Feet
2
+ This gem was developed part of a group study of the [Rebuilding Rails](https://rebuilding-rails.com/) book by [Noah Gibbs](https://github.com/noahgibbs).
3
+
4
+ **Ruby on Feet** is a baby Rails-like MVC framework and replicates some of the main features of Rails (see Usage).
5
+
6
+ <p align="center">
7
+ <img src="public/cover.png" width="250" height="446" />
8
+ </p>
9
+
10
+ ## Installation
11
+
12
+ In your Rack application, add `feet` in your Gemfile:
13
+
14
+ ```
15
+ gem 'feet'
16
+ ```
17
+
18
+ And then run `bundle install`
19
+
20
+ Use Feet in your app. An example app:
21
+
22
+ ```ruby
23
+ # config/application.rb
24
+ require 'feet'
25
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'app',
26
+ 'controllers')
27
+
28
+ module MyApp
29
+ class Application < Feet::Application; end
30
+ end
31
+ ```
32
+
33
+ Initialize the application in your rack config.ru.
34
+
35
+ ```ruby
36
+ # config.ru
37
+ require './config/application'
38
+
39
+ app = MyApp::Application.new
40
+
41
+ run app
42
+ ```
43
+
44
+ After, start the rackup and navigate to the `/feet` path to see Feet welcome page and more info.
45
+ You can also check this Usage section for adding controllers and further configuring routes.
46
+
47
+
48
+ ## Usage
49
+
50
+ This projects mocks different Rails features. After installing it in your personal project, you can check some example for each feature.
51
+
52
+ <details>
53
+ <summary>Routing</summary>
54
+
55
+ Map different routes to their controller action.
56
+ Similar to Rails.
57
+
58
+ ```ruby
59
+ # config.ru
60
+ app.route do
61
+ root 'home#index'
62
+
63
+ match 'posts', 'posts#index'
64
+ match 'posts/:id', 'posts#new_post', via: 'POST' # Use different HTTP verb with the `via` option
65
+ match 'posts/:id', 'posts#show'
66
+
67
+ # Get all the default resources with the `resource` method
68
+ resource 'article'
69
+
70
+ # Or just assign default routes
71
+ match ":controller/:id/:action.(:type)?"
72
+ match ':controller/:id/:action'
73
+ match ':controller/:id',
74
+ default: { 'action' => 'show' }
75
+
76
+ end
77
+ ```
78
+ </details>
79
+
80
+ <details>
81
+ <summary>Controllers</summary>
82
+
83
+ ```ruby
84
+ # app/controllers/posts_controller.rb
85
+ class PostsController < Feet::Controller
86
+ def show
87
+ render :show
88
+ end
89
+
90
+ def index
91
+ render :index
92
+ end
93
+ end
94
+ ```
95
+ </details>
96
+
97
+ <details>
98
+ <summary>Views</summary>
99
+
100
+ ```ruby
101
+ # app/views/posts/show.html.erb
102
+ <h1><%= @post['title'] %></h1>
103
+ <p> <%= @post['body'] %></p>
104
+ ```
105
+ </details>
106
+
107
+ <details>
108
+ <summary>FileModel (for building basic file-base models)</summary>
109
+ <br />
110
+ Create a directory to store the files. Each file will be a row on the DB
111
+
112
+ The number in the file name will be the `id` of that record
113
+
114
+ ```ruby
115
+ # db/posts/1.json
116
+ {
117
+ "title": "Ruby on Feet",
118
+ "body": "..."
119
+ }
120
+ ```
121
+
122
+ Then use the `FileModel` to do CRUD operations
123
+
124
+ ```ruby
125
+ # app/controllers/post_controller.rb
126
+ def index
127
+ @posts = FileModel.all
128
+ render :index
129
+ end
130
+ ```
131
+ </details>
132
+
133
+ <details>
134
+ <summary>SQLiteModel ORM</summary>
135
+
136
+ First, create and run a mini migration to initiate the DB (test.db) and create the table (my_table). Modify the DB and table name.
137
+ ```ruby
138
+ # mini_migration.rb
139
+ require 'sqlite3'
140
+
141
+ conn = SQLite3::Database.new 'test.db'
142
+ conn.execute <<~SQL
143
+ create table my_table (
144
+ id INTEGER PRIMARY KEY,
145
+ posted INTEGER,
146
+ title VARCHAR(30),
147
+ body VARCHAR(32000)
148
+ );
149
+ SQL
150
+ ```
151
+ Run the migration
152
+
153
+ $ ruby mini_migration.rb
154
+
155
+ ```ruby
156
+ # app/my_table.rb
157
+ require 'feet/sqlite_model'
158
+
159
+ class MyTable < Feet::Model::SQLiteModel; end
160
+
161
+ # You can add a seed method on MyTable
162
+ MyTable.class_eval do
163
+ def self.seed
164
+ MyTable.create "title" => "Ruby on Feet", "posted" => 1,"body" => "..."
165
+ end
166
+ end
167
+ ```
168
+
169
+ Then you can use MyTable in your controller to handle your DB entries
170
+
171
+ ```ruby
172
+ # app/controller/post_controller.rb
173
+ require_relative '../my_table'
174
+ class PostsController < Feet::Controller
175
+ def show
176
+ @post = MyTable.find(params['id'])
177
+ render :show
178
+ end
179
+ def index
180
+ @posts = MyTable.all
181
+ render :index
182
+ end
183
+ def create
184
+ @post = MyTable.seed
185
+ render :show
186
+ end
187
+ end
188
+ ```
189
+
190
+ </details>
191
+
192
+ ## Development
193
+
194
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
195
+
196
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
197
+
198
+ Running `gem build feet.gemspec` will create a build file feet-VERSION.gem
199
+
200
+ ## Testing
201
+ The [Minitest](http://docs.seattlerb.org/minitest/) library was used for testing.
202
+
203
+ `Rake::TestTask` was configure to run the test suite. You can see a list of Rake commands with `rake -T`.
204
+
205
+ Run all tests with:
206
+
207
+ $ rake test
208
+
209
+ To run only one test, use:
210
+
211
+ $ rake test TEST=test/filename.rb
212
+
213
+
214
+ Currently, the following test files are available:
215
+ - application_test.rb
216
+ - utils_test.rb
217
+ - file_model_test.rb
218
+ - sqlite_model_test.rb
219
+ - route_test.rb
220
+
221
+ ## Contributing
222
+
223
+ Bug reports and pull requests are welcome on GitHub at https://github.com/dariuspirvulescu/feet.
224
+
225
+ ## License
226
+
227
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
228
+
229
+
230
+ <!-- TODO: -->
231
+ <!-- Fix FileModel so to work with multiple models, not just quotes -->
232
+ <!-- Remove @route comment from RouteObject -->
233
+ <!-- Deploy the gem to rubygems org https://guides.rubygems.org/publishing/ -->
234
+ <!-- CHECK README FOR ANY ERROR instructions -->
235
+ <!-- Add content to the Welcome page -->
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "feet"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/feet/array.rb ADDED
@@ -0,0 +1,17 @@
1
+ class Array
2
+ def deeply_empty?
3
+ empty? || all?(&:empty?)
4
+ end
5
+
6
+ def present?
7
+ !blank?
8
+ end
9
+
10
+ def blank?
11
+ respond_to?(:empty?) ? !!empty? : !self
12
+ end
13
+
14
+ def all_strings?
15
+ all?(String)
16
+ end
17
+ end
@@ -0,0 +1,85 @@
1
+ require 'erubis'
2
+ require 'rack/request'
3
+ require 'feet/file_model'
4
+ require 'feet/view'
5
+
6
+ module Feet
7
+ class Controller
8
+ include Feet::Model
9
+
10
+ def initialize(env)
11
+ @env = env
12
+ @routing_params = {}
13
+ end
14
+
15
+ def env
16
+ @env
17
+ end
18
+
19
+ def request
20
+ @request ||= Rack::Request.new(env)
21
+ end
22
+
23
+ def params
24
+ request.params.merge @routing_params
25
+ end
26
+
27
+ def build_response(text, status = 200, headers = {})
28
+ raise 'Already responded!' if @response
29
+
30
+ a = [text].flatten
31
+ @response = Rack::Response.new(a, status, headers)
32
+ end
33
+
34
+ def response
35
+ @response
36
+ end
37
+
38
+ def render_response(*args)
39
+ build_response(render(*args))
40
+ end
41
+
42
+ def class_name
43
+ self.class
44
+ end
45
+
46
+ def feet_version
47
+ Feet::VERSION
48
+ end
49
+
50
+ def controller_name
51
+ # self.class.to_s.split('Controller').first.downcase
52
+ klass = self.class
53
+ klass = klass.to_s.gsub(/Controller$/, '')
54
+ Feet.to_snake_case klass
55
+ end
56
+
57
+ def instance_hash
58
+ instance_variables.each_with_object(Hash.new('')) do |iv, hash|
59
+ hash[iv] = instance_variable_get iv
60
+ end
61
+ end
62
+
63
+ def render(view_name)
64
+ filename = File.join 'app', 'views', controller_name, "#{view_name}.html.erb"
65
+ template = File.read filename
66
+ View.new(template, instance_hash).call
67
+ end
68
+
69
+ def self.action(act, route_params = {})
70
+ proc { |e| self.new(e).dispatch(act, route_params) }
71
+ end
72
+
73
+ def dispatch(action, routing_params = {})
74
+ @routing_params = routing_params
75
+
76
+ text = send(action)
77
+ if response
78
+ [response.status, response.headers, [response.body].flatten]
79
+ else
80
+ [200, { 'Content-Type' => 'text/html' }, [text].flatten]
81
+ end
82
+
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,13 @@
1
+ class Object
2
+ def self.const_missing(c)
3
+ @const_missing_called ||= {}
4
+ return nil if @const_missing_called[c]
5
+
6
+ @const_missing_called[c] = true
7
+ require Feet.to_snake_case(c.to_s)
8
+ klass = Object.const_get(c)
9
+ @const_missing_called[c] = false
10
+
11
+ klass
12
+ end
13
+ end
@@ -0,0 +1,125 @@
1
+ require 'multi_json'
2
+
3
+ module Feet
4
+ module Model
5
+ class FileModel
6
+ def initialize(filename)
7
+ @filename = filename
8
+
9
+ # If filename is "dir/21.json", @id is 21
10
+ basename = File.split(filename)[-1]
11
+ @id = File.basename(basename, '.json').to_i
12
+
13
+ row_object = File.read(filename)
14
+ @hash = MultiJson.load(row_object)
15
+ end
16
+
17
+ def [](name)
18
+ @hash[name.to_s]
19
+ end
20
+
21
+ def []=(name, value)
22
+ @hash[name.to_s] = value
23
+ end
24
+
25
+ def self.find(id)
26
+ id = id.to_i
27
+ @dm_style_cache ||= {}
28
+ begin
29
+ return @dm_style_cache[id] if @dm_style_cache[id]
30
+
31
+ found = FileModel.new("db/quotes/#{id}.json")
32
+ @dm_style_cache[id] = found
33
+ found
34
+ rescue Errno::ENOENT
35
+ nil
36
+ end
37
+ end
38
+
39
+ def self.all
40
+ files = Dir['db/quotes/*.json']
41
+ files.map { |f| FileModel.new f }
42
+ end
43
+
44
+ def self.create(attrs)
45
+ # Create hash
46
+ hash = {}
47
+ hash['attribution'] = attrs['attribution'] || ''
48
+ hash['submitter'] = attrs['submitter'] || ''
49
+ hash['quote'] = attrs['quote'] || ''
50
+
51
+ # Find highest id
52
+ files = Dir['db/quotes/*.json']
53
+ names = files.map { |f| File.split(f)[-1] } # transform to_i here?
54
+ highest = names.map(&:to_i).max
55
+ id = highest + 1
56
+
57
+ # Open and write the new file
58
+ new_filename = "db/quotes/#{id}.json"
59
+ File.open("db/quotes/#{id}.json", 'w') do |f|
60
+ f.write <<~TEMPLATE
61
+ {
62
+ "submitter": "#{hash['submitter']}",
63
+ "quote": "#{hash['quote']}",
64
+ "attribution": "#{hash['attribution']}"
65
+ }
66
+ TEMPLATE
67
+ end
68
+
69
+ # Create new FileModel instance with the new file
70
+ FileModel.new new_filename
71
+ end
72
+
73
+ def save
74
+ return 'No valid hash' unless @hash
75
+
76
+ # Write JSON to file
77
+ File.open(@filename, 'w') do |f|
78
+ f.write <<~TEMPLATE
79
+ {
80
+ "submitter": "#{@hash['submitter']}",
81
+ "quote": "#{@hash['quote']}",
82
+ "attribution": "#{@hash['attribution']}"
83
+ }
84
+ TEMPLATE
85
+ end
86
+
87
+ # Return the hash
88
+ @hash
89
+ end
90
+
91
+ def self.find_all_by_attribute(attribute, value)
92
+ id = 1
93
+ results = []
94
+ loop do
95
+ model = find(id)
96
+ return results unless model
97
+
98
+ results.push(model) if model[attribute] == value
99
+ id += 1
100
+ end
101
+ end
102
+
103
+ def self.find_all_by_submitter(name = '')
104
+ return [] unless name
105
+
106
+ find_all_by_attribute('submitter', name)
107
+ end
108
+
109
+ def self.method_missing(m, *args)
110
+ base = /^find_all_by_(.*)/
111
+ if m.to_s.start_with? base
112
+ key = m.to_s.match(base)[1]
113
+ find_all_by_attribute(key, args[0])
114
+ else
115
+ super
116
+ end
117
+ end
118
+
119
+ def self.respond_to_missing?(method_name, include_private = false)
120
+ method_name.to_s.start_with?('find_all_by') || super
121
+ end
122
+
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,141 @@
1
+ module Feet
2
+ class RouteObject
3
+ def initialize
4
+ @rules = []
5
+ @default_rules = [
6
+ {
7
+ regexp: /^\/([a-zA-Z0-9]+)$/,
8
+ vars: ['controller'],
9
+ dest: nil,
10
+ via: false,
11
+ options: { default: { 'action' => 'index' } }
12
+ }
13
+ ]
14
+ end
15
+
16
+ def root(*args)
17
+ match('', *args)
18
+ end
19
+
20
+ def resource(name)
21
+ match "/#{name}", default: { 'action' => 'index' }, via: 'GET'
22
+ match "/#{name}/new", default: { 'action' => 'new' }, via: 'GET'
23
+ match "/#{name}", default: { 'action' => 'create' }, via: 'POST'
24
+ match "/#{name}/:id", default: { 'action' => 'show' }, via: 'GET'
25
+ match "/#{name}/:id/edit", default: { 'action' => 'edit' }, via: 'GET'
26
+ match "/#{name}/:id", default: { 'action' => 'update' }, via: 'PUT'
27
+ match "/#{name}/:id", default: { 'action' => 'update' }, via: 'PATCH'
28
+ match "/#{name}/:id", default: { 'action' => 'destroy' }, via: 'DELETE'
29
+ end
30
+
31
+ # Example arguments
32
+ # url ":controller/:id"
33
+ # args [{:default=>{"action"=>"show"}}]
34
+ def match(url, *args)
35
+ # Capture the options hash
36
+ options = {}
37
+ options = args.pop if args[-1].is_a? Hash
38
+ # Check for default option
39
+ options[:default] ||= {}
40
+
41
+ # Get destination and limit the # of arguments
42
+ dest = nil
43
+ dest = args.pop if args.size > 0
44
+ raise 'Too many arguments!' if args.size > 0
45
+
46
+ # Parse URL parts. Split on appropriate punctuation
47
+ # (slash, parens, question mark, dot)
48
+ parts = url.split /(\/|\(|\)|\?|\.)/
49
+ parts.reject! { |p| p.empty? }
50
+
51
+ vars = []
52
+ regexp_parts = parts.map do |part|
53
+ case part[0]
54
+ when ':'
55
+ # Map Variable
56
+ vars << part[1..-1]
57
+ '([a-zA-Z0-9]+)?'
58
+ when '*'
59
+ # Map Wildcard
60
+ vars << part[1..-1]
61
+ '(.*)'
62
+ when '.'
63
+ '\\.' # Literal dot
64
+ else
65
+ part
66
+ end
67
+ end
68
+
69
+ # Join the main regexp
70
+ regexp = regexp_parts.join('')
71
+
72
+ # Store match object
73
+ @rules.push({
74
+ regexp: Regexp.new("^/#{regexp}$"),
75
+ vars: vars,
76
+ dest: dest,
77
+ via: options[:via] ? options[:via].downcase : false,
78
+ options: options
79
+ })
80
+ end
81
+
82
+ def check_url(url, method)
83
+ (@rules + @default_rules).each do |rule|
84
+ next if rule[:via] && rule[:via] != method.downcase
85
+
86
+ # Check if rule against regexp
87
+ matched_data = rule[:regexp].match(url)
88
+
89
+ if matched_data
90
+ # Build params hash
91
+ options = rule[:options]
92
+ params = options[:default].dup
93
+
94
+ # Match variable names with the regexp captured parts
95
+ rule[:vars].each_with_index do |var, i|
96
+ params[var] = matched_data.captures[i]
97
+ end
98
+
99
+ if rule[:dest]
100
+ # There's either a destination like 'controller#action' or a Proc
101
+ return get_dest(rule[:dest], params)
102
+ else
103
+ # The params are specified in the options[:default]
104
+ # Use controller#action to get the Rack application
105
+ controller = params['controller']
106
+ action = params['action']
107
+ return get_dest("#{controller}##{action}", params)
108
+ end
109
+ end
110
+ end
111
+ nil
112
+ end
113
+
114
+ # Returns a Rack application or raises an error if no/invalid 'dest'
115
+ def get_dest(dest, routing_params = {})
116
+ return dest if dest.respond_to?(:call)
117
+
118
+ # Ex 'controller#action'
119
+ if dest =~ /^([^#]+)#([^#]+)$/
120
+ name = $1.capitalize
121
+ controller = Object.const_get("#{name}Controller")
122
+ return controller.action($2, routing_params)
123
+ end
124
+
125
+ raise "No destination: #{dest.inspect}!"
126
+ end
127
+ end
128
+
129
+ class Application
130
+ def route(&block)
131
+ @route_obj ||= RouteObject.new
132
+ @route_obj.instance_eval(&block)
133
+ end
134
+
135
+ def get_rack_app(env)
136
+ raise 'No routes!' unless @route_obj
137
+
138
+ @route_obj.check_url env['PATH_INFO'], env['REQUEST_METHOD']
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,142 @@
1
+ require 'sqlite3'
2
+ require 'feet/utils'
3
+
4
+ DB = SQLite3::Database.new 'test.db'
5
+
6
+ module Feet
7
+ module Model
8
+ class SQLiteModel
9
+ def initialize(data = nil)
10
+ @hash = data
11
+ end
12
+
13
+ def self.table
14
+ Feet.to_snake_case name # name = method to return the class name, ex: MyName
15
+ end
16
+
17
+ def self.schema
18
+ return @schema if @schema
19
+
20
+ @schema = {}
21
+ DB.table_info(table) do |row|
22
+ @schema[row['name']] = row['type']
23
+ end
24
+
25
+ @schema
26
+ end
27
+
28
+ def self.to_sql(value)
29
+ case value
30
+ when NilClass
31
+ 'null'
32
+ when Numeric
33
+ value.to_s
34
+ when String
35
+ "'#{value}'"
36
+ else
37
+ raise "Can't convert #{value.class} to SQL."
38
+ end
39
+ end
40
+
41
+ def self.create(initial_hash)
42
+ # Get initial_hash and schema keys without ids and map initial_hash to schema keys
43
+ initial_hash.delete 'id'
44
+ keys = schema.keys - ['id']
45
+ sql_values = keys.map do |key|
46
+ initial_hash[key] ? to_sql(initial_hash[key]) : 'null'
47
+ end
48
+
49
+ # Insert values into table
50
+ DB.execute <<~SQL
51
+ INSERT INTO #{table} (#{keys.join ','}) VALUES (#{sql_values.join ','});
52
+ SQL
53
+
54
+ # Build and return the new table entry
55
+ raw_values = keys.map { |k| initial_hash[k] }
56
+ data = Hash[keys.zip raw_values]
57
+
58
+ # Get the latest id
59
+ sql = 'SELECT last_insert_rowid();'
60
+ data['id'] = DB.execute(sql)[0][0]
61
+
62
+ self.new data
63
+ end
64
+
65
+ def self.find(id)
66
+ keys = schema.keys
67
+ response = DB.execute <<~SQL
68
+ SELECT #{keys.join ','} FROM #{table} WHERE id = #{id}
69
+ SQL
70
+ return nil unless response[0]
71
+
72
+ data = Hash[keys.zip response[0]]
73
+ self.new data
74
+ end
75
+
76
+ def [](name)
77
+ @hash[name.to_s]
78
+ end
79
+
80
+ def []=(key, value)
81
+ @hash[key] = value
82
+ end
83
+
84
+ def save!
85
+ return nil unless @hash['id']
86
+
87
+ hash_map = @hash.keys.map do |key|
88
+ "#{key} = #{self.class.to_sql(@hash[key])}"
89
+ end
90
+
91
+ DB.execute <<~SQL
92
+ UPDATE #{self.class.table}
93
+ SET #{hash_map.join ','}
94
+ WHERE id = #{@hash['id']};
95
+ SQL
96
+ @hash
97
+ end
98
+
99
+ def self.all
100
+ keys = schema.keys
101
+ rows = DB.execute <<~SQL
102
+ SELECT * FROM #{table}
103
+ SQL
104
+
105
+ rows.map do |row|
106
+ data = Hash[keys.zip row]
107
+ self.new data
108
+ end
109
+ end
110
+
111
+ def self.count
112
+ db_result = DB.execute <<~SQL
113
+ SELECT COUNT(*) FROM #{table};
114
+ SQL
115
+ db_result[0][0]
116
+ end
117
+
118
+ def method_missing(method, *args)
119
+ # Check for getter
120
+ if @hash[method.to_s]
121
+ self.class.define_method(method) do
122
+ self[method]
123
+ end
124
+ return send(method)
125
+ end
126
+
127
+ # Check for setters
128
+ if method.to_s[-1..-1] == '='
129
+ field = method.to_s[0..-2]
130
+ self.class.class_eval do
131
+ define_method(method) do |value|
132
+ self[field] = value
133
+ end
134
+ end
135
+ return self.send(method, args[0])
136
+ end
137
+
138
+ super
139
+ end
140
+ end
141
+ end
142
+ end
data/lib/feet/utils.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Feet
4
+ def self.to_snake_case(string)
5
+ string.gsub(/::/, '/') # to remove the subdirectory feature
6
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
7
+ .gsub(/([a-z\d])([A-Z][a-z])/, '\1_\2')
8
+ .tr('-', '_')
9
+ .downcase
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Feet
4
+ VERSION = '0.0.91'
5
+ end
data/lib/feet/view.rb ADDED
@@ -0,0 +1,28 @@
1
+ require 'erubis'
2
+
3
+ module Feet
4
+ class View
5
+ def initialize(template, instance_hash)
6
+ @template = template
7
+ init_vars(instance_hash)
8
+ end
9
+
10
+ def call
11
+ eruby = Erubis::Eruby.new(@template)
12
+ # Locals here are in addition to instance variables, if any
13
+ eval eruby.src
14
+ end
15
+
16
+ def init_vars(received_vars)
17
+ received_vars.each do |key, value|
18
+ # key example = '@quote'
19
+ instance_variable_set(key, value)
20
+ end
21
+ end
22
+
23
+ # Helper method for View
24
+ def h(str)
25
+ URI.escape str
26
+ end
27
+ end
28
+ end
data/lib/feet.rb ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'feet/version'
4
+ require 'feet/array'
5
+ require 'feet/routing'
6
+ require 'feet/dependencies'
7
+ require 'feet/controller'
8
+ require 'feet/utils'
9
+
10
+ module Feet
11
+ class Error < StandardError; end
12
+
13
+ class Application
14
+ def call(env)
15
+ return [404, { 'Content-Type' => 'text/html' }, []] if env['PATH_INFO'] == '/favicon.ico'
16
+
17
+ # Assign a default Feet HTML welcome page (in public/index.html)
18
+ if env['PATH_INFO'] == '/feet'
19
+ path = File.expand_path('../public/index.html', __dir__)
20
+ return [200, { 'Content-Type' => 'text/html' }, File.open(path)]
21
+ end
22
+
23
+ rack_app = get_rack_app(env)
24
+ rack_app.call(env)
25
+ end
26
+ end
27
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: feet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9
4
+ version: 0.0.91
5
5
  platform: ruby
6
6
  authors:
7
7
  - Darius Pirvulescu
@@ -114,7 +114,21 @@ email:
114
114
  executables: []
115
115
  extensions: []
116
116
  extra_rdoc_files: []
117
- files: []
117
+ files:
118
+ - CHANGELOG.md
119
+ - README.md
120
+ - bin/console
121
+ - bin/setup
122
+ - lib/feet.rb
123
+ - lib/feet/array.rb
124
+ - lib/feet/controller.rb
125
+ - lib/feet/dependencies.rb
126
+ - lib/feet/file_model.rb
127
+ - lib/feet/routing.rb
128
+ - lib/feet/sqlite_model.rb
129
+ - lib/feet/utils.rb
130
+ - lib/feet/version.rb
131
+ - lib/feet/view.rb
118
132
  homepage: https://i.etsystatic.com/13348558/r/il/a29ab1/2918306283/il_570xN.2918306283_ojql.jpg
119
133
  licenses:
120
134
  - MIT