jimmu 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.
Files changed (94) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +279 -0
  4. data/examples/bbs/app.rb +21 -0
  5. data/examples/bbs/public/css/style.css +15 -0
  6. data/examples/bbs/views/404.erb +1 -0
  7. data/examples/bbs/views/500.erb +1 -0
  8. data/examples/bbs/views/index.erb +29 -0
  9. data/examples/bbs/views/layout.erb +15 -0
  10. data/examples/blog/app.rb +28 -0
  11. data/examples/blog/public/css/style.css +18 -0
  12. data/examples/blog/views/404.erb +1 -0
  13. data/examples/blog/views/500.erb +1 -0
  14. data/examples/blog/views/index.erb +9 -0
  15. data/examples/blog/views/layout.erb +15 -0
  16. data/examples/blog/views/new.erb +18 -0
  17. data/examples/blog/views/post/[id].erb +17 -0
  18. data/examples/chatroom/app.rb +44 -0
  19. data/examples/chatroom/public/css/style.css +15 -0
  20. data/examples/chatroom/views/404.erb +1 -0
  21. data/examples/chatroom/views/500.erb +1 -0
  22. data/examples/chatroom/views/index.erb +18 -0
  23. data/examples/chatroom/views/layout.erb +12 -0
  24. data/examples/chatroom/views/room/[name].erb +47 -0
  25. data/examples/counter/app.rb +15 -0
  26. data/examples/counter/public/css/style.css +8 -0
  27. data/examples/counter/public/js/app.js +11 -0
  28. data/examples/counter/views/404.erb +1 -0
  29. data/examples/counter/views/500.erb +1 -0
  30. data/examples/counter/views/api/increment.erb +5 -0
  31. data/examples/counter/views/api/reset.erb +4 -0
  32. data/examples/counter/views/index.erb +8 -0
  33. data/examples/counter/views/layout.erb +13 -0
  34. data/examples/hello-world/app.rb +6 -0
  35. data/examples/hello-world/public/css/style.css +3 -0
  36. data/examples/hello-world/views/404.erb +2 -0
  37. data/examples/hello-world/views/500.erb +1 -0
  38. data/examples/hello-world/views/greet/[name].erb +2 -0
  39. data/examples/hello-world/views/index.erb +3 -0
  40. data/examples/hello-world/views/layout.erb +12 -0
  41. data/examples/image-upload/app.rb +22 -0
  42. data/examples/image-upload/public/css/style.css +11 -0
  43. data/examples/image-upload/views/404.erb +1 -0
  44. data/examples/image-upload/views/500.erb +1 -0
  45. data/examples/image-upload/views/index.erb +31 -0
  46. data/examples/image-upload/views/layout.erb +15 -0
  47. data/examples/json-api/app.rb +27 -0
  48. data/examples/json-api/views/404.erb +2 -0
  49. data/examples/json-api/views/500.erb +2 -0
  50. data/examples/json-api/views/api/notes/[id].erb +33 -0
  51. data/examples/json-api/views/api/notes.erb +20 -0
  52. data/examples/json-api/views/index.erb +13 -0
  53. data/examples/login-demo/app.rb +31 -0
  54. data/examples/login-demo/public/css/style.css +12 -0
  55. data/examples/login-demo/views/404.erb +1 -0
  56. data/examples/login-demo/views/500.erb +1 -0
  57. data/examples/login-demo/views/index.erb +6 -0
  58. data/examples/login-demo/views/layout.erb +24 -0
  59. data/examples/login-demo/views/login.erb +22 -0
  60. data/examples/login-demo/views/logout.erb +4 -0
  61. data/examples/login-demo/views/mypage/index.erb +3 -0
  62. data/examples/login-demo/views/signup.erb +24 -0
  63. data/examples/poll/app.rb +55 -0
  64. data/examples/poll/public/css/style.css +10 -0
  65. data/examples/poll/public/js/app.js +24 -0
  66. data/examples/poll/views/404.erb +1 -0
  67. data/examples/poll/views/500.erb +1 -0
  68. data/examples/poll/views/index.erb +6 -0
  69. data/examples/poll/views/layout.erb +13 -0
  70. data/examples/todo/app.rb +21 -0
  71. data/examples/todo/public/css/style.css +17 -0
  72. data/examples/todo/views/404.erb +1 -0
  73. data/examples/todo/views/500.erb +1 -0
  74. data/examples/todo/views/delete/[id].erb +4 -0
  75. data/examples/todo/views/index.erb +27 -0
  76. data/examples/todo/views/layout.erb +15 -0
  77. data/examples/todo/views/toggle/[id].erb +5 -0
  78. data/examples/url-shortener/app.rb +39 -0
  79. data/examples/url-shortener/public/css/style.css +13 -0
  80. data/examples/url-shortener/views/404.erb +1 -0
  81. data/examples/url-shortener/views/500.erb +1 -0
  82. data/examples/url-shortener/views/[code].erb +10 -0
  83. data/examples/url-shortener/views/api/shorten.erb +13 -0
  84. data/examples/url-shortener/views/index.erb +35 -0
  85. data/examples/url-shortener/views/layout.erb +15 -0
  86. data/examples/visitor-counter/app.rb +22 -0
  87. data/examples/visitor-counter/public/css/style.css +4 -0
  88. data/examples/visitor-counter/views/404.erb +1 -0
  89. data/examples/visitor-counter/views/500.erb +1 -0
  90. data/examples/visitor-counter/views/index.erb +11 -0
  91. data/examples/visitor-counter/views/layout.erb +12 -0
  92. data/exe/jimmu +5 -0
  93. data/lib/jimmu.rb +1916 -0
  94. metadata +143 -0
