feet 0.0.9 → 0.0.91

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