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
@@ -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
|
data/lib/yescode/env.rb
ADDED
@@ -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
|