@@ -0,0 +1,21 @@
1
+ # Todo app.
2
+ # Demonstrates: SQLite CRUD, forms, dynamic routes for actions,
3
+ # redirect.
4
+
5
+ port 3000
6
+
7
+ db = sqlite "db/todo.db"
8
+ db.exec <<~SQL
9
+ CREATE TABLE IF NOT EXISTS todos (
10
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
11
+ text TEXT NOT NULL,
12
+ done INTEGER NOT NULL DEFAULT 0,
13
+ created_at TEXT NOT NULL
14
+ )
15
+ SQL
16
+
17
+ before do
18
+ log "#{request.method} #{request.path}"
19
+ end
20
+
21
+ run
@@ -0,0 +1,17 @@
1
+ :root{color-scheme:light dark}
2
+ *{box-sizing:border-box}
3
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#fafafa;color:#1a1a1a;margin:0}
4
+ .wrap{max-width:480px;margin:0 auto;padding:48px 20px}
5
+ h1{font-size:22px}
6
+ .add-form{display:flex;gap:8px;margin-bottom:20px}
7
+ .add-form input{flex:1;padding:12px;border-radius:8px;border:1px solid #ddd;font-size:15px}
8
+ .add-form button{padding:12px 18px;border-radius:8px;border:0;background:#222;color:#fff;font-weight:600;cursor:pointer}
9
+ .todos{list-style:none;padding:0;display:flex;flex-direction:column;gap:6px}
10
+ .todos li{display:flex;align-items:center;gap:10px;background:#fff;padding:10px 12px;border-radius:8px;box-shadow:0 1px 2px rgba(0,0,0,.06)}
11
+ .todos li.done .text{text-decoration:line-through;color:#999}
12
+ .inline{display:inline;margin:0}
13
+ .check{width:26px;height:26px;border-radius:50%;border:1px solid #ccc;background:#fff;cursor:pointer;color:#2a2}
14
+ .todos li.done .check{background:#2a2;color:#fff;border-color:#2a2}
15
+ .text{flex:1}
16
+ .del{border:0;background:none;color:#c33;font-size:18px;cursor:pointer;padding:0 6px}
17
+ .empty{color:#999;text-align:center;padding:20px 0}
@@ -0,0 +1 @@
1
+ <h1>404</h1><p><a href="/">Home</a></p>
@@ -0,0 +1 @@
1
+ <h1>500</h1>
@@ -0,0 +1,4 @@
1
+ <%
2
+ db.exec("DELETE FROM todos WHERE id = ?", params[:id])
3
+ redirect "/"
4
+ %>
@@ -0,0 +1,27 @@
1
+ <%
2
+ if request.method == "POST"
3
+ text = params[:text].to_s.strip
4
+ db.exec("INSERT INTO todos(text, done, created_at) VALUES (?, 0, ?)", text, Time.now.to_s) unless text.empty?
5
+ redirect "/"
6
+ end
7
+ todos = db.query("SELECT * FROM todos ORDER BY done ASC, id DESC")
8
+ %>
9
+ <form method="post" action="/" class="add-form">
10
+ <input type="text" name="text" placeholder="What needs doing?" required autofocus>
11
+ <button type="submit">Add</button>
12
+ </form>
13
+
14
+ <ul class="todos">
15
+ <% todos.each do |todo| %>
16
+ <li class="<%= todo[:done] == 1 ? 'done' : '' %>">
17
+ <form method="post" action="/toggle/<%= todo[:id] %>" class="inline">
18
+ <button type="submit" class="check" aria-label="toggle"><%= todo[:done] == 1 ? "&#10003;" : "" %></button>
19
+ </form>
20
+ <span class="text"><%= CGI.escapeHTML(todo[:text]) %></span>
21
+ <form method="post" action="/delete/<%= todo[:id] %>" class="inline">
22
+ <button type="submit" class="del" aria-label="delete">&times;</button>
23
+ </form>
24
+ </li>
25
+ <% end %>
26
+ </ul>
27
+ <% if todos.empty? %><p class="empty">Nothing to do. Add a task above.</p><% end %>
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Jimmu Todo</title>
7
+ <link rel="stylesheet" href="/css/style.css">
8
+ </head>
9
+ <body>
10
+ <div class="wrap">
11
+ <h1>Todo</h1>
12
+ <%= yield %>
13
+ </div>
14
+ </body>
15
+ </html>
@@ -0,0 +1,5 @@
1
+ <%
2
+ todo = db.query("SELECT * FROM todos WHERE id = ?", params[:id]).first
3
+ db.exec("UPDATE todos SET done = ? WHERE id = ?", todo ? (todo[:done] == 1 ? 0 : 1) : 0, params[:id]) if todo
4
+ redirect "/"
5
+ %>
@@ -0,0 +1,39 @@
1
+ # URL shortener.
2
+ # Demonstrates: SQLite, a dynamic catch-all-ish route ("/[code].erb")
3
+ # that redirects, and a small JSON API.
4
+ #
5
+ # Note on helpers: a `def` written directly at the top level of
6
+ # app.rb only becomes a method on the application-configuration
7
+ # object itself, so it would NOT be callable from inside views (they
8
+ # run against a separate per-request object). Wrapping shared helper
9
+ # logic in a plain Ruby module with `module_function`, as below, makes
10
+ # it callable as Helpers.random_code from anywhere -- app.rb, views,
11
+ # before/after hooks, or websocket blocks.
12
+
13
+ port 3000
14
+
15
+ db = sqlite "db/links.db"
16
+ db.exec <<~SQL
17
+ CREATE TABLE IF NOT EXISTS links (
18
+ code TEXT PRIMARY KEY,
19
+ url TEXT NOT NULL,
20
+ clicks INTEGER NOT NULL DEFAULT 0,
21
+ created_at TEXT NOT NULL
22
+ )
23
+ SQL
24
+
25
+ module Helpers
26
+ module_function
27
+
28
+ CODE_CHARS = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
29
+
30
+ def random_code
31
+ Array.new(6) { CODE_CHARS.sample }.join
32
+ end
33
+ end
34
+
35
+ before do
36
+ log "#{request.method} #{request.path}"
37
+ end
38
+
39
+ run
@@ -0,0 +1,13 @@
1
+ :root{color-scheme:light dark}
2
+ *{box-sizing:border-box}
3
+ body{font-family:-apple-system,sans-serif;background:#f6f7fb;color:#1a1a1a;margin:0}
4
+ .wrap{max-width:640px;margin:0 auto;padding:48px 20px}
5
+ h1{font-size:22px}
6
+ .shorten-form{display:flex;gap:8px;margin:20px 0}
7
+ .shorten-form input{flex:1;padding:12px;border-radius:8px;border:1px solid #ddd;font-size:15px}
8
+ .shorten-form button{padding:12px 20px;border-radius:8px;border:0;background:#3b6fe0;color:#fff;font-weight:600;cursor:pointer}
9
+ .result{background:#e7f3e8;border:1px solid #bfe3c2;padding:12px 16px;border-radius:8px;margin-bottom:20px}
10
+ table{width:100%;border-collapse:collapse;font-size:14px}
11
+ th,td{text-align:left;padding:8px 6px;border-bottom:1px solid #e5e5e5}
12
+ .url{max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
13
+ a{color:#3b6fe0}
@@ -0,0 +1 @@
1
+ <h1>404</h1><p>Unknown short link.</p><p><a href="/">&larr; home</a></p>
@@ -0,0 +1 @@
1
+ <h1>500</h1>
@@ -0,0 +1,10 @@
1
+ <%
2
+ link = db.query("SELECT * FROM links WHERE code = ?", params[:code]).first
3
+ if link
4
+ db.exec("UPDATE links SET clicks = clicks + 1 WHERE code = ?", params[:code])
5
+ redirect link[:url]
6
+ else
7
+ status 404
8
+ render "404"
9
+ end
10
+ %>
@@ -0,0 +1,13 @@
1
+ <%
2
+ data = params
3
+ target = data[:url].to_s.strip
4
+ if target !~ /\Ahttps?:\/\//
5
+ status 422
6
+ json(error: "url must start with http:// or https://")
7
+ else
8
+ code = Helpers.random_code
9
+ code = Helpers.random_code while db.query("SELECT 1 AS x FROM links WHERE code = ?", code).any?
10
+ db.exec("INSERT INTO links(code, url, clicks, created_at) VALUES (?, ?, 0, ?)", code, target, Time.now.to_s)
11
+ json(code: code, short_url: "#{request.header('host')}/#{code}", url: target)
12
+ end
13
+ %>
@@ -0,0 +1,35 @@
1
+ <%
2
+ short_url = nil
3
+ if request.method == "POST"
4
+ target = params[:url].to_s.strip
5
+ if target =~ /\Ahttps?:\/\//
6
+ code = Helpers.random_code
7
+ code = Helpers.random_code while db.query("SELECT 1 AS x FROM links WHERE code = ?", code).any?
8
+ db.exec("INSERT INTO links(code, url, clicks, created_at) VALUES (?, ?, 0, ?)", code, target, Time.now.to_s)
9
+ short_url = "#{request.header('host')}/#{code}"
10
+ else
11
+ status 422
12
+ end
13
+ end
14
+ recent = db.query("SELECT * FROM links ORDER BY rowid DESC LIMIT 10")
15
+ %>
16
+ <form method="post" action="/" class="shorten-form">
17
+ <input type="url" name="url" placeholder="https://example.com/a/very/long/url" required value="<%= CGI.escapeHTML(params[:url].to_s) %>">
18
+ <button type="submit">Shorten</button>
19
+ </form>
20
+
21
+ <% if short_url %>
22
+ <div class="result">Your short link: <a href="http://<%= short_url %>"><%= short_url %></a></div>
23
+ <% end %>
24
+
25
+ <h2>Recent links</h2>
26
+ <table>
27
+ <tr><th>Code</th><th>URL</th><th>Clicks</th></tr>
28
+ <% recent.each do |link| %>
29
+ <tr>
30
+ <td><a href="/<%= link[:code] %>">/<%= link[:code] %></a></td>
31
+ <td class="url"><%= CGI.escapeHTML(link[:url]) %></td>
32
+ <td><%= link[:clicks] %></td>
33
+ </tr>
34
+ <% end %>
35
+ </table>
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Jimmu URL Shortener</title>
7
+ <link rel="stylesheet" href="/css/style.css">
8
+ </head>
9
+ <body>
10
+ <div class="wrap">
11
+ <h1>URL Shortener</h1>
12
+ <%= yield %>
13
+ </div>
14
+ </body>
15
+ </html>
@@ -0,0 +1,22 @@
1
+ # Page-view counter.
2
+ # Demonstrates: the `before` hook running on every request, SQLite
3
+ # for durable counters, and a cookie used to recognize a repeat
4
+ # visitor (as opposed to a brand new one) without requiring login.
5
+
6
+ port 3000
7
+
8
+ db = sqlite "db/visits.db"
9
+ db.exec "CREATE TABLE IF NOT EXISTS stats (name TEXT PRIMARY KEY, value INTEGER NOT NULL)"
10
+ db.exec "INSERT OR IGNORE INTO stats(name, value) VALUES ('total_views', 0)"
11
+ db.exec "INSERT OR IGNORE INTO stats(name, value) VALUES ('unique_visitors', 0)"
12
+
13
+ before do
14
+ next unless request.path == "/"
15
+ db.exec("UPDATE stats SET value = value + 1 WHERE name = 'total_views'")
16
+ if cookie[:visitor_id].nil?
17
+ cookie[:visitor_id] = SecureRandom.hex(8)
18
+ db.exec("UPDATE stats SET value = value + 1 WHERE name = 'unique_visitors'")
19
+ end
20
+ end
21
+
22
+ run
@@ -0,0 +1,4 @@
1
+ :root{color-scheme:light dark}
2
+ body{font-family:-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#fafafa;color:#1a1a1a;text-align:center}
3
+ .big{font-size:80px;font-weight:800;color:#3b6fe0;font-variant-numeric:tabular-nums}
4
+ .you{color:#888;font-size:14px;margin-top:24px}
@@ -0,0 +1 @@
1
+ <div class="wrap"><h1>404</h1></div>
@@ -0,0 +1 @@
1
+ <div class="wrap"><h1>500</h1></div>
@@ -0,0 +1,11 @@
1
+ <%
2
+ total = db.query("SELECT value FROM stats WHERE name = 'total_views'").first[:value]
3
+ unique = db.query("SELECT value FROM stats WHERE name = 'unique_visitors'").first[:value]
4
+ is_returning = !cookie[:visitor_id].nil?
5
+ %>
6
+ <div class="wrap">
7
+ <h1>This page has been viewed</h1>
8
+ <div class="big"><%= total %></div>
9
+ <p>time<%= total == 1 ? "" : "s" %>, by <strong><%= unique %></strong> unique visitor<%= unique == 1 ? "" : "s" %>.</p>
10
+ <p class="you"><%= is_returning ? "Welcome back -- you've been counted before." : "This is your first visit -- welcome!" %></p>
11
+ </div>
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Jimmu Visitor Counter</title>
7
+ <link rel="stylesheet" href="/css/style.css">
8
+ </head>
9
+ <body>
10
+ <%= yield %>
11
+ </body>
12
+ </html>
data/exe/jimmu ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: false
3
+ require_relative '../lib/jimmu'
4
+
5
+ Jimmu::CLI.start(ARGV)