yescode 1.0.0
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 +7 -0
- data/CHANGELOG.md +23 -0
- data/Dockerfile +46 -0
- data/Gemfile +7 -0
- data/LICENSE +21 -0
- data/README.md +73 -0
- data/exe/yescode +16 -0
- data/lib/yes_app.rb +114 -0
- data/lib/yes_controller.rb +85 -0
- data/lib/yes_logger.rb +8 -0
- data/lib/yes_mail.rb +88 -0
- data/lib/yes_rack_logger.rb +59 -0
- data/lib/yes_record.rb +283 -0
- data/lib/yes_routes.rb +64 -0
- data/lib/yes_static.rb +25 -0
- data/lib/yes_view.rb +99 -0
- data/lib/yescode/asset.rb +24 -0
- data/lib/yescode/asset_compiler.rb +33 -0
- data/lib/yescode/assets.rb +30 -0
- data/lib/yescode/database.rb +136 -0
- data/lib/yescode/emote.rb +85 -0
- data/lib/yescode/env.rb +23 -0
- data/lib/yescode/generator.rb +487 -0
- data/lib/yescode/logfmt_formatter.rb +30 -0
- data/lib/yescode/queries.rb +27 -0
- data/lib/yescode/refinements.rb +19 -0
- data/lib/yescode/request_cache/middleware.rb +29 -0
- data/lib/yescode/request_cache.rb +56 -0
- data/lib/yescode/router.rb +78 -0
- data/lib/yescode/strings.rb +23 -0
- data/lib/yescode.rb +48 -0
- data/yescode.gemspec +41 -0
- metadata +161 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 179d3590c5376a70115b2e0287a90e786bd38efd7360a95dc7d3d5dd4a871ee6
|
4
|
+
data.tar.gz: 78994d192bcd5459c6029b1e5bb716a205a6b022011707133713095f8502cfe9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: de7bff8cb3e6d2f82a24c8daaaa78c96c7f0df0da54e18391c5852bd025ab9a1e1d04170a00a8a6e00beb48b2b7ec56af9fa00be077fce44c9a2b4de814c5523
|
7
|
+
data.tar.gz: 83669e3e586ab2d131ddcd1739c90085873d5546d5e88b9fc305db9292052f6a2d789d0b932670be76b8e48a3cb8da3bee952e4b067604f2e3a7a0e489d8e23e
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
6
|
+
|
7
|
+
# [1.0.0]
|
8
|
+
|
9
|
+
- MVC
|
10
|
+
- Plain sql migrations
|
11
|
+
- Rack support
|
12
|
+
- Routing
|
13
|
+
- Vanilla js yes frame
|
14
|
+
- Ajax support
|
15
|
+
- Models are sql with a PORO on top
|
16
|
+
- Views are emote (a fork of mote) with a PORO on top
|
17
|
+
- Controllers are... you guessed it a PORO
|
18
|
+
- No tests yet
|
19
|
+
- Two examples showing hello world / crud
|
20
|
+
- Simple CLI generators
|
21
|
+
- No autoload, no zeitwerk
|
22
|
+
- No activesupport
|
23
|
+
- No ORM
|
data/Dockerfile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
FROM docker.io/library/ruby:slim
|
2
|
+
|
3
|
+
RUN apt-get update -qq
|
4
|
+
RUN apt-get install -y --no-install-recommends build-essential libjemalloc2 fonts-liberation wget gnupg2 libc6
|
5
|
+
RUN rm -rf /var/lib/apt/lists/*
|
6
|
+
|
7
|
+
# Use sqlite >= 3.38 for returns keyword
|
8
|
+
RUN wget https://www.sqlite.org/2022/sqlite-autoconf-3380200.tar.gz && \
|
9
|
+
tar xvfz sqlite-autoconf-3380200.tar.gz && \
|
10
|
+
cd sqlite-autoconf-3380200 && \
|
11
|
+
./configure && \
|
12
|
+
make && \
|
13
|
+
make install && \
|
14
|
+
rm -rf sqlite-autoconf-3380200
|
15
|
+
|
16
|
+
RUN wget https://github.com/watchexec/watchexec/releases/download/cli-v1.18.11/watchexec-1.18.11-x86_64-unknown-linux-gnu.tar.xz && \
|
17
|
+
tar xf watchexec-1.18.11-x86_64-unknown-linux-gnu.tar.xz && \
|
18
|
+
mv watchexec-1.18.11-x86_64-unknown-linux-gnu/watchexec /usr/local/bin/ && \
|
19
|
+
rm -rf watchexec-1.18.11-x86_64-unknown-linux-gnu
|
20
|
+
|
21
|
+
RUN wget https://github.com/DarthSim/hivemind/releases/download/v1.1.0/hivemind-v1.1.0-linux-amd64.gz && \
|
22
|
+
gunzip hivemind-v1.1.0-linux-amd64.gz && \
|
23
|
+
mv hivemind-v1.1.0-linux-amd64 /usr/local/bin/hivemind && \
|
24
|
+
chmod +x /usr/local/bin/hivemind
|
25
|
+
|
26
|
+
ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
|
27
|
+
|
28
|
+
ARG USER=app
|
29
|
+
ARG GROUP=app
|
30
|
+
ARG UID=1101
|
31
|
+
ARG GID=1101
|
32
|
+
ARG DIR=/home/app
|
33
|
+
|
34
|
+
RUN groupadd --gid $GID $GROUP
|
35
|
+
RUN useradd --uid $UID --gid $GID --groups $GROUP -ms /bin/bash $USER
|
36
|
+
|
37
|
+
RUN chown -R $USER:$GROUP $DIR
|
38
|
+
|
39
|
+
USER $USER
|
40
|
+
WORKDIR $DIR
|
41
|
+
|
42
|
+
COPY --chown=$USER Gemfile Gemfile.lock $DIR
|
43
|
+
|
44
|
+
RUN bundle install
|
45
|
+
|
46
|
+
COPY --chown=$USER . $DIR
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2021 Sean Walker
|
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 all
|
13
|
+
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 SOTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# yescode
|
2
|
+
|
3
|
+
A ruby full stack mvc web framework
|
4
|
+
|
5
|
+
# Learning
|
6
|
+
|
7
|
+
Check out the `examples/` folder for some ideas on how to get started
|
8
|
+
|
9
|
+
# Quickstart without docker
|
10
|
+
|
11
|
+
Install the gem
|
12
|
+
|
13
|
+
```sh
|
14
|
+
gem install yescode
|
15
|
+
```
|
16
|
+
|
17
|
+
Generate a new app and get in there
|
18
|
+
|
19
|
+
```sh
|
20
|
+
yescode g app todos
|
21
|
+
cd todos
|
22
|
+
```
|
23
|
+
|
24
|
+
Install the dependencies
|
25
|
+
|
26
|
+
```sh
|
27
|
+
bundle install
|
28
|
+
```
|
29
|
+
|
30
|
+
Set the environment from the .env file
|
31
|
+
|
32
|
+
```sh
|
33
|
+
export $(cat .env | xargs)
|
34
|
+
```
|
35
|
+
|
36
|
+
Start the server
|
37
|
+
|
38
|
+
```sh
|
39
|
+
bundle exec tipi --listen localhost:9292 config.ru
|
40
|
+
```
|
41
|
+
|
42
|
+
Test it out with curl or you can just visit `http://localhost:9292` in your browser
|
43
|
+
|
44
|
+
```sh
|
45
|
+
curl localhost:9292
|
46
|
+
```
|
47
|
+
|
48
|
+
# Quickstart with docker
|
49
|
+
|
50
|
+
Install the gem
|
51
|
+
|
52
|
+
```sh
|
53
|
+
gem install yescode
|
54
|
+
```
|
55
|
+
|
56
|
+
Generate a new app and get in there
|
57
|
+
|
58
|
+
```sh
|
59
|
+
yescode g app todos
|
60
|
+
cd todos
|
61
|
+
```
|
62
|
+
|
63
|
+
Build a docker image. The Dockerfile installs [hivemind](https://github.com/DarthSim/hivemind) and [watchexec](https://github.com/watchexec/watchexec) and will restart the server on file changes using Procfile.dev
|
64
|
+
|
65
|
+
```sh
|
66
|
+
docker build -t todos .
|
67
|
+
```
|
68
|
+
|
69
|
+
Run the container
|
70
|
+
|
71
|
+
```sh
|
72
|
+
docker run --rm -it -v $(pwd):/home/app --env-file=.env -p 9292:9292 --name "todos" todos hivemind Procfile.dev
|
73
|
+
```
|
data/exe/yescode
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "yescode"
|
4
|
+
|
5
|
+
command, *args = ARGV
|
6
|
+
|
7
|
+
case command
|
8
|
+
when "g", "gen", "generate"
|
9
|
+
Yescode::Generator.generate(*args)
|
10
|
+
when "migrate"
|
11
|
+
Yescode::Database.migrate
|
12
|
+
when "rollback"
|
13
|
+
Yescode::Database.rollback(*args)
|
14
|
+
else
|
15
|
+
puts Yescode::Generator::INVALID_COMMAND_MESSAGE
|
16
|
+
end
|
data/lib/yes_app.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class YesApp
|
4
|
+
class << self
|
5
|
+
attr_accessor :middleware, :assets, :route_class
|
6
|
+
attr_writer :routes
|
7
|
+
|
8
|
+
def use(middleware, *args)
|
9
|
+
@middleware ||= []
|
10
|
+
@middleware << [middleware, args]
|
11
|
+
end
|
12
|
+
|
13
|
+
def app
|
14
|
+
@app || build_rack_app
|
15
|
+
end
|
16
|
+
|
17
|
+
def build_rack_app
|
18
|
+
builder = Rack::Builder.new
|
19
|
+
|
20
|
+
middleware&.each do |m, args|
|
21
|
+
m.logger = @logger if m.respond_to?(:logger=)
|
22
|
+
builder.use(m, *args)
|
23
|
+
end
|
24
|
+
|
25
|
+
builder.use Yescode::RequestCache::Middleware
|
26
|
+
|
27
|
+
builder.run Yescode::Router.new(self, @routes)
|
28
|
+
|
29
|
+
@app = builder.to_app
|
30
|
+
end
|
31
|
+
|
32
|
+
def freeze
|
33
|
+
build_rack_app
|
34
|
+
@app.freeze
|
35
|
+
@middleware.freeze
|
36
|
+
@assets.freeze
|
37
|
+
|
38
|
+
super
|
39
|
+
end
|
40
|
+
|
41
|
+
def call(env)
|
42
|
+
app.call(env)
|
43
|
+
end
|
44
|
+
|
45
|
+
def logger(logger_class, params: true, database: true)
|
46
|
+
@logger = logger_class
|
47
|
+
Yescode::Database.logger = logger_class if database
|
48
|
+
Yescode::Router.logger = logger_class if params
|
49
|
+
end
|
50
|
+
|
51
|
+
def css(arr)
|
52
|
+
@assets ||= Yescode::Assets.new
|
53
|
+
@assets.css(arr)
|
54
|
+
end
|
55
|
+
|
56
|
+
def js(arr)
|
57
|
+
@assets ||= Yescode::Assets.new
|
58
|
+
@assets.js(arr)
|
59
|
+
end
|
60
|
+
|
61
|
+
def bundle_static_files
|
62
|
+
@assets ||= Yescode::Assets.new
|
63
|
+
@assets.compile_assets
|
64
|
+
end
|
65
|
+
|
66
|
+
def migrations(dir = "db/migrations")
|
67
|
+
@migrations ||= Dir[dir]
|
68
|
+
end
|
69
|
+
|
70
|
+
def migrate
|
71
|
+
Yescode::Database.migrate(migrations)
|
72
|
+
end
|
73
|
+
|
74
|
+
def rollback(step = 1)
|
75
|
+
Yescode::Database.rollback_schema(migrations, step:)
|
76
|
+
end
|
77
|
+
|
78
|
+
def development?
|
79
|
+
Yescode::Env.development?
|
80
|
+
end
|
81
|
+
|
82
|
+
def test?
|
83
|
+
Yescode::Env.test?
|
84
|
+
end
|
85
|
+
|
86
|
+
def production?
|
87
|
+
Yescode::Env.production?
|
88
|
+
end
|
89
|
+
|
90
|
+
def default_session_cookie
|
91
|
+
{
|
92
|
+
path: "/",
|
93
|
+
expire_after: 2_592_000,
|
94
|
+
secret: ENV["SECRET"],
|
95
|
+
http_only: true,
|
96
|
+
same_site: :strict,
|
97
|
+
secure: production?
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
def routes(class_name = :Routes)
|
102
|
+
@route_class = Object.const_get(class_name)
|
103
|
+
@routes ||= @route_class.routes
|
104
|
+
end
|
105
|
+
|
106
|
+
def paths
|
107
|
+
@paths ||= @route_class.paths
|
108
|
+
end
|
109
|
+
|
110
|
+
def path(class_name, method_name, params = {})
|
111
|
+
@route_class.path(class_name, method_name, params)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
class YesController
|
2
|
+
class << self
|
3
|
+
attr_accessor :before_actions
|
4
|
+
attr_writer :layout
|
5
|
+
|
6
|
+
def layout
|
7
|
+
@layout || Object.const_get(:Layout).new
|
8
|
+
rescue NameError => _e
|
9
|
+
nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def before_action(*symbols)
|
13
|
+
@before_actions = symbols
|
14
|
+
end
|
15
|
+
|
16
|
+
def inherited(subclass)
|
17
|
+
subclass.before_actions = @before_actions
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(env)
|
23
|
+
@env = env
|
24
|
+
end
|
25
|
+
|
26
|
+
def flash
|
27
|
+
@flash ||= @env['rack.session']['__FLASH__']
|
28
|
+
end
|
29
|
+
|
30
|
+
def session
|
31
|
+
@session ||= @env['rack.session']
|
32
|
+
end
|
33
|
+
|
34
|
+
def response(status, body = nil, headers = { "content-type" => "text/html; charset=utf-8" })
|
35
|
+
[status, headers, [body].compact]
|
36
|
+
end
|
37
|
+
|
38
|
+
def ok(body = nil, headers = { "content-type" => "text/html; charset=utf-8" })
|
39
|
+
response(200, body, headers)
|
40
|
+
end
|
41
|
+
|
42
|
+
def xml(body = nil)
|
43
|
+
ok body, { "content-type" => "text/xml; charset=utf-8" }
|
44
|
+
end
|
45
|
+
|
46
|
+
def redirect(controller_or_url, method_name = nil, params = {})
|
47
|
+
if method_name
|
48
|
+
response 302, nil, { "Location" => path(controller_or_url, method_name, params) }
|
49
|
+
else
|
50
|
+
response 302, nil, { "Location" => controller_or_url }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def params
|
55
|
+
@env["params"]
|
56
|
+
end
|
57
|
+
|
58
|
+
def csrf_value
|
59
|
+
Rack::Csrf.token(@env) if @env['rack.session']
|
60
|
+
end
|
61
|
+
|
62
|
+
def csrf_name
|
63
|
+
Rack::Csrf.field
|
64
|
+
end
|
65
|
+
|
66
|
+
def not_found
|
67
|
+
[404, {"content-type" => "text/plain"}, ["not found"]]
|
68
|
+
end
|
69
|
+
|
70
|
+
def not_found!
|
71
|
+
raise NotFoundError
|
72
|
+
end
|
73
|
+
|
74
|
+
def server_error
|
75
|
+
[500, {"content-type" => "text/plain"}, ["internal server error"]]
|
76
|
+
end
|
77
|
+
|
78
|
+
def server_error!
|
79
|
+
raise ServerError
|
80
|
+
end
|
81
|
+
|
82
|
+
def path(*args)
|
83
|
+
Object.const_get(:App).path(*args)
|
84
|
+
end
|
85
|
+
end
|
data/lib/yes_logger.rb
ADDED
data/lib/yes_mail.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
Mail.defaults do
|
2
|
+
delivery_method :logger, logger: Logger.new($stdout) if Yescode.env.development?
|
3
|
+
end
|
4
|
+
|
5
|
+
class YesMail
|
6
|
+
using Refinements
|
7
|
+
include Emote::Helpers
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_writer :template_path
|
11
|
+
attr_accessor :_from, :_layout, :_html_view, :_text_view
|
12
|
+
|
13
|
+
def template_path
|
14
|
+
@template_path || File.join(".", "app", "emails")
|
15
|
+
end
|
16
|
+
|
17
|
+
def layout(arg)
|
18
|
+
@_layout = arg
|
19
|
+
end
|
20
|
+
|
21
|
+
def from(arg)
|
22
|
+
@_from = arg
|
23
|
+
end
|
24
|
+
|
25
|
+
def html_view(arg)
|
26
|
+
@_html_view = arg
|
27
|
+
end
|
28
|
+
|
29
|
+
def text_view(arg)
|
30
|
+
@_text_view = arg
|
31
|
+
end
|
32
|
+
|
33
|
+
def inherited(subclass)
|
34
|
+
subclass._from = @_from
|
35
|
+
subclass._layout = @_layout
|
36
|
+
subclass._html_view = @_html_view
|
37
|
+
subclass._text_view = @_text_view
|
38
|
+
super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def deliver
|
43
|
+
raise NotImplementedError
|
44
|
+
end
|
45
|
+
|
46
|
+
def mail(from: nil, to: nil, subject: nil)
|
47
|
+
mail = Mail.new
|
48
|
+
mail[:from] = from || self.class._from
|
49
|
+
mail[:to] = to
|
50
|
+
mail[:subject] = subject
|
51
|
+
|
52
|
+
default_name = self.class.to_s.snake_case
|
53
|
+
|
54
|
+
text = part(self.class._text_view || "#{default_name}.text.emote")
|
55
|
+
html = part(self.class._html_view || "#{default_name}.html.emote")
|
56
|
+
|
57
|
+
text_part = Mail::Part.new do
|
58
|
+
body text
|
59
|
+
end
|
60
|
+
|
61
|
+
html_part = Mail::Part.new do
|
62
|
+
content_type "text/html; charset=UTF-8"
|
63
|
+
body html
|
64
|
+
end
|
65
|
+
|
66
|
+
mail.text_part = text_part
|
67
|
+
mail.html_part = html_part
|
68
|
+
|
69
|
+
mail.deliver
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def template(name)
|
75
|
+
File.join(self.class.template_path, name)
|
76
|
+
end
|
77
|
+
|
78
|
+
def part(name)
|
79
|
+
content = template(name)
|
80
|
+
layout = template(self.class._layout || "layout")
|
81
|
+
|
82
|
+
if File.exist?(layout)
|
83
|
+
emote(layout, { content: })
|
84
|
+
else
|
85
|
+
emote(content)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class YesRackLogger
|
4
|
+
class << self
|
5
|
+
attr_accessor :logger
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(app)
|
9
|
+
@app = app
|
10
|
+
@logger = self.class.logger
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
log_request(env)
|
15
|
+
|
16
|
+
start_allocations = GC.stat[:total_allocated_objects]
|
17
|
+
response = @app.call(env)
|
18
|
+
allocations = GC.stat[:total_allocated_objects] - start_allocations
|
19
|
+
|
20
|
+
log_response(env, response, allocations)
|
21
|
+
|
22
|
+
response
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def common_parts(env)
|
28
|
+
{
|
29
|
+
method: env[Rack::REQUEST_METHOD],
|
30
|
+
path: env[Rack::PATH_INFO],
|
31
|
+
remote_addr: env["HTTP_X_FORWARDED_FOR"] || env["REMOTE_ADDR"],
|
32
|
+
protocol: env[Rack::SERVER_PROTOCOL],
|
33
|
+
content_type: env["CONTENT_TYPE"]
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def log_request(env)
|
38
|
+
@logger&.info(
|
39
|
+
msg: "Request started",
|
40
|
+
**common_parts(env)
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def log_response(env, response, allocations)
|
45
|
+
status, headers = response
|
46
|
+
|
47
|
+
duration = headers["X-Runtime"]
|
48
|
+
content_length = headers[Rack::CONTENT_LENGTH]
|
49
|
+
|
50
|
+
@logger&.info(
|
51
|
+
msg: "Response finished",
|
52
|
+
**common_parts(env),
|
53
|
+
content_length:,
|
54
|
+
status:,
|
55
|
+
duration:,
|
56
|
+
allocations:
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|