tarsier 0.1.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 +175 -0
- data/LICENSE.txt +21 -0
- data/README.md +984 -0
- data/exe/tarsier +7 -0
- data/lib/tarsier/application.rb +336 -0
- data/lib/tarsier/cli/commands/console.rb +87 -0
- data/lib/tarsier/cli/commands/generate.rb +85 -0
- data/lib/tarsier/cli/commands/help.rb +50 -0
- data/lib/tarsier/cli/commands/new.rb +59 -0
- data/lib/tarsier/cli/commands/routes.rb +139 -0
- data/lib/tarsier/cli/commands/server.rb +123 -0
- data/lib/tarsier/cli/commands/version.rb +14 -0
- data/lib/tarsier/cli/generators/app.rb +528 -0
- data/lib/tarsier/cli/generators/base.rb +93 -0
- data/lib/tarsier/cli/generators/controller.rb +91 -0
- data/lib/tarsier/cli/generators/middleware.rb +81 -0
- data/lib/tarsier/cli/generators/migration.rb +109 -0
- data/lib/tarsier/cli/generators/model.rb +109 -0
- data/lib/tarsier/cli/generators/resource.rb +27 -0
- data/lib/tarsier/cli/loader.rb +18 -0
- data/lib/tarsier/cli.rb +46 -0
- data/lib/tarsier/controller.rb +282 -0
- data/lib/tarsier/database.rb +588 -0
- data/lib/tarsier/errors.rb +77 -0
- data/lib/tarsier/middleware/base.rb +47 -0
- data/lib/tarsier/middleware/compression.rb +113 -0
- data/lib/tarsier/middleware/cors.rb +101 -0
- data/lib/tarsier/middleware/csrf.rb +88 -0
- data/lib/tarsier/middleware/logger.rb +74 -0
- data/lib/tarsier/middleware/rate_limit.rb +110 -0
- data/lib/tarsier/middleware/stack.rb +143 -0
- data/lib/tarsier/middleware/static.rb +124 -0
- data/lib/tarsier/model.rb +590 -0
- data/lib/tarsier/params.rb +269 -0
- data/lib/tarsier/query.rb +495 -0
- data/lib/tarsier/request.rb +274 -0
- data/lib/tarsier/response.rb +282 -0
- data/lib/tarsier/router/compiler.rb +173 -0
- data/lib/tarsier/router/node.rb +97 -0
- data/lib/tarsier/router/route.rb +119 -0
- data/lib/tarsier/router.rb +272 -0
- data/lib/tarsier/version.rb +5 -0
- data/lib/tarsier/websocket.rb +275 -0
- data/lib/tarsier.rb +167 -0
- data/sig/tarsier.rbs +485 -0
- metadata +230 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarsier
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
class New
|
|
7
|
+
class Generator < Generators::Base
|
|
8
|
+
def initialize(name, options = {})
|
|
9
|
+
super(name, options)
|
|
10
|
+
@app_path = name
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def generate
|
|
14
|
+
puts "Creating new Tarsier application: #{name}"
|
|
15
|
+
puts
|
|
16
|
+
|
|
17
|
+
create_directory(@app_path)
|
|
18
|
+
create_structure
|
|
19
|
+
create_files
|
|
20
|
+
initialize_git unless options[:skip_git]
|
|
21
|
+
run_bundle unless options[:skip_bundle]
|
|
22
|
+
|
|
23
|
+
print_success_message
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def create_structure
|
|
29
|
+
dirs = %w[
|
|
30
|
+
app/controllers
|
|
31
|
+
app/models
|
|
32
|
+
app/middleware
|
|
33
|
+
app/views
|
|
34
|
+
bin
|
|
35
|
+
config
|
|
36
|
+
db/migrations
|
|
37
|
+
lib
|
|
38
|
+
public
|
|
39
|
+
spec/controllers
|
|
40
|
+
spec/models
|
|
41
|
+
spec/support
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
dirs.reject! { |d| d.start_with?("app/views") } if options[:api]
|
|
45
|
+
dirs = %w[app/controllers bin config lib spec] if options[:minimal]
|
|
46
|
+
|
|
47
|
+
dirs.each { |dir| create_directory("#{@app_path}/#{dir}") }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def create_files
|
|
51
|
+
create_file("#{@app_path}/Gemfile", gemfile_content)
|
|
52
|
+
create_file("#{@app_path}/config.ru", config_ru_content)
|
|
53
|
+
create_file("#{@app_path}/config/application.rb", application_content)
|
|
54
|
+
create_file("#{@app_path}/config/routes.rb", routes_content)
|
|
55
|
+
create_file("#{@app_path}/app/controllers/application_controller.rb", app_controller)
|
|
56
|
+
create_file("#{@app_path}/app/controllers/home_controller.rb", home_controller)
|
|
57
|
+
create_file("#{@app_path}/spec/spec_helper.rb", spec_helper_content)
|
|
58
|
+
create_file("#{@app_path}/bin/server", server_script)
|
|
59
|
+
create_file("#{@app_path}/bin/console", console_script)
|
|
60
|
+
create_file("#{@app_path}/.rspec", rspec_content)
|
|
61
|
+
create_file("#{@app_path}/.gitignore", gitignore_content)
|
|
62
|
+
create_file("#{@app_path}/README.md", readme_content)
|
|
63
|
+
|
|
64
|
+
# Make scripts executable
|
|
65
|
+
FileUtils.chmod(0o755, "#{@app_path}/bin/server") if File.exist?("#{@app_path}/bin/server")
|
|
66
|
+
FileUtils.chmod(0o755, "#{@app_path}/bin/console") if File.exist?("#{@app_path}/bin/console")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def gemfile_content
|
|
70
|
+
<<~RUBY
|
|
71
|
+
# frozen_string_literal: true
|
|
72
|
+
|
|
73
|
+
source "https://rubygems.org"
|
|
74
|
+
|
|
75
|
+
gem "tarsier", "~> #{Tarsier::VERSION}"
|
|
76
|
+
gem "puma", "~> 6.0"
|
|
77
|
+
gem "rackup", "~> 2.1"
|
|
78
|
+
|
|
79
|
+
group :development do
|
|
80
|
+
gem "rerun"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
group :test do
|
|
84
|
+
gem "rspec", "~> 3.12"
|
|
85
|
+
gem "rack-test", "~> 2.1"
|
|
86
|
+
end
|
|
87
|
+
RUBY
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def config_ru_content
|
|
91
|
+
<<~RUBY
|
|
92
|
+
# frozen_string_literal: true
|
|
93
|
+
|
|
94
|
+
require_relative "config/application"
|
|
95
|
+
|
|
96
|
+
run Tarsier.current_app
|
|
97
|
+
RUBY
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def application_content
|
|
101
|
+
<<~RUBY
|
|
102
|
+
# frozen_string_literal: true
|
|
103
|
+
|
|
104
|
+
require "bundler/setup"
|
|
105
|
+
require "tarsier"
|
|
106
|
+
|
|
107
|
+
# Load application files
|
|
108
|
+
Dir[File.expand_path("../app/**/*.rb", __dir__)].sort.each { |f| require f }
|
|
109
|
+
|
|
110
|
+
Tarsier.application do
|
|
111
|
+
configure do
|
|
112
|
+
self.secret_key = ENV.fetch("SECRET_KEY", "development_secret_\#{SecureRandom.hex(32)}")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Middleware
|
|
116
|
+
use Tarsier::Middleware::Logger
|
|
117
|
+
#{options[:api] ? "" : "use Tarsier::Middleware::Static, root: 'public'"}
|
|
118
|
+
|
|
119
|
+
# Routes
|
|
120
|
+
routes do
|
|
121
|
+
root to: "home#index"
|
|
122
|
+
|
|
123
|
+
# Add your routes here
|
|
124
|
+
# resources :users
|
|
125
|
+
# namespace :api do
|
|
126
|
+
# resources :posts
|
|
127
|
+
# end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
RUBY
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def routes_content
|
|
134
|
+
<<~RUBY
|
|
135
|
+
# frozen_string_literal: true
|
|
136
|
+
|
|
137
|
+
# This file documents your routes.
|
|
138
|
+
# Routes are defined in config/application.rb within the routes block.
|
|
139
|
+
#
|
|
140
|
+
# Example routes:
|
|
141
|
+
# root to: "home#index"
|
|
142
|
+
# get "/about", to: "pages#about"
|
|
143
|
+
# resources :users
|
|
144
|
+
# namespace :api do
|
|
145
|
+
# resources :posts
|
|
146
|
+
# end
|
|
147
|
+
RUBY
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def app_controller
|
|
151
|
+
<<~RUBY
|
|
152
|
+
# frozen_string_literal: true
|
|
153
|
+
|
|
154
|
+
class ApplicationController < Tarsier::Controller
|
|
155
|
+
# Add shared controller logic here
|
|
156
|
+
end
|
|
157
|
+
RUBY
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def home_controller
|
|
161
|
+
<<~RUBY
|
|
162
|
+
# frozen_string_literal: true
|
|
163
|
+
|
|
164
|
+
class HomeController < ApplicationController
|
|
165
|
+
def index
|
|
166
|
+
#{options[:api] ? 'render json: { message: "Welcome to Tarsier!" }' : 'render html: welcome_html'}
|
|
167
|
+
end
|
|
168
|
+
#{options[:api] ? "" : home_html_method}
|
|
169
|
+
end
|
|
170
|
+
RUBY
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def home_html_method
|
|
174
|
+
<<~RUBY
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
def welcome_html
|
|
179
|
+
<<~HTML
|
|
180
|
+
<!DOCTYPE html>
|
|
181
|
+
<html lang="en">
|
|
182
|
+
<head>
|
|
183
|
+
<meta charset="UTF-8">
|
|
184
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
185
|
+
<title>#{name} | Tarsier</title>
|
|
186
|
+
<style>
|
|
187
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
188
|
+
body {
|
|
189
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
190
|
+
background: #2d3748;
|
|
191
|
+
color: #e2e8f0;
|
|
192
|
+
min-height: 100vh;
|
|
193
|
+
display: flex;
|
|
194
|
+
align-items: center;
|
|
195
|
+
justify-content: center;
|
|
196
|
+
}
|
|
197
|
+
.container {
|
|
198
|
+
text-align: center;
|
|
199
|
+
padding: 2rem;
|
|
200
|
+
}
|
|
201
|
+
.logo {
|
|
202
|
+
width: 180px;
|
|
203
|
+
height: 180px;
|
|
204
|
+
margin-bottom: 2rem;
|
|
205
|
+
}
|
|
206
|
+
.logo svg {
|
|
207
|
+
width: 100%;
|
|
208
|
+
height: 100%;
|
|
209
|
+
}
|
|
210
|
+
h1 {
|
|
211
|
+
font-size: 2.5rem;
|
|
212
|
+
font-weight: 300;
|
|
213
|
+
margin-bottom: 0.5rem;
|
|
214
|
+
color: #fff;
|
|
215
|
+
}
|
|
216
|
+
.app-name {
|
|
217
|
+
color: #e57373;
|
|
218
|
+
font-weight: 600;
|
|
219
|
+
}
|
|
220
|
+
.tagline {
|
|
221
|
+
font-size: 1.1rem;
|
|
222
|
+
color: #a0aec0;
|
|
223
|
+
margin-bottom: 2.5rem;
|
|
224
|
+
}
|
|
225
|
+
.status {
|
|
226
|
+
display: inline-flex;
|
|
227
|
+
align-items: center;
|
|
228
|
+
gap: 0.5rem;
|
|
229
|
+
background: rgba(72, 187, 120, 0.15);
|
|
230
|
+
color: #68d391;
|
|
231
|
+
padding: 0.5rem 1rem;
|
|
232
|
+
border-radius: 2rem;
|
|
233
|
+
font-size: 0.9rem;
|
|
234
|
+
margin-bottom: 2rem;
|
|
235
|
+
}
|
|
236
|
+
.status-dot {
|
|
237
|
+
width: 8px;
|
|
238
|
+
height: 8px;
|
|
239
|
+
background: #68d391;
|
|
240
|
+
border-radius: 50%;
|
|
241
|
+
animation: pulse 2s infinite;
|
|
242
|
+
}
|
|
243
|
+
@keyframes pulse {
|
|
244
|
+
0%, 100% { opacity: 1; }
|
|
245
|
+
50% { opacity: 0.5; }
|
|
246
|
+
}
|
|
247
|
+
.links {
|
|
248
|
+
display: flex;
|
|
249
|
+
gap: 1rem;
|
|
250
|
+
justify-content: center;
|
|
251
|
+
flex-wrap: wrap;
|
|
252
|
+
}
|
|
253
|
+
.link {
|
|
254
|
+
color: #e57373;
|
|
255
|
+
text-decoration: none;
|
|
256
|
+
padding: 0.75rem 1.5rem;
|
|
257
|
+
border: 1px solid #e57373;
|
|
258
|
+
border-radius: 0.5rem;
|
|
259
|
+
transition: all 0.2s;
|
|
260
|
+
font-size: 0.9rem;
|
|
261
|
+
}
|
|
262
|
+
.link:hover {
|
|
263
|
+
background: #e57373;
|
|
264
|
+
color: #2d3748;
|
|
265
|
+
}
|
|
266
|
+
.footer {
|
|
267
|
+
margin-top: 3rem;
|
|
268
|
+
font-size: 0.85rem;
|
|
269
|
+
color: #718096;
|
|
270
|
+
}
|
|
271
|
+
code {
|
|
272
|
+
background: rgba(0,0,0,0.3);
|
|
273
|
+
padding: 0.2rem 0.5rem;
|
|
274
|
+
border-radius: 0.25rem;
|
|
275
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
276
|
+
font-size: 0.85rem;
|
|
277
|
+
}
|
|
278
|
+
</style>
|
|
279
|
+
</head>
|
|
280
|
+
<body>
|
|
281
|
+
<div class="container">
|
|
282
|
+
<div class="logo">
|
|
283
|
+
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
|
284
|
+
<!-- Ears -->
|
|
285
|
+
<polygon points="35,85 55,45 75,85" fill="#e57373"/>
|
|
286
|
+
<polygon points="125,85 145,45 165,85" fill="#e57373"/>
|
|
287
|
+
<!-- Left Eye Outer -->
|
|
288
|
+
<circle cx="65" cy="110" r="45" fill="#1a1a1a"/>
|
|
289
|
+
<circle cx="65" cy="110" r="40" fill="#fff"/>
|
|
290
|
+
<circle cx="65" cy="110" r="32" fill="#e57373"/>
|
|
291
|
+
<circle cx="65" cy="110" r="24" fill="#fff" fill-opacity="0.3"/>
|
|
292
|
+
<circle cx="65" cy="110" r="16" fill="#8b1a1a"/>
|
|
293
|
+
<!-- Diamond in left eye -->
|
|
294
|
+
<polygon points="65,100 72,110 65,120 58,110" fill="#fff" fill-opacity="0.8"/>
|
|
295
|
+
<!-- Right Eye Outer -->
|
|
296
|
+
<circle cx="135" cy="110" r="45" fill="#1a1a1a"/>
|
|
297
|
+
<circle cx="135" cy="110" r="40" fill="#fff"/>
|
|
298
|
+
<circle cx="135" cy="110" r="32" fill="#e57373"/>
|
|
299
|
+
<circle cx="135" cy="110" r="24" fill="#fff" fill-opacity="0.3"/>
|
|
300
|
+
<circle cx="135" cy="110" r="16" fill="#8b1a1a"/>
|
|
301
|
+
<!-- Highlight in right eye -->
|
|
302
|
+
<circle cx="140" cy="105" r="4" fill="#fff"/>
|
|
303
|
+
<!-- Nose -->
|
|
304
|
+
<polygon points="100,145 108,160 92,160" fill="#e57373"/>
|
|
305
|
+
</svg>
|
|
306
|
+
</div>
|
|
307
|
+
<h1>Welcome to <span class="app-name">#{name}</span></h1>
|
|
308
|
+
<p class="tagline">Powered by Tarsier Framework</p>
|
|
309
|
+
<div class="status">
|
|
310
|
+
<span class="status-dot"></span>
|
|
311
|
+
Application running
|
|
312
|
+
</div>
|
|
313
|
+
<div class="links">
|
|
314
|
+
<a href="https://github.com/tarsier-rb/tarsier" class="link" target="_blank">Documentation</a>
|
|
315
|
+
<a href="https://github.com/tarsier-rb/tarsier" class="link" target="_blank">GitHub</a>
|
|
316
|
+
</div>
|
|
317
|
+
<p class="footer">
|
|
318
|
+
Edit <code>app/controllers/home_controller.rb</code> to customize this page
|
|
319
|
+
</p>
|
|
320
|
+
</div>
|
|
321
|
+
</body>
|
|
322
|
+
</html>
|
|
323
|
+
HTML
|
|
324
|
+
end
|
|
325
|
+
RUBY
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def spec_helper_content
|
|
329
|
+
<<~RUBY
|
|
330
|
+
# frozen_string_literal: true
|
|
331
|
+
|
|
332
|
+
require_relative "../config/application"
|
|
333
|
+
require "rack/test"
|
|
334
|
+
|
|
335
|
+
RSpec.configure do |config|
|
|
336
|
+
config.include Rack::Test::Methods
|
|
337
|
+
|
|
338
|
+
def app
|
|
339
|
+
Tarsier.app
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
RUBY
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def rspec_content
|
|
346
|
+
<<~TEXT
|
|
347
|
+
--require spec_helper
|
|
348
|
+
--format documentation
|
|
349
|
+
--color
|
|
350
|
+
TEXT
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def gitignore_content
|
|
354
|
+
<<~TEXT
|
|
355
|
+
# Dependencies
|
|
356
|
+
/.bundle/
|
|
357
|
+
/vendor/bundle/
|
|
358
|
+
|
|
359
|
+
# Environment
|
|
360
|
+
.env
|
|
361
|
+
.env.local
|
|
362
|
+
.env.development.local
|
|
363
|
+
.env.test.local
|
|
364
|
+
.env.production.local
|
|
365
|
+
|
|
366
|
+
# Logs
|
|
367
|
+
/log/*.log
|
|
368
|
+
/tmp/
|
|
369
|
+
|
|
370
|
+
# Database
|
|
371
|
+
*.sqlite3
|
|
372
|
+
*.sqlite3-journal
|
|
373
|
+
/db/*.sqlite3
|
|
374
|
+
/db/*.sqlite3-*
|
|
375
|
+
|
|
376
|
+
# IDE and editors
|
|
377
|
+
/.idea/
|
|
378
|
+
/.vscode/
|
|
379
|
+
*.swp
|
|
380
|
+
*.swo
|
|
381
|
+
*~
|
|
382
|
+
.project
|
|
383
|
+
.settings/
|
|
384
|
+
|
|
385
|
+
# OS files
|
|
386
|
+
.DS_Store
|
|
387
|
+
.DS_Store?
|
|
388
|
+
._*
|
|
389
|
+
Thumbs.db
|
|
390
|
+
ehthumbs.db
|
|
391
|
+
|
|
392
|
+
# Test and coverage
|
|
393
|
+
/coverage/
|
|
394
|
+
/spec/examples.txt
|
|
395
|
+
|
|
396
|
+
# Build artifacts
|
|
397
|
+
*.gem
|
|
398
|
+
/pkg/
|
|
399
|
+
|
|
400
|
+
# Temporary files
|
|
401
|
+
*.tmp
|
|
402
|
+
*.bak
|
|
403
|
+
*.pid
|
|
404
|
+
|
|
405
|
+
# Secrets
|
|
406
|
+
/config/secrets.yml
|
|
407
|
+
/config/master.key
|
|
408
|
+
/config/credentials.yml.enc
|
|
409
|
+
TEXT
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def readme_content
|
|
413
|
+
<<~MARKDOWN
|
|
414
|
+
# #{camelize(name)}
|
|
415
|
+
|
|
416
|
+
A Tarsier web application.
|
|
417
|
+
|
|
418
|
+
## Setup
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
bundle install
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
## Development
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
bin/server
|
|
428
|
+
# or
|
|
429
|
+
bundle exec puma
|
|
430
|
+
# or
|
|
431
|
+
bundle exec rackup
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## Console
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
bin/console
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
## Testing
|
|
441
|
+
|
|
442
|
+
```bash
|
|
443
|
+
bundle exec rspec
|
|
444
|
+
```
|
|
445
|
+
MARKDOWN
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def server_script
|
|
449
|
+
<<~BASH
|
|
450
|
+
#!/usr/bin/env ruby
|
|
451
|
+
# frozen_string_literal: true
|
|
452
|
+
|
|
453
|
+
require "bundler/setup"
|
|
454
|
+
|
|
455
|
+
port = ENV.fetch("PORT", 7827)
|
|
456
|
+
host = ENV.fetch("HOST", "0.0.0.0")
|
|
457
|
+
env = ENV.fetch("TARSIER_ENV", "development")
|
|
458
|
+
|
|
459
|
+
puts "Tarsier Development Server"
|
|
460
|
+
puts "=" * 40
|
|
461
|
+
puts "Environment: \#{env}"
|
|
462
|
+
puts "Listening on: http://\#{host}:\#{port}"
|
|
463
|
+
puts "Press Ctrl+C to stop"
|
|
464
|
+
puts "=" * 40
|
|
465
|
+
puts
|
|
466
|
+
|
|
467
|
+
if system("which puma > /dev/null 2>&1")
|
|
468
|
+
exec "bundle", "exec", "puma", "-p", port.to_s, "-b", "tcp://\#{host}:\#{port}"
|
|
469
|
+
else
|
|
470
|
+
exec "bundle", "exec", "rackup", "-p", port.to_s, "-o", host
|
|
471
|
+
end
|
|
472
|
+
BASH
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def console_script
|
|
476
|
+
<<~BASH
|
|
477
|
+
#!/usr/bin/env ruby
|
|
478
|
+
# frozen_string_literal: true
|
|
479
|
+
|
|
480
|
+
require "bundler/setup"
|
|
481
|
+
require_relative "../config/application"
|
|
482
|
+
|
|
483
|
+
puts "Tarsier Console (\#{ENV.fetch('TARSIER_ENV', 'development')})"
|
|
484
|
+
puts "Type 'exit' to quit"
|
|
485
|
+
puts
|
|
486
|
+
|
|
487
|
+
require "irb"
|
|
488
|
+
IRB.start
|
|
489
|
+
BASH
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def initialize_git
|
|
493
|
+
Dir.chdir(@app_path) do
|
|
494
|
+
system("git init -q")
|
|
495
|
+
puts " \e[32mcreate\e[0m .git"
|
|
496
|
+
end
|
|
497
|
+
rescue StandardError
|
|
498
|
+
puts " \e[33mskip\e[0m git init (git not available)"
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def run_bundle
|
|
502
|
+
puts "\nInstalling dependencies..."
|
|
503
|
+
Dir.chdir(@app_path) do
|
|
504
|
+
system("bundle install")
|
|
505
|
+
end
|
|
506
|
+
rescue StandardError
|
|
507
|
+
puts " \e[33mskip\e[0m bundle install"
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def print_success_message
|
|
511
|
+
puts
|
|
512
|
+
puts "=" * 50
|
|
513
|
+
puts "Success! Created #{name}"
|
|
514
|
+
puts "=" * 50
|
|
515
|
+
puts
|
|
516
|
+
puts "Next steps:"
|
|
517
|
+
puts " cd #{name}"
|
|
518
|
+
puts " bundle install"
|
|
519
|
+
puts " bin/server"
|
|
520
|
+
puts
|
|
521
|
+
puts "Then open http://localhost:7827"
|
|
522
|
+
puts
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarsier
|
|
4
|
+
class CLI
|
|
5
|
+
module Generators
|
|
6
|
+
class Base
|
|
7
|
+
attr_reader :name, :options
|
|
8
|
+
|
|
9
|
+
def initialize(name, options = {})
|
|
10
|
+
@name = name
|
|
11
|
+
@options = options
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def generate
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
protected
|
|
19
|
+
|
|
20
|
+
def create_file(path, content)
|
|
21
|
+
dir = File.dirname(path)
|
|
22
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
23
|
+
|
|
24
|
+
if File.exist?(path)
|
|
25
|
+
puts " \e[33mskip\e[0m #{path} (already exists)"
|
|
26
|
+
return false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
File.write(path, content)
|
|
30
|
+
puts " \e[32mcreate\e[0m #{path}"
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def create_directory(path)
|
|
35
|
+
if File.directory?(path)
|
|
36
|
+
puts " \e[33mskip\e[0m #{path}/ (already exists)"
|
|
37
|
+
return false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
FileUtils.mkdir_p(path)
|
|
41
|
+
puts " \e[32mcreate\e[0m #{path}/"
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def append_to_file(path, content)
|
|
46
|
+
unless File.exist?(path)
|
|
47
|
+
puts " \e[31merror\e[0m #{path} does not exist"
|
|
48
|
+
return false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
File.open(path, "a") { |f| f.puts content }
|
|
52
|
+
puts " \e[32mappend\e[0m #{path}"
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def camelize(string)
|
|
57
|
+
string.to_s.split(/[_-]/).map(&:capitalize).join
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def underscore(string)
|
|
61
|
+
string.to_s.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, "")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def pluralize(string)
|
|
65
|
+
word = string.to_s
|
|
66
|
+
# Don't pluralize if already plural (simple check)
|
|
67
|
+
return word if word.end_with?("s") && !word.end_with?("ss")
|
|
68
|
+
|
|
69
|
+
if word.end_with?("y") && !%w[a e i o u].include?(word[-2])
|
|
70
|
+
word[0..-2] + "ies"
|
|
71
|
+
elsif word.end_with?("s", "x", "ch", "sh")
|
|
72
|
+
word + "es"
|
|
73
|
+
else
|
|
74
|
+
word + "s"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def singularize(string)
|
|
79
|
+
word = string.to_s
|
|
80
|
+
if word.end_with?("ies")
|
|
81
|
+
word[0..-4] + "y"
|
|
82
|
+
elsif word.end_with?("es")
|
|
83
|
+
word[0..-3]
|
|
84
|
+
elsif word.end_with?("s")
|
|
85
|
+
word[0..-2]
|
|
86
|
+
else
|
|
87
|
+
word
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarsier
|
|
4
|
+
class CLI
|
|
5
|
+
module Generators
|
|
6
|
+
class Controller < Base
|
|
7
|
+
def initialize(name, actions = [])
|
|
8
|
+
super(name)
|
|
9
|
+
@actions = actions
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def generate
|
|
13
|
+
create_file(controller_path, controller_content)
|
|
14
|
+
create_file(spec_path, spec_content)
|
|
15
|
+
puts "\nController generated successfully!"
|
|
16
|
+
puts "Add routes to config/routes.rb:"
|
|
17
|
+
puts route_suggestion
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def controller_path
|
|
23
|
+
"app/controllers/#{underscore(name)}_controller.rb"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def spec_path
|
|
27
|
+
"spec/controllers/#{underscore(name)}_controller_spec.rb"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def controller_content
|
|
31
|
+
<<~RUBY
|
|
32
|
+
# frozen_string_literal: true
|
|
33
|
+
|
|
34
|
+
class #{camelize(name)}Controller < Tarsier::Controller
|
|
35
|
+
#{action_methods}
|
|
36
|
+
end
|
|
37
|
+
RUBY
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def action_methods
|
|
41
|
+
return " # Add your actions here" if @actions.empty?
|
|
42
|
+
|
|
43
|
+
@actions.map do |action|
|
|
44
|
+
<<~RUBY.chomp
|
|
45
|
+
def #{action}
|
|
46
|
+
render json: { action: "#{action}" }
|
|
47
|
+
end
|
|
48
|
+
RUBY
|
|
49
|
+
end.join("\n\n").gsub(/^/, " ")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def spec_content
|
|
53
|
+
<<~RUBY
|
|
54
|
+
# frozen_string_literal: true
|
|
55
|
+
|
|
56
|
+
require "spec_helper"
|
|
57
|
+
|
|
58
|
+
RSpec.describe #{camelize(name)}Controller do
|
|
59
|
+
#{action_specs}
|
|
60
|
+
end
|
|
61
|
+
RUBY
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def action_specs
|
|
65
|
+
return ' pending "add some examples"' if @actions.empty?
|
|
66
|
+
|
|
67
|
+
@actions.map do |action|
|
|
68
|
+
<<~RUBY.chomp
|
|
69
|
+
describe "##{action}" do
|
|
70
|
+
it "returns success" do
|
|
71
|
+
# Add your test here
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
RUBY
|
|
75
|
+
end.join("\n\n").gsub(/^/, " ")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def route_suggestion
|
|
79
|
+
resource_name = pluralize(underscore(name))
|
|
80
|
+
if @actions.empty?
|
|
81
|
+
" resources :#{resource_name}"
|
|
82
|
+
else
|
|
83
|
+
@actions.map do |action|
|
|
84
|
+
" get '/#{resource_name}/#{action}', to: '#{underscore(name)}##{action}'"
|
|
85
|
+
end.join("\n")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|