yescode 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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