yescode 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ gemspec
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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class YesLogger < Logger
4
+ def initialize(*)
5
+ super
6
+ @formatter = Yescode::LogfmtFormatter.new
7
+ end
8
+ end
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