feet 0.0.9 → 0.0.92
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 +4 -4
- data/.gitignore +11 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +43 -0
- data/LICENSE.txt +21 -0
- data/README.md +235 -0
- data/Rakefile +11 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/feet.gemspec +46 -0
- data/lib/feet/array.rb +17 -0
- data/lib/feet/controller.rb +85 -0
- data/lib/feet/dependencies.rb +13 -0
- data/lib/feet/file_model.rb +125 -0
- data/lib/feet/routing.rb +141 -0
- data/lib/feet/sqlite_model.rb +142 -0
- data/lib/feet/utils.rb +11 -0
- data/lib/feet/version.rb +5 -0
- data/lib/feet/view.rb +28 -0
- data/lib/feet.rb +27 -0
- data/public/cover.png +0 -0
- data/public/index.html +13 -0
- data/sig/feet.rbs +4 -0
- metadata +25 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f15402526d193d6dbbaae0faa7824b6f6c63fc5ebba323c0b5db44b861a4f72b
|
4
|
+
data.tar.gz: 76c466d6a6a7c11eb7f7499e7df7af429802a0aa8149da533d138231c89acb2e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e13310b0fce8c11163e5d7db16440e44379a6272b9a0a276b13ee96c07a8c5f7e839d008594b76799d4431b0e93ee18d49810e95c108d35102433636947c1895
|
7
|
+
data.tar.gz: e665d27bd3e05a42f259f550112d433f3e26bc0146e0bd7834da168a66471a2ef4276d10f711ef8b8c0dbda0195f8ee5b1dbf03640c90806a9fef6e054c88d65
|
data/.gitignore
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
feet (0.0.92)
|
5
|
+
erubis
|
6
|
+
multi_json
|
7
|
+
rack (~> 2.2)
|
8
|
+
sqlite3
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: https://rubygems.org/
|
12
|
+
specs:
|
13
|
+
byebug (11.1.3)
|
14
|
+
coderay (1.1.3)
|
15
|
+
erubis (2.7.0)
|
16
|
+
method_source (1.0.0)
|
17
|
+
minitest (5.17.0)
|
18
|
+
multi_json (1.15.0)
|
19
|
+
pry (0.14.1)
|
20
|
+
coderay (~> 1.1)
|
21
|
+
method_source (~> 1.0)
|
22
|
+
pry-byebug (3.10.1)
|
23
|
+
byebug (~> 11.0)
|
24
|
+
pry (>= 0.13, < 0.15)
|
25
|
+
rack (2.2.5)
|
26
|
+
rack-test (2.0.2)
|
27
|
+
rack (>= 1.3)
|
28
|
+
rake (13.0.6)
|
29
|
+
sqlite3 (1.6.0-arm64-darwin)
|
30
|
+
|
31
|
+
PLATFORMS
|
32
|
+
arm64-darwin-21
|
33
|
+
arm64-darwin-22
|
34
|
+
|
35
|
+
DEPENDENCIES
|
36
|
+
feet!
|
37
|
+
minitest
|
38
|
+
pry-byebug
|
39
|
+
rack-test
|
40
|
+
rake (~> 13.0)
|
41
|
+
|
42
|
+
BUNDLED WITH
|
43
|
+
2.3.17
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2022 Darius Pirvulescu
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
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/Rakefile
ADDED
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
data/feet.gemspec
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/feet/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'feet'
|
7
|
+
spec.version = Feet::VERSION
|
8
|
+
spec.authors = ['Darius Pirvulescu']
|
9
|
+
spec.email = ['organicdarius@gmail.com']
|
10
|
+
|
11
|
+
spec.summary = 'Hairy rails twin'
|
12
|
+
spec.description = 'A more down to earth, hairy rails twin'
|
13
|
+
spec.homepage = 'https://i.etsystatic.com/13348558/r/il/a29ab1/2918306283/il_570xN.2918306283_ojql.jpg'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.required_ruby_version = '>= 2.6.0'
|
16
|
+
|
17
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org/'
|
18
|
+
|
19
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
20
|
+
spec.metadata['source_code_uri'] = 'https://github.com/DariusPirvulescu/feet'
|
21
|
+
spec.metadata['changelog_uri'] = 'https://github.com/DariusPirvulescu/feet/pulls?q=is%3Apr+is%3Aclosed'
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
26
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
27
|
+
end
|
28
|
+
spec.bindir = 'exe'
|
29
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ['lib']
|
31
|
+
|
32
|
+
# Uncomment to register a new dependency of your gem
|
33
|
+
# spec.add_dependency 'example-gem', '~> 1.0'
|
34
|
+
|
35
|
+
spec.add_runtime_dependency 'erubis'
|
36
|
+
spec.add_runtime_dependency 'multi_json'
|
37
|
+
spec.add_runtime_dependency 'rack', '~>2.2'
|
38
|
+
spec.add_runtime_dependency 'sqlite3'
|
39
|
+
|
40
|
+
spec.add_development_dependency 'minitest'
|
41
|
+
spec.add_development_dependency 'pry-byebug'
|
42
|
+
spec.add_development_dependency 'rack-test'
|
43
|
+
|
44
|
+
# For more information and examples about making a new gem, check out our
|
45
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
46
|
+
end
|
data/lib/feet/array.rb
ADDED
@@ -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
|
data/lib/feet/routing.rb
ADDED
@@ -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
|
data/lib/feet/version.rb
ADDED
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
|
data/public/cover.png
ADDED
Binary file
|
data/public/index.html
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7
|
+
<title>Immanuel Kan</title>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
<h1>Ruby on Feet</h1>
|
11
|
+
<h2>Welcome</h2>
|
12
|
+
</body>
|
13
|
+
</html>
|
data/sig/feet.rbs
ADDED
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.
|
4
|
+
version: 0.0.92
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Darius Pirvulescu
|
@@ -114,7 +114,30 @@ email:
|
|
114
114
|
executables: []
|
115
115
|
extensions: []
|
116
116
|
extra_rdoc_files: []
|
117
|
-
files:
|
117
|
+
files:
|
118
|
+
- ".gitignore"
|
119
|
+
- CHANGELOG.md
|
120
|
+
- Gemfile
|
121
|
+
- Gemfile.lock
|
122
|
+
- LICENSE.txt
|
123
|
+
- README.md
|
124
|
+
- Rakefile
|
125
|
+
- bin/console
|
126
|
+
- bin/setup
|
127
|
+
- feet.gemspec
|
128
|
+
- lib/feet.rb
|
129
|
+
- lib/feet/array.rb
|
130
|
+
- lib/feet/controller.rb
|
131
|
+
- lib/feet/dependencies.rb
|
132
|
+
- lib/feet/file_model.rb
|
133
|
+
- lib/feet/routing.rb
|
134
|
+
- lib/feet/sqlite_model.rb
|
135
|
+
- lib/feet/utils.rb
|
136
|
+
- lib/feet/version.rb
|
137
|
+
- lib/feet/view.rb
|
138
|
+
- public/cover.png
|
139
|
+
- public/index.html
|
140
|
+
- sig/feet.rbs
|
118
141
|
homepage: https://i.etsystatic.com/13348558/r/il/a29ab1/2918306283/il_570xN.2918306283_ojql.jpg
|
119
142
|
licenses:
|
120
143
|
- MIT
|