src/rally@init.erl

-module(rally@init).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/rally/init.gleam").
-export([files/1, init_project/1]).
-export_type([scaffold_file/0]).

-type scaffold_file() :: {scaffold_file, binary(), binary()}.

-file("src/rally/init.gleam", 89).
-spec join(binary(), binary()) -> binary().
join(Root, Path) ->
    case Root of
        <<"."/utf8>> ->
            Path;

        _ ->
            <<<<Root/binary, "/"/utf8>>/binary, Path/binary>>
    end.

-file("src/rally/init.gleam", 96).
-spec set_executable(binary()) -> {ok, nil} | {error, binary()}.
set_executable(Path) ->
    case rally_cli_ffi:find_executable(<<"chmod"/utf8>>) of
        {some, Chmod} ->
            case rally_cli_ffi:run_executable(Chmod, [<<"+x"/utf8>>, Path]) of
                0 ->
                    {ok, nil};

                _ ->
                    {error,
                        <<<<"Failed to mark "/utf8, Path/binary>>/binary,
                            " executable"/utf8>>}
            end;

        none ->
            {ok, nil}
    end.

-file("src/rally/init.gleam", 428).
-spec dev_script() -> binary().
dev_script() ->
    <<"#!/usr/bin/env bash
set -euo pipefail
cd \"$(dirname \"$0\")/..\"
if [ -f \".env\" ]; then
  set -a; . .env; set +a
fi
export APP_ENV=\"${APP_ENV:-dev}\"
echo \"==> Running rally codegen...\"
gleam run -m rally
echo \"==> Building client...\"
cd .generated_clients/public && gleam build --target javascript && cd ../..
echo \"==> Starting server...\"
gleam run -m app
"/utf8>>.

-file("src/rally/init.gleam", 418).
-spec server_context() -> binary().
server_context() ->
    <<"// Scaffolded by rally: yours to customize.
import sqlight

pub type ServerContext {
  ServerContext(db: sqlight.Connection)
}
"/utf8>>.

-file("src/rally/init.gleam", 401).
-spec shell_html() -> binary().
shell_html() ->
    <<"<!-- Scaffolded by rally: yours to customize. -->
<!DOCTYPE html>
<html>
<head>
  <meta charset=\"utf-8\">
  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
  <title>My App</title>
</head>
<body>
  <div id=\"app\"></div>
  <script type=\"module\" src=\"/client.js\"></script>
</body>
</html>
"/utf8>>.

-file("src/rally/init.gleam", 246).
-spec app_module() -> binary().
app_module() ->
    <<"// Scaffolded by rally: yours to customize.
import gleam/bytes_tree
import gleam/erlang/process
import gleam/http.{Get, Post}
import gleam/http/request.{type Request, Request}
import gleam/http/response
import mist.{type Connection, type ResponseData}
import generated/public/http_handler as http_handler
import generated/public/router as router
import generated/public/ssr_handler as ssr_handler
import generated/public/ws_handler as ws_handler
import rally_runtime/db
import rally_runtime/env
import rally_runtime/session
import rally_runtime/system
import server_context.{ServerContext}
import simplifile
import sqlight

pub fn main() {
  let db = start_db()
  system.start(\"system.db\")
  let server_context = ServerContext(db:)

  let handler = fn(req: Request(Connection)) {
    let Request(path: path, method: method, ..) = req
    case path {
      \"/ws\" -> {
        let session_id = get_session_id(req)
        let hostname = request_header(req, \"host\")
        mist.websocket(
          req,
          ws_handler.handler,
          fn(conn) {
            ws_handler.on_init(
              conn: conn,
              server_context: server_context,
              session_id: session_id,
              hostname: hostname,
            )
          },
          ws_handler.on_close,
        )
      }
      \"/rpc\" -> handle_rpc(req, server_context)
      \"/client.js\" -> serve_client_js()
      _ -> {
        case method {
          Get -> {
            let session_id = get_session_id(req)
            let route = router.parse_route(request.to_uri(req))
            let resp = ssr_handler.handle_request(route)
            set_session_cookie_if_missing(req, resp, session_id)
          }
          _ ->
            response.new(405)
            |> response.set_body(mist.Bytes(bytes_tree.from_string(\"Not found\")))
        }
      }
    }
  }

  let assert Ok(_) =
    mist.new(handler)
    |> mist.port(8080)
    |> mist.start
  process.sleep_forever()
}

fn handle_rpc(req: Request(Connection), server_context: ServerContext) {
  case req.method {
    Post -> {
      let session_id = get_session_id(req)
      case mist.read_body(req, max_body_limit: 16_000_000) {
        Ok(Request(body: body, ..)) -> {
          let resp =
            http_handler.handle(
              body: body,
              server_context: server_context,
              session_id: session_id,
            )
          set_session_cookie_if_missing(req, resp, session_id)
        }
        Error(_) ->
          response.new(413)
          |> response.set_body(
            mist.Bytes(bytes_tree.from_string(\"Request body too large\")),
          )
      }
    }
    _ ->
      response.new(405)
      |> response.set_body(mist.Bytes(bytes_tree.from_string(\"Not found\")))
  }
}

fn request_header(req: Request(Connection), name: String) -> String {
  case request.get_header(req, name) {
    Ok(value) -> value
    Error(_) -> \"\"
  }
}

fn get_session_id(req: Request(Connection)) -> String {
  case request.get_header(req, \"cookie\") {
    Ok(cookie) ->
      case session.extract_session_id(cookie) {
        Ok(id) -> id
        Error(_) -> session.generate_id()
      }
    Error(_) -> session.generate_id()
  }
}

fn set_session_cookie_if_missing(req, resp, session_id: String) {
  case request.get_header(req, \"cookie\") {
    Ok(cookie) ->
      case session.extract_session_id(cookie) {
        Ok(_) -> resp
        Error(_) ->
          response.set_header(
            resp,
            \"set-cookie\",
            session.set_cookie_header(session_id:, secure: env.secure_cookies()),
          )
      }
    Error(_) ->
      response.set_header(
        resp,
        \"set-cookie\",
        session.set_cookie_header(session_id:, secure: env.secure_cookies()),
      )
  }
}

fn serve_client_js() {
  case simplifile.read(\".generated_clients/public/build/dev/javascript/client/generated/app.mjs\") {
    Ok(js) ->
      response.new(200)
      |> response.set_header(\"content-type\", \"application/javascript\")
      |> response.set_body(mist.Bytes(bytes_tree.from_string(js)))
    Error(_) ->
      response.new(404)
      |> response.set_body(mist.Bytes(bytes_tree.from_string(\"Client JS not found\")))
  }
}

fn start_db() -> sqlight.Connection {
  let assert Ok(conn) = db.open(\"app.db\")
  conn
}
"/utf8>>.

-file("src/rally/init.gleam", 236).
-spec layout_page() -> binary().
layout_page() ->
    <<"// Scaffolded by rally: yours to customize.
import lustre/element.{type Element}

pub fn layout(content: Element(msg)) -> Element(msg) {
  content
}
"/utf8>>.

-file("src/rally/init.gleam", 169).
-spec home_page() -> binary().
home_page() ->
    <<"// Scaffolded by rally: yours to customize.
import gleam/string
import lustre/element.{type Element}
import lustre/element/html
import lustre/effect.{type Effect}
import lustre/event
import rally_runtime/effect as rally_effect
import server_context.{type ServerContext}

pub type Model {
  Model(count: Int)
}

pub type Msg {
  UserClickedIncrement
  UserClickedDecrement
  GotIncrement(Result(Int, Nil))
}

pub type ServerIncrement {
  ServerIncrement
}

pub type ServerDecrement {
  ServerDecrement
}

pub fn init() -> #(Model, Effect(Msg)) {
  #(Model(count: 0), effect.none())
}

pub fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
  case msg {
    UserClickedIncrement ->
      #(model, rally_effect.rpc(ServerIncrement, on_response: GotIncrement))
    UserClickedDecrement ->
      #(model, rally_effect.rpc(ServerDecrement, on_response: GotIncrement))
    GotIncrement(Ok(n)) -> #(Model(count: model.count + n), effect.none())
    GotIncrement(Error(_)) -> #(model, effect.none())
  }
}

pub fn view(model: Model) -> Element(Msg) {
  html.div([], [
    html.button([event.on_click(UserClickedIncrement)], [html.text(\"+\")]),
    html.text(string.inspect(model.count)),
    html.button([event.on_click(UserClickedDecrement)], [html.text(\"-\")]),
  ])
}

pub fn server_increment(
  msg _msg: ServerIncrement,
  server_context _server_context: ServerContext,
) -> Result(Int, Nil) {
  Ok(1)
}

pub fn server_decrement(
  msg _msg: ServerDecrement,
  server_context _server_context: ServerContext,
) -> Result(Int, Nil) {
  Ok(-1)
}
"/utf8>>.

-file("src/rally/init.gleam", 130).
-spec gleam_toml(binary()) -> binary().
gleam_toml(Project_name) ->
    <<<<"name = \""/utf8, Project_name/binary>>/binary,
        "\"
version = \"0.1.0\"
target = \"erlang\"

[dependencies]
gleam_erlang = \">= 1.0.0 and < 2.0.0\"
gleam_http = \">= 4.0.0 and < 5.0.0\"
gleam_stdlib = \">= 0.60.0 and < 2.0.0\"
rally = \">= 1.0.0 and < 2.0.0\"
libero = \">= 6.0.0 and < 7.0.0\"
lustre = \">= 5.6.0 and < 7.0.0\"
marmot = \">= 1.3.0 and < 2.0.0\"
mist = \">= 6.0.0 and < 7.0.0\"
sqlight = \">= 1.0.0 and < 2.0.0\"
simplifile = \">= 2.0.0 and < 3.0.0\"
gleam_time = \">= 1.7.0 and < 2.0.0\"

[dev-dependencies]
gleeunit = \">= 1.0.0 and < 2.0.0\"
birdie = \">= 2.0.0 and < 3.0.0\"
glinter = \">= 2.16.0 and < 3.0.0\"

[tools.glinter]
stats = true
warnings_as_errors = true
exclude = [\"src/generated/\"]

[[tools.rally.clients]]
namespace = \"public\"
route_root = \"/\"

[tools.marmot]
database = \"app.db\"
sql_dir = \"src/sql\"
output = \"src/generated/sql\"
"/utf8>>.

-file("src/rally/init.gleam", 124).
-spec env_example() -> binary().
env_example() ->
    <<"APP_ENV=dev
LOG_LEVEL=debug
"/utf8>>.

-file("src/rally/init.gleam", 114).
-spec gitignore() -> binary().
gitignore() ->
    <<"build/
app.db
erl_crash.dump
*.bak
.DS_Store
.generated_clients/
"/utf8>>.

-file("src/rally/init.gleam", 19).
-spec files(binary()) -> list(scaffold_file()).
files(Project_name) ->
    [{scaffold_file, <<".gitignore"/utf8>>, gitignore()},
        {scaffold_file, <<".env.example"/utf8>>, env_example()},
        {scaffold_file, <<"gleam.toml"/utf8>>, gleam_toml(Project_name)},
        {scaffold_file, <<"src/public/pages/home_.gleam"/utf8>>, home_page()},
        {scaffold_file, <<"src/public/pages/layout.gleam"/utf8>>, layout_page()},
        {scaffold_file, <<"src/app.gleam"/utf8>>, app_module()},
        {scaffold_file, <<"src/public/shell.html"/utf8>>, shell_html()},
        {scaffold_file, <<"src/server_context.gleam"/utf8>>, server_context()},
        {scaffold_file, <<"bin/dev"/utf8>>, dev_script()}].

-file("src/rally/init.gleam", 50).
-spec write_files(binary(), list(scaffold_file())) -> {ok, nil} |
    {error, binary()}.
write_files(Root, Files) ->
    _pipe = Files,
    gleam@list:try_each(
        _pipe,
        fun(File) ->
            Path = join(Root, erlang:element(2, File)),
            _pipe@1 = simplifile:write(Path, erlang:element(3, File)),
            gleam@result:map_error(
                _pipe@1,
                fun(E) ->
                    <<<<<<"Failed to write "/utf8, Path/binary>>/binary,
                            ": "/utf8>>/binary,
                        (simplifile:describe_error(E))/binary>>
                end
            )
        end
    ).

-file("src/rally/init.gleam", 33).
-spec create_dirs(binary()) -> {ok, nil} | {error, binary()}.
create_dirs(Root) ->
    _pipe = [<<"src/public/pages"/utf8>>,
        <<"src/sql"/utf8>>,
        <<"src/generated/public"/utf8>>,
        <<".generated_clients/public/src/generated"/utf8>>,
        <<"bin"/utf8>>],
    gleam@list:try_each(
        _pipe,
        fun(Dir) ->
            Path = join(Root, Dir),
            _pipe@1 = simplifile:create_directory_all(Path),
            gleam@result:map_error(
                _pipe@1,
                fun(E) ->
                    <<<<<<"Failed to create "/utf8, Path/binary>>/binary,
                            ": "/utf8>>/binary,
                        (simplifile:describe_error(E))/binary>>
                end
            )
        end
    ).

-file("src/rally/init.gleam", 81).
-spec basename(binary()) -> binary().
basename(Path) ->
    _pipe = Path,
    _pipe@1 = gleam@string:split(_pipe, <<"/"/utf8>>),
    _pipe@2 = lists:reverse(_pipe@1),
    _pipe@3 = gleam@list:first(_pipe@2),
    gleam@result:unwrap(_pipe@3, <<"rally_app"/utf8>>).

-file("src/rally/init.gleam", 74).
-spec trim_trailing_slash(binary()) -> binary().
trim_trailing_slash(Path) ->
    case gleam_stdlib:string_ends_with(Path, <<"/"/utf8>>) of
        true ->
            _pipe = gleam@string:drop_end(Path, 1),
            trim_trailing_slash(_pipe);

        false ->
            Path
    end.

-file("src/rally/init.gleam", 61).
-spec project_name(binary()) -> binary().
project_name(Root) ->
    Path = case Root of
        <<"."/utf8>> ->
            _pipe = simplifile:current_directory(),
            gleam@result:unwrap(_pipe, <<"rally_app"/utf8>>);

        Other ->
            Other
    end,
    _pipe@1 = Path,
    _pipe@2 = trim_trailing_slash(_pipe@1),
    _pipe@3 = basename(_pipe@2),
    _pipe@4 = gleam@string:replace(_pipe@3, <<"-"/utf8>>, <<"_"/utf8>>),
    string:lowercase(_pipe@4).

-file("src/rally/init.gleam", 11).
-spec init_project(binary()) -> {ok, nil} | {error, binary()}.
init_project(Root) ->
    Name = project_name(Root),
    gleam@result:'try'(
        create_dirs(Root),
        fun(_use0) ->
            nil = _use0,
            gleam@result:'try'(
                write_files(Root, files(Name)),
                fun(_use0@1) ->
                    nil = _use0@1,
                    set_executable(join(Root, <<"bin/dev"/utf8>>))
                end
            )
        end
    ).