fresco 0.0.1

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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/exe/fresco +3 -0
  3. data/lib/fresco/application.rb +12 -0
  4. data/lib/fresco/cli/build.rb +682 -0
  5. data/lib/fresco/cli/dev.rb +17 -0
  6. data/lib/fresco/cli/dev_loop.rb +815 -0
  7. data/lib/fresco/cli/new.rb +120 -0
  8. data/lib/fresco/cli/release.rb +76 -0
  9. data/lib/fresco/cli.rb +56 -0
  10. data/lib/fresco/database_config.rb +34 -0
  11. data/lib/fresco/generators/app/Gemfile.tt +18 -0
  12. data/lib/fresco/generators/app/README.md.tt +32 -0
  13. data/lib/fresco/generators/app/app/action.rb.tt +20 -0
  14. data/lib/fresco/generators/app/app/actions/root_path.rb.tt +5 -0
  15. data/lib/fresco/generators/app/app/views/layouts/application.html.erb +29 -0
  16. data/lib/fresco/generators/app/app/views/root_path.html.erb +8 -0
  17. data/lib/fresco/generators/app/app.rb.tt +15 -0
  18. data/lib/fresco/generators/app/bin/build +2 -0
  19. data/lib/fresco/generators/app/bin/dev +2 -0
  20. data/lib/fresco/generators/app/bin/release +2 -0
  21. data/lib/fresco/generators/app/config/app.rb.tt +26 -0
  22. data/lib/fresco/generators/app/config/database.rb +17 -0
  23. data/lib/fresco/generators/app/config/routes.rb +11 -0
  24. data/lib/fresco/generators/app/db/schema.rb +14 -0
  25. data/lib/fresco/generators/app/public/404.html +87 -0
  26. data/lib/fresco/generators/app/public/500.html +84 -0
  27. data/lib/fresco/migration_builder.rb +55 -0
  28. data/lib/fresco/model_builder.rb +54 -0
  29. data/lib/fresco/paths.rb +20 -0
  30. data/lib/fresco/router.rb +67 -0
  31. data/lib/fresco/runtime/boot.rb +34 -0
  32. data/lib/fresco/runtime/db_postgres.rb +403 -0
  33. data/lib/fresco/runtime/db_sqlite.rb +495 -0
  34. data/lib/fresco/runtime/http.c +456 -0
  35. data/lib/fresco/runtime/postgres.c +339 -0
  36. data/lib/fresco/runtime/runtime.rb +1810 -0
  37. data/lib/fresco/runtime/sqlite.c +220 -0
  38. data/lib/fresco/runtime/welcome.rb +152 -0
  39. data/lib/fresco/schema_builder.rb +71 -0
  40. data/lib/fresco/templates/dispatch.rb.erb +32 -0
  41. data/lib/fresco/templates/layout_dispatch.rb.erb +16 -0
  42. data/lib/fresco/templates/manifest.rb.erb +5 -0
  43. data/lib/fresco/templates/migrations.rb.erb +152 -0
  44. data/lib/fresco/templates/model.rb.erb +223 -0
  45. data/lib/fresco/templates/view.rb.erb +5 -0
  46. data/lib/fresco/version.rb +3 -0
  47. data/lib/fresco.rb +61 -0
  48. metadata +115 -0
