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