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.
@@ -0,0 +1,85 @@
1
+ # Copyright (c) 2011 Michel Martens
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require "cgi/escape"
22
+
23
+ class Emote
24
+ VERSION = "1.3.1".freeze
25
+
26
+ PATTERN = /
27
+ ^[^\S\n]*(%)[^\S\n]*(.*?)(?:\n|\Z) | # Ruby evaluated lines
28
+ (<\?)\s+(.*?)\s+\?> | # Multiline Ruby blocks
29
+ (\$\{)(.*?)\} | # Ruby evaluated to strings unescaped
30
+ (\{\{)(.*?)\}\} # Ruby evaluated to strings html escaped
31
+ /mx
32
+
33
+ def self.h(value)
34
+ CGI.escapeHTML(value.to_s)
35
+ end
36
+
37
+ def self.src(template, vars = [])
38
+ terms = template.split(PATTERN)
39
+
40
+ code = "proc do |params, __o| params ||= {}; __o ||= '';"
41
+
42
+ vars.each do |var|
43
+ code << format("%<val>s = params[%<key>p];", { val: var, key: var })
44
+ end
45
+
46
+ while (term = terms.shift)
47
+ code << case term
48
+ when "<?"
49
+ "#{terms.shift}\n"
50
+ when "%"
51
+ "#{terms.shift}\n"
52
+ when "${"
53
+ "__o << (#{terms.shift}).to_s\n"
54
+ when "{{"
55
+ "__o << Emote.h(#{terms.shift}).to_s\n"
56
+ else
57
+ "__o << #{term.dump}\n"
58
+ end
59
+ end
60
+
61
+ code << "__o; end"
62
+ end
63
+
64
+ def self.parse(template, context = self, vars = [], name = "template")
65
+ context.instance_eval(src(template, vars), name, -1)
66
+ end
67
+
68
+ module Helpers
69
+ def emote(file, params = {}, context = self)
70
+ file_cache[file] ||= File.read(file)
71
+ key = params.hash + context.hash
72
+ emote_cache[key] ||= Emote.parse(file_cache[file], context, params.keys, file)
73
+
74
+ emote_cache[key][params]
75
+ end
76
+
77
+ def emote_cache
78
+ Thread.current[:_emote_cache] ||= {}
79
+ end
80
+
81
+ def file_cache
82
+ Thread.current[:_file_cache] ||= {}
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yescode
4
+ class Env
5
+ class << self
6
+ def development?
7
+ ENV["RACK_ENV"] == "development"
8
+ end
9
+
10
+ def test?
11
+ ENV["RACK_ENV"] == "test"
12
+ end
13
+
14
+ def production?
15
+ ENV["RACK_ENV"] == "production"
16
+ end
17
+ end
18
+ end
19
+
20
+ def self.env
21
+ Env
22
+ end
23
+ end
@@ -0,0 +1,487 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "securerandom"
5
+
6
+ module Yescode
7
+ class Generator
8
+ INVALID_COMMAND_MESSAGE = "Command not supported. Try g, gen, generate, migrate or rollback."
9
+ VIEW_DIR = File.join(".", "app", "views")
10
+
11
+ using Refinements
12
+
13
+ class << self
14
+ def generate(*gen_args)
15
+ type, *args = gen_args
16
+
17
+ case type
18
+ when "controller"
19
+ generate_controller(*args)
20
+ when "queries"
21
+ generate_queries(*args)
22
+ when "model"
23
+ generate_model(*args)
24
+ when "view"
25
+ generate_view(*args)
26
+ when "views"
27
+ generate_views(*args)
28
+ when "template"
29
+ generate_template(*args)
30
+ when "migration"
31
+ generate_migration(*args)
32
+ when "mvc"
33
+ generate_mvc(*args)
34
+ when "app"
35
+ generate_app(*args)
36
+ else
37
+ puts INVALID_COMMAND_MESSAGE
38
+ end
39
+ end
40
+
41
+ def generate_app(dir)
42
+ FileUtils.mkdir_p(File.join(".", dir))
43
+ {
44
+ "public" => %w[css js],
45
+ "db" => %w[migrations],
46
+ "app" => %w[models views controllers emails jobs modules]
47
+ }.each do |k, v|
48
+ v.each do |folder|
49
+ FileUtils.mkdir_p(File.join(".", dir, k, folder))
50
+ end
51
+ end
52
+ File.write(
53
+ File.join(dir, "Gemfile"),
54
+ <<~RB
55
+ source "https://rubygems.org"
56
+ git_source(:github) { |repo| "https://github.com/\#\{repo\}.git" }
57
+
58
+ ruby "3.1.2"
59
+
60
+ gem "tipi", "0.52"
61
+ gem "yescode", "1.0.0"
62
+ RB
63
+ )
64
+ File.write(
65
+ File.join(dir, "Dockerfile"),
66
+ <<~DOCKER
67
+ FROM docker.io/library/ruby:slim
68
+
69
+ RUN apt-get update -qq
70
+ RUN apt-get install -y --no-install-recommends build-essential libjemalloc2 fonts-liberation wget gnupg2 libc6
71
+ RUN rm -rf /var/lib/apt/lists/*
72
+
73
+ RUN wget https://www.sqlite.org/2022/sqlite-autoconf-3380200.tar.gz && \
74
+ tar xvfz sqlite-autoconf-3380200.tar.gz && \
75
+ cd sqlite-autoconf-3380200 && \
76
+ ./configure && \
77
+ make && \
78
+ make install && \
79
+ rm -rf sqlite-autoconf-3380200
80
+
81
+ RUN wget https://github.com/watchexec/watchexec/releases/download/cli-v1.18.11/watchexec-1.18.11-x86_64-unknown-linux-gnu.tar.xz && \
82
+ tar xf watchexec-1.18.11-x86_64-unknown-linux-gnu.tar.xz && \
83
+ mv watchexec-1.18.11-x86_64-unknown-linux-gnu/watchexec /usr/local/bin/ && \
84
+ rm -rf watchexec-1.18.11-x86_64-unknown-linux-gnu
85
+
86
+ RUN wget https://github.com/DarthSim/hivemind/releases/download/v1.1.0/hivemind-v1.1.0-linux-amd64.gz && \
87
+ gunzip hivemind-v1.1.0-linux-amd64.gz && \
88
+ mv hivemind-v1.1.0-linux-amd64 /usr/local/bin/hivemind && \
89
+ chmod +x /usr/local/bin/hivemind
90
+
91
+ ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
92
+
93
+ ARG USER=app
94
+ ARG GROUP=app
95
+ ARG UID=1101
96
+ ARG GID=1101
97
+ ARG DIR=/home/app
98
+
99
+ RUN groupadd --gid $GID $GROUP
100
+ RUN useradd --uid $UID --gid $GID --groups $GROUP -ms /bin/bash $USER
101
+
102
+ RUN chown -R $USER:$GROUP $DIR
103
+
104
+ USER $USER
105
+ WORKDIR $DIR
106
+
107
+ COPY --chown=$USER Gemfile Gemfile.lock $DIR
108
+
109
+ RUN bundle install
110
+
111
+ COPY --chown=$USER . $DIR
112
+ DOCKER
113
+ )
114
+ File.write(
115
+ File.join(dir, "Procfile"),
116
+ "web: bundle exec tipi --listen 0.0.0.0:$PORT config.ru"
117
+ )
118
+ File.write(
119
+ File.join(dir, "Procfile.dev"),
120
+ "web: watchexec --exts rb,emote,sql --restart --signal SIGKILL --debounce 100 -- bundle exec tipi --listen 0.0.0.0:$PORT config.ru"
121
+ )
122
+ File.write(
123
+ File.join(dir, "config.ru"),
124
+ "require \"./app\"\n\nrun App.freeze.app"
125
+ )
126
+ File.write(
127
+ File.join(dir, "public", "404.html"),
128
+ "<h1>404</h1>"
129
+ )
130
+ File.write(
131
+ File.join(dir, "public", "500.html"),
132
+ "<h1>500</h1>"
133
+ )
134
+ File.write(
135
+ File.join(dir, "app", "routes.rb"),
136
+ <<~RB
137
+ class Routes < YesRoutes
138
+ get "/", :Home, :index
139
+ end
140
+ RB
141
+ )
142
+ File.write(
143
+ File.join(dir, "app", "controllers", "home.rb"),
144
+ <<~RB
145
+ class Home < YesController
146
+ def index
147
+ HomeIndex.new
148
+ end
149
+ end
150
+ RB
151
+ )
152
+ File.write(
153
+ File.join(dir, "app", "views", "home_index.rb"),
154
+ <<~RB
155
+ class HomeIndex < Layout
156
+ end
157
+ RB
158
+ )
159
+ File.write(
160
+ File.join(dir, "app", "views", "home_index.emote"),
161
+ <<~RB
162
+ <h1>Welcome to yescode!</h1>
163
+ RB
164
+ )
165
+ File.write(
166
+ File.join(dir, "app", "views", "layout.rb"),
167
+ <<~RB
168
+ class Layout < YesView
169
+ def title
170
+ "yescode"
171
+ end
172
+
173
+ def description
174
+ "Yes, another ruby mvc web framework"
175
+ end
176
+ end
177
+ RB
178
+ )
179
+ File.write(
180
+ File.join(dir, "app", "views", "layout.emote"),
181
+ <<~RB
182
+ <!DOCTYPE html>
183
+ <html lang="en">
184
+ <head>
185
+ <title>{{title}}</title>
186
+ <meta charset="utf-8" />
187
+ <meta name="title" description="{{title}}" />
188
+ <meta name="description" description="{{description}}" />
189
+ % css.each do |href|
190
+ <link href={{href}} rel="stylesheet"></link>
191
+ % end
192
+ % js.each do |src|
193
+ <script src={{src}} type="application/javascript" defer></script>
194
+ % end
195
+ </head>
196
+ <body>
197
+ <nav>
198
+ <a href={{path :Home, :index}}>Home</a>
199
+ </nav>
200
+ ${content}
201
+ </body>
202
+ </html>
203
+ <html
204
+ RB
205
+ )
206
+ File.write(
207
+ File.join(dir, "app.rb"),
208
+ <<~RB
209
+ require "yescode"
210
+
211
+ require_all %w[
212
+ ./app/modules/*
213
+ ./app/models/*
214
+ ./app/emails/*
215
+ ./app/jobs/*
216
+ ./app/views/layout
217
+ ./app/views/*
218
+ ./app/controllers/*
219
+ ./app/routes
220
+ ]
221
+
222
+ class App < YesApp
223
+ logger YesLogger.new($stdout)
224
+
225
+ use YesStatic, root: "public" if development?
226
+ use YesRackLogger
227
+ use Rack::ShowExceptions if development?
228
+ use Rack::Runtime
229
+ use Rack::ETag
230
+ use Rack::Head
231
+ use Rack::ContentLength
232
+ use Rack::ContentType
233
+ use Rack::Session::Cookie, default_session_cookie
234
+ use Rack::Csrf, raise: development?
235
+
236
+ css %w[]
237
+
238
+ js %w[]
239
+
240
+ migrations "db/migrations/*.sql"
241
+
242
+ routes :Routes
243
+
244
+ if production?
245
+ migrate
246
+ bundle_static_files
247
+ end
248
+ end
249
+ RB
250
+ )
251
+ File.write(
252
+ File.join(dir, ".env"),
253
+ <<~SH
254
+ RACK_ENV=development
255
+ PORT=9292
256
+ SECRET=#{SecureRandom.hex(32)}
257
+ DATABASE_URL=development.sqlite3
258
+ SH
259
+ )
260
+ # .env
261
+ # Gemfile
262
+ # Gemfile.lock
263
+ # Dockerfile
264
+ # Procfile
265
+ # config.ru
266
+ # public/
267
+ # public/404.html
268
+ # public/500.html
269
+ # public/js
270
+ # public/css
271
+ # db/
272
+ # db/migrations
273
+ # app/
274
+ # app/models
275
+ # app/views
276
+ # app/controllers
277
+ # app/jobs
278
+ # app/modules
279
+ # app/emails
280
+ # app/routes.rb
281
+ # app.rb
282
+ end
283
+
284
+ def generate_mvc(filename)
285
+ generate_model(filename)
286
+ generate_controller(filename)
287
+ generate_views(filename)
288
+ end
289
+
290
+ def generate_queries(filename)
291
+ filepath = File.join(".", "app", "models", "#{filename}.sql")
292
+ contents = <<~SQL
293
+ -- name: all
294
+ select *
295
+ from #{filename}
296
+
297
+ -- name: count
298
+ -- fn: value
299
+ select count(*)
300
+ from #{filename}
301
+
302
+ -- name: by_#{filename}_id
303
+ select *
304
+ from #{filename}
305
+ where #{filename}_id = ?
306
+
307
+ -- name: latest
308
+ select *
309
+ from #{filename}
310
+ order by created_at desc
311
+
312
+ -- name: oldest
313
+ select *
314
+ from #{filename}
315
+ order by created_at
316
+
317
+ -- name: latest_with_limit
318
+ select *
319
+ from #{filename}
320
+ order by created_at desc
321
+ limit 30
322
+ SQL
323
+
324
+ File.write(filepath, contents)
325
+ end
326
+
327
+ def generate_model(filename)
328
+ filepath = File.join(".", "app", "models", "#{filename}.rb")
329
+ class_name = filename.pascal_case
330
+ contents = <<~RB
331
+ class #{class_name} < AppRecord
332
+ queries "#{filename}.sql"
333
+ end
334
+ RB
335
+ File.write(filepath, contents)
336
+
337
+ generate_migration("create_table_#{filename}")
338
+ generate_queries(filename)
339
+ end
340
+
341
+ def generate_controller(filename)
342
+ class_name = filename.pascal_case
343
+ var_name = filename
344
+ route = <<~RB
345
+ class #{class_name} < AppController
346
+ def index
347
+ all = #{class_name}.all
348
+
349
+ #{class_name}Index.new(all)
350
+ end
351
+
352
+ def show
353
+ #{class_name}Show.new(#{var_name})
354
+ end
355
+
356
+ def new
357
+ @#{var_name} ||= #{class_name}.new
358
+
359
+ #{class_name}New.new(@#{var_name})
360
+ end
361
+
362
+ def create
363
+ @#{var_name} = #{class_name}.new(#{var_name}_params)
364
+
365
+ if @#{var_name}.insert
366
+ redirect path(:#{class_name}, :index)
367
+ else
368
+ new
369
+ end
370
+ end
371
+
372
+ def edit
373
+ #{class_name}Edit.new(#{var_name})
374
+ end
375
+
376
+ def update
377
+ if #{var_name}.update(#{var_name}_params)
378
+ redirect path(:#{class_name}, :index)
379
+ else
380
+ edit
381
+ end
382
+ end
383
+
384
+ def delete
385
+ #{var_name}.delete
386
+
387
+ redirect path(:#{class_name}, :index)
388
+ end
389
+
390
+ private
391
+
392
+ def #{var_name}_params
393
+ params.slice()
394
+ end
395
+
396
+ def #{var_name}
397
+ @#{var_name} ||= #{class_name}.first! :by_#{var_name}_id, params[:#{var_name}_id]
398
+ end
399
+ end
400
+ RB
401
+
402
+ File.write(
403
+ File.join(".", "app", "controllers", "#{filename}.rb"),
404
+ route
405
+ )
406
+
407
+ routes_filename = File.join(".", "app", "routes.rb")
408
+ routes_file = File.read(routes_filename)
409
+ idx = routes_file.rindex(/end/)
410
+ routes_file.insert(idx, "\n resource \"/#{filename}\", :#{class_name}\n")
411
+
412
+ File.write routes_filename, routes_file
413
+ end
414
+
415
+ def generate_view(filename, accessors = [])
416
+ class_name = filename.pascal_case
417
+ filepath = File.join(VIEW_DIR, "#{filename}.rb")
418
+
419
+ return if File.exist?(filepath)
420
+
421
+ view = <<~RB
422
+ class #{class_name} < Layout
423
+ view "#{filename}.emote"
424
+ attr_accessor #{accessors.map { |a| ":#{a}" }.join(', ')}
425
+
426
+ def initialize(#{accessors.join(', ')})
427
+ #{accessors.map { |a| "@#{a} = #{a}" }.join("\n")}
428
+ end
429
+ end
430
+ RB
431
+ File.write(filepath, view)
432
+
433
+ generate_template(filename)
434
+ end
435
+
436
+ def generate_views(prefix)
437
+ %w[new show edit index].each do |suffix|
438
+ generate_view("#{prefix}_#{suffix}", [])
439
+ end
440
+ end
441
+
442
+ def generate_template(filename)
443
+ filename = File.join(VIEW_DIR, "#{filename}.emote")
444
+
445
+ return if File.exist?(filename)
446
+
447
+ File.write(filename, "<div></div>")
448
+ end
449
+
450
+ def generate_templates(prefix)
451
+ %w[index new edit show form].each do |page|
452
+ generate_template("#{prefix}_#{page}")
453
+ end
454
+ end
455
+
456
+ def generate_migration(filename, columns = [])
457
+ table_name = filename.split('_').last
458
+ column_string = columns.map do |c|
459
+ name, type = c.split(':')
460
+
461
+ "#{name} #{type}"
462
+ end.join(",\n ")
463
+
464
+ sql = <<~SQL
465
+ -- name: up
466
+ create table #{table_name} (
467
+ #{table_name}_id integer primary key,
468
+ #{column_string}
469
+ created_at integer not null default(strftime('%s', 'now')),
470
+ updated_at integer
471
+ )
472
+
473
+ -- name: down
474
+ drop table #{table_name}
475
+ SQL
476
+
477
+ sql = "-- name: up\n\n-- name: down" unless filename.start_with?("create_table")
478
+
479
+ filename = "#{Time.now.to_i}_#{filename}"
480
+ filepath = File.join(".", "db", "migrations", "#{filename}.sql")
481
+
482
+ puts "Writing file #{filepath}"
483
+ File.write(filepath, sql)
484
+ end
485
+ end
486
+ end
487
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yescode
4
+ class LogfmtFormatter < ::Logger::Formatter
5
+ def call(severity, datetime, progname, msg)
6
+ timestamp = datetime.strftime('%Y-%m-%d %H:%M:%S.%L')
7
+
8
+ parts = {
9
+ level: severity.downcase,
10
+ in: progname
11
+ }
12
+
13
+ parts.merge!(msg)
14
+
15
+ output = "[#{timestamp}] #{parts.reject { |_, v| v.nil? }.map { |k, v| "#{k}=#{v}" }.join(' ')}\n"
16
+
17
+ if severity.downcase == "debug"
18
+ blue(output)
19
+ else
20
+ output
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def blue(str)
27
+ "\e[34m#{str}\e[0m"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ module Yescode
2
+ class Queries
3
+ class << self
4
+ def name(line)
5
+ line.match(/^--\s*name\s*:\s*(\S+)/).to_a.last
6
+ end
7
+
8
+ def sql(line)
9
+ line.match(/^[^-{2}](.*)/).to_a.first
10
+ end
11
+
12
+ def queries(filename)
13
+ queries = []
14
+
15
+ File.foreach filename do |line|
16
+ query_name = name(line)
17
+ query_sql = sql(line)
18
+
19
+ queries << [query_name.to_sym, ""] if query_name
20
+ queries.last[1] = "#{queries.last[1]} #{query_sql}".strip if query_sql
21
+ end
22
+
23
+ queries
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ module Refinements
2
+ SNAKE_CASE_REGEX = /\B([A-Z])/
3
+
4
+ refine String do
5
+ def snake_case
6
+ gsub(SNAKE_CASE_REGEX, '_\1').downcase
7
+ end
8
+
9
+ def camel_case
10
+ first, *rest = split("_")
11
+
12
+ "#{first}#{rest.map(&:capitalize).join}"
13
+ end
14
+
15
+ def pascal_case
16
+ split("_").map(&:capitalize).join
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+ module Yescode
2
+ module RequestCache
3
+ class Middleware
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ RequestCache.begin!
10
+
11
+ status, headers, body = @app.call(env)
12
+
13
+ body = Rack::BodyProxy.new(body) do
14
+ RequestCache.end!
15
+ RequestCache.clear!
16
+ end
17
+
18
+ returned = true
19
+
20
+ [status, headers, body]
21
+ ensure
22
+ unless returned
23
+ RequestCache.end!
24
+ RequestCache.clear!
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end