@@ -0,0 +1,84 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>500 — Internal Server Error</title>
6
+ <link rel="preconnect" href="https://fonts.googleapis.com">
7
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
8
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;700&display=swap" rel="stylesheet">
9
+ <script>
10
+ (function () {
11
+ var t = localStorage.getItem("theme");
12
+ if (!t) {
13
+ t = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light";
14
+ localStorage.setItem("theme", t);
15
+ }
16
+ document.documentElement.setAttribute("data-theme", t);
17
+ })();
18
+ </script>
19
+ <style>
20
+ :root {
21
+ --bg: #fff; --fg: #222; --muted: #888;
22
+ --border: #eee; --code-bg: #f7f7f7;
23
+ --accent: #0066cc; --status: #b00020;
24
+ }
25
+ :root[data-theme="dark"] {
26
+ --bg: #141414; --fg: #e8e8e8; --muted: #9a9a9a;
27
+ --border: #2a2a2a; --code-bg: #1f1f1f;
28
+ --accent: #6ab0ff; --status: #ff6680;
29
+ }
30
+ body {
31
+ font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
32
+ max-width: 40rem;
33
+ margin: 4rem auto;
34
+ padding: 0 1.5rem;
35
+ background: var(--bg);
36
+ color: var(--fg);
37
+ transition: background 0.15s, color 0.15s;
38
+ }
39
+ h1 { font-weight: 700; font-size: 1.5rem; margin-bottom: 0.25rem; }
40
+ .status { color: var(--status); font-size: 0.9rem; letter-spacing: 0.05em; text-transform: uppercase; }
41
+ p { line-height: 1.5; }
42
+ code { background: var(--code-bg); padding: 0.1rem 0.3rem; border-radius: 3px; }
43
+ a { color: var(--accent); }
44
+ a:hover { text-decoration: underline; }
45
+ hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
46
+ footer { color: var(--muted); font-size: 0.85rem; }
47
+ #theme-toggle {
48
+ position: fixed; top: 1rem; right: 1rem;
49
+ background: transparent; border: 1px solid var(--border);
50
+ color: var(--fg); border-radius: 6px;
51
+ padding: 0.4rem; cursor: pointer;
52
+ display: inline-flex; align-items: center; justify-content: center;
53
+ }
54
+ #theme-toggle:hover { background: var(--code-bg); }
55
+ #theme-toggle svg { width: 18px; height: 18px; display: none; }
56
+ :root[data-theme="light"] #theme-toggle .icon-moon { display: inline-block; }
57
+ :root[data-theme="dark"] #theme-toggle .icon-sun { display: inline-block; }
58
+ </style>
59
+ </head>
60
+ <body>
61
+ <button id="theme-toggle" aria-label="Toggle color theme" title="Toggle color theme">
62
+ <svg class="icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
63
+ <svg class="icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
64
+ </button>
65
+
66
+ <div class="status">500 &middot; Internal Server Error</div>
67
+ <h1>Something went wrong on our end.</h1>
68
+ <p>An action raised an exception while handling the request. The error has been logged; the page you tried to load couldn't be rendered.</p>
69
+ <p><a href="/">Back to home</a></p>
70
+ <hr>
71
+ <footer>Powered by Spinel + Fresco.</footer>
72
+
73
+ <script>
74
+ (function () {
75
+ document.getElementById("theme-toggle").addEventListener("click", function () {
76
+ var cur = document.documentElement.getAttribute("data-theme");
77
+ var nxt = cur === "dark" ? "light" : "dark";
78
+ document.documentElement.setAttribute("data-theme", nxt);
79
+ localStorage.setItem("theme", nxt);
80
+ });
81
+ })();
82
+ </script>
83
+ </body>
84
+ </html>
@@ -0,0 +1,55 @@
1
+ # Build-time migration DSL. db/migrations/<NNNN>_<name>.rb files
2
+ # each call `Fresco.migration "<NNNN>_<name>" do ... end`. `fresco build`
3
+ # loads all migration files in sorted order and emits the captured
4
+ # SQL into generated/migrations.rb, which the runtime runs via
5
+ # `./app db:migrate` / `./app db:rollback`.
6
+ #
7
+ # Each migration captures two ordered Arrays of SQL strings (up and
8
+ # down). No DSL helpers for ALTER / ADD / RENAME yet — write raw SQL
9
+ # in the block. Adapter-specific syntax goes in the file the
10
+ # developer wrote; we don't translate. (M6+ could add adapter-aware
11
+ # helpers, but raw SQL covers the M5 footprint.)
12
+ module Fresco
13
+ @migrations = []
14
+
15
+ def self.migrations
16
+ @migrations
17
+ end
18
+
19
+ def self.migration(name, &blk)
20
+ builder = Fresco::MigrationBuilder.new
21
+ builder.instance_eval(&blk) if blk
22
+ @migrations << {
23
+ name: name,
24
+ up_sql: builder.up_sql,
25
+ down_sql: builder.down_sql,
26
+ }
27
+ end
28
+ end
29
+
30
+ class Fresco::MigrationBuilder
31
+ attr_reader :up_sql, :down_sql
32
+
33
+ def initialize
34
+ @up_sql = []
35
+ @down_sql = []
36
+ @current = nil
37
+ end
38
+
39
+ def up(&blk)
40
+ @current = @up_sql
41
+ instance_eval(&blk) if blk
42
+ @current = nil
43
+ end
44
+
45
+ def down(&blk)
46
+ @current = @down_sql
47
+ instance_eval(&blk) if blk
48
+ @current = nil
49
+ end
50
+
51
+ def sql(stmt)
52
+ abort "[build] `sql` called outside `up` / `down` block" if @current.nil?
53
+ @current << stmt
54
+ end
55
+ end
@@ -0,0 +1,54 @@
1
+ # Build-time model DSL. Captured by `app/models/*.rb` files; each
2
+ # calls `Fresco.model :User, table: :users do ... end`. `fresco build`
3
+ # iterates `Fresco.models` and emits one generated/models/*.rb
4
+ # per entry, with adapter-specific SQL baked in.
5
+ #
6
+ # Shape captured per model:
7
+ # { name: :User, table: :users, finders: [:email, :name] }
8
+ module Fresco
9
+ @models = []
10
+
11
+ def self.models
12
+ @models
13
+ end
14
+
15
+ def self.model(name, table:, &blk)
16
+ builder = Fresco::ModelBuilder.new
17
+ builder.instance_eval(&blk) if blk
18
+ @models << {
19
+ name: name,
20
+ table: table,
21
+ finders: builder.finders,
22
+ validators: builder.validators,
23
+ }
24
+ end
25
+ end
26
+
27
+ class Fresco::ModelBuilder
28
+ attr_reader :finders, :validators
29
+
30
+ def initialize
31
+ @finders = []
32
+ @validators = []
33
+ end
34
+
35
+ # Each call adds one indexed column that gets a `where_<col>`
36
+ # finder method on the generated model class. Spinel-shape
37
+ # constraint: each finder is its own typed method, single arity —
38
+ # we don't generate a dynamic `where(hash)` because hash dispatch
39
+ # widens to RbVal.
40
+ def finder(column_name)
41
+ @finders << column_name
42
+ end
43
+
44
+ # M4 validations DSL. Currently supports `validates :col, presence:
45
+ # true` — emitted as a top-of-method early-return guard inside
46
+ # insert/update. All validation is pure-Ruby (no DB roundtrip);
47
+ # rules expand to monomorphic `==` / `.length` checks so Spinel
48
+ # doesn't widen the param types.
49
+ def validates(column_name, **opts)
50
+ if opts[:presence]
51
+ @validators << { column: column_name, rule: :presence }
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,20 @@
1
+ # Resolves paths into the installed gem so the CLI never hardcodes
2
+ # anything cwd-relative. Codegen reads templates and verbatim runtime
3
+ # files from here; `fresco new` reads the app skeleton from here.
4
+ module Fresco
5
+ module Paths
6
+ GEM_ROOT = File.expand_path("../..", __dir__)
7
+
8
+ def self.template_root
9
+ File.join(GEM_ROOT, "lib/fresco/templates")
10
+ end
11
+
12
+ def self.runtime_root
13
+ File.join(GEM_ROOT, "lib/fresco/runtime")
14
+ end
15
+
16
+ def self.generator_root
17
+ File.join(GEM_ROOT, "lib/fresco/generators/app")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,67 @@
1
+ class Fresco::Router
2
+ # Rails-style CRUD expansion table. Verb / suffix / class suffix
3
+ # for each of the seven canonical resource actions.
4
+ RESOURCE_ROUTES = [
5
+ [:index, "GET", "", "Index"],
6
+ [:new, "GET", "/new", "New"],
7
+ [:create, "POST", "", "Create"],
8
+ [:show, "GET", "/:id", "Show"],
9
+ [:edit, "GET", "/:id/edit", "Edit"],
10
+ [:update, "PATCH", "/:id", "Update"],
11
+ [:destroy, "DELETE", "/:id", "Destroy"],
12
+ ].freeze
13
+
14
+ # Each entry: [verb, pattern, klass, constraints]. `fresco build` reads
15
+ # this and emits generated/dispatch.rb.
16
+ attr_reader :entries
17
+
18
+ def initialize
19
+ @entries = []
20
+ end
21
+
22
+ %i[get post put delete patch].each do |verb|
23
+ define_method(verb) do |pattern, to:, constraints: {}|
24
+ @entries << [verb.to_s.upcase, pattern, to, constraints]
25
+ end
26
+ end
27
+
28
+ # Sugar for `get "/", to: ...`. Mirrors Rails' `root to: Foo`.
29
+ def root(to:)
30
+ @entries << ["GET", "/", to, {}]
31
+ end
32
+
33
+ # Expand `resources :users` into the seven CRUD routes. `only:` /
34
+ # `except:` filter the set. Each generated route expects a matching
35
+ # action file under app/actions/ — missing files fail the build
36
+ # with a path-and-suggestion message rather than emitting a
37
+ # dispatch line that references a non-existent class.
38
+ # `resources :users` resolves to namespaced classes — `Users::Index`
39
+ # at `app/actions/users/index.rb`, not `UsersIndex` flat. Mixing
40
+ # `resources` with flat files isn't supported on purpose: if you
41
+ # opt into the resources DSL, you opt into the folder convention.
42
+ def resources(name, only: nil, except: nil)
43
+ base_path = "/#{name}"
44
+ base_class = camelize(name)
45
+
46
+ RESOURCE_ROUTES.each do |action, verb, suffix, class_suffix|
47
+ next if only && !only.include?(action)
48
+ next if except && except.include?(action)
49
+
50
+ class_name = "#{base_class}::#{class_suffix}"
51
+ file_path = "app/actions/#{Fresco.class_to_path(class_name)}.rb"
52
+ unless File.exist?(file_path)
53
+ raise "[build] resources :#{name} expects #{file_path} for :#{action} " \
54
+ "(add the file or use `only:`/`except:`)"
55
+ end
56
+
57
+ Fresco.const_set_path(class_name, Class.new)
58
+ @entries << [verb, base_path + suffix, Fresco.const_get_path(class_name), {}]
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def camelize(name)
65
+ name.to_s.split("_").map(&:capitalize).join
66
+ end
67
+ end
@@ -0,0 +1,34 @@
1
+ # Fresco: boot sequence. Auto-generated by `fresco build`; do not edit.
2
+ #
3
+ # Source of truth: lib/fresco/runtime/boot.rb in the fresco gem —
4
+ # `fresco build` copies this file verbatim into generated/boot.rb.
5
+ # app.rb requires it and then hands off to App::Base.run.
6
+ #
7
+ # Load order matters:
8
+ # runtime → Fresco::Action, Fresco::App, Request, Response, Sock
9
+ # views → render_<name> helpers (depend on Spinel::View)
10
+ # layout_dispatch → dispatch_layout (depends on render_layouts_<name>)
11
+ # db_adapter → Fresco::Db::Active + adapter FFI module (Sqlite or Pg);
12
+ # empty when no config/database.rb is present
13
+ # migrations → Fresco::DbMigrations runner with embedded SQL;
14
+ # consumed by `./app db:migrate` / `db:rollback`
15
+ # and depends on Fresco::Db.exec being defined
16
+ # config/app → App::Base < Fresco::App (depends on runtime)
17
+ # config/database → Fresco.database :sqlite/:postgres call; sets
18
+ # Fresco.app.database_config so actions and (later)
19
+ # generated code can read the configured DSN at
20
+ # runtime via ENV.fetch
21
+ # requires → models + app/action.rb + app/actions/*.rb (depend
22
+ # on Fresco::App, dispatch_layout, Fresco::Db, and
23
+ # any view helpers actions may call)
24
+ # dispatch → dispatch_request (depends on action classes being defined)
25
+
26
+ require_relative "runtime"
27
+ require_relative "views"
28
+ require_relative "layout_dispatch"
29
+ require_relative "db_adapter"
30
+ require_relative "migrations"
31
+ require_relative "../config/app"
32
+ require_relative "../config/database"
33
+ require_relative "requires"
34
+ require_relative "dispatch"