Trading card city builder game?

remove gleam, update ci

eldridge.cam 0fffb057 632919cc

verified
+292 -2865
-81
.github/workflows/gleam.yml
··· 1 - name: Gleam CI 2 - on: push 3 - 4 - env: 5 - DATABASE_URL: postgresql://postgres:postgres@localhost/cartography 6 - 7 - jobs: 8 - test: 9 - runs-on: ubuntu-latest 10 - steps: 11 - - uses: actions/checkout@v4 12 - - name: Set up Gleam 13 - uses: erlef/setup-beam@v1 14 - with: 15 - gleam-version: "1.14.0" 16 - otp-version: "27" 17 - rebar3-version: "3.25.1" 18 - - name: Restore dependencies cache 19 - uses: actions/cache@v4 20 - with: 21 - path: deps 22 - key: ${{ runner.os }}-gleam-${{ hashFiles('**/manifest.toml') }} 23 - restore-keys: ${{ runner.os }}-gleam- 24 - - name: Install dependencies 25 - run: gleam deps download 26 - working-directory: ./server 27 - - name: Test server 28 - run: gleam test 29 - working-directory: ./server 30 - - name: Test API 31 - run: gleam test 32 - working-directory: ./api 33 - 34 - format: 35 - runs-on: ubuntu-latest 36 - steps: 37 - - uses: actions/checkout@v4 38 - - name: Set up Gleam 39 - uses: erlef/setup-beam@v1 40 - with: 41 - gleam-version: "1.14.0" 42 - otp-version: "27" 43 - rebar3-version: "3.25.1" 44 - - name: Restore dependencies cache 45 - uses: actions/cache@v4 46 - with: 47 - path: deps 48 - key: ${{ runner.os }}-gleam-${{ hashFiles('**/manifest.toml') }} 49 - restore-keys: ${{ runner.os }}-gleam- 50 - - name: Format server 51 - run: gleam format --check 52 - working-directory: ./server 53 - - name: Format API 54 - run: gleam format --check 55 - working-directory: ./api 56 - 57 - check: 58 - runs-on: ubuntu-latest 59 - steps: 60 - - uses: actions/checkout@v4 61 - - name: Set up Gleam 62 - uses: erlef/setup-beam@v1 63 - with: 64 - gleam-version: "1.14.0" 65 - otp-version: "27" 66 - rebar3-version: "3.25.1" 67 - - name: Restore dependencies cache 68 - uses: actions/cache@v4 69 - with: 70 - path: deps 71 - key: ${{ runner.os }}-gleam-${{ hashFiles('**/manifest.toml') }} 72 - restore-keys: ${{ runner.os }}-gleam- 73 - - name: Install dependencies 74 - run: gleam deps download 75 - working-directory: ./server 76 - - name: Check server 77 - run: gleam check 78 - working-directory: ./server 79 - - name: Check API 80 - run: gleam check 81 - working-directory: ./api
+97
.github/workflows/rust.yml
··· 1 + name: Rust CI 2 + 3 + on: push 4 + 5 + jobs: 6 + clippy: 7 + name: clippy 8 + runs-on: ubuntu-latest 9 + steps: 10 + - name: Checkout the source code 11 + uses: actions/checkout@v4 12 + - uses: actions/cache@v4 13 + with: 14 + path: | 15 + ~/.cargo/bin/ 16 + ~/.cargo/registry/index/ 17 + ~/.cargo/registry/cache/ 18 + ~/.cargo/git/db/ 19 + target/ 20 + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 21 + - name: Install Rust 22 + run: | 23 + rustup install stable 24 + rustup component add clippy 25 + rustup default stable 26 + - name: Run cargo clippy 27 + run: | 28 + cargo clippy -- -D warnings 29 + cargo sqlx prepare --check 30 + rustfmt: 31 + name: rustfmt 32 + runs-on: ubuntu-latest 33 + steps: 34 + - name: Checkout the source code 35 + uses: actions/checkout@v4 36 + - uses: actions/cache@v4 37 + with: 38 + path: | 39 + ~/.cargo/bin/ 40 + ~/.cargo/registry/index/ 41 + ~/.cargo/registry/cache/ 42 + ~/.cargo/git/db/ 43 + target/ 44 + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 45 + - name: Install Rust 46 + run: | 47 + rustup install stable 48 + rustup component add rustfmt 49 + rustup default stable 50 + cargo install sqruff --locked 51 + cargo install --git https://github.com/jflessau/sqlx-fmt --locked 52 + - name: Run cargo fmt 53 + run: | 54 + cargo fmt -- --check 55 + sqlx-fmt check 56 + build: 57 + name: build 58 + runs-on: ubuntu-latest 59 + steps: 60 + - name: Checkout the source code 61 + uses: actions/checkout@v4 62 + - uses: actions/cache@v4 63 + with: 64 + path: | 65 + ~/.cargo/bin/ 66 + ~/.cargo/registry/index/ 67 + ~/.cargo/registry/cache/ 68 + ~/.cargo/git/db/ 69 + target/ 70 + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 71 + - name: Install Rust 72 + run: | 73 + rustup install stable 74 + rustup default stable 75 + - name: Run cargo check 76 + run: cargo check 77 + test: 78 + name: test 79 + runs-on: ubuntu-latest 80 + steps: 81 + - name: Checkout the source code 82 + uses: actions/checkout@v4 83 + - uses: actions/cache@v4 84 + with: 85 + path: | 86 + ~/.cargo/bin/ 87 + ~/.cargo/registry/index/ 88 + ~/.cargo/registry/cache/ 89 + ~/.cargo/git/db/ 90 + target/ 91 + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 92 + - name: Install Rust 93 + run: | 94 + rustup install stable 95 + rustup default stable 96 + - name: Run cargo test 97 + run: cargo test
-9
.github/workflows/sql.yml
··· 31 31 cache: npm 32 32 node-version-file: app/.node-version 33 33 cache-dependency-path: app/package-lock.json 34 - - name: Set up Gleam 35 - uses: erlef/setup-beam@v1 36 - with: 37 - gleam-version: "1.14.0" 38 - otp-version: "27" 39 - rebar3-version: "3.25.1" 40 34 - name: Check status 41 35 run: npx graphile-migrate status --skipDatabase 42 36 - name: Run migrate 43 37 run: npx graphile-migrate migrate 44 - - name: Check squirrel 45 - run: gleam run -m squirrel check 46 - working-directory: ./server
+32
.sqlx/query-1a81794620597eea4bdbf37414406179aa1a25f11ae747b5edf71c0c0afdc9b1.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT id FROM accounts WHERE id = $1", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "id", 9 + "type_info": { 10 + "Custom": { 11 + "name": "citext", 12 + "kind": "Simple" 13 + } 14 + } 15 + } 16 + ], 17 + "parameters": { 18 + "Left": [ 19 + { 20 + "Custom": { 21 + "name": "citext", 22 + "kind": "Simple" 23 + } 24 + } 25 + ] 26 + }, 27 + "nullable": [ 28 + false 29 + ] 30 + }, 31 + "hash": "1a81794620597eea4bdbf37414406179aa1a25f11ae747b5edf71c0c0afdc9b1" 32 + }
+58
.sqlx/query-30cd5f21a915094c10d64a49666a4da02a6639d9647116b529ed8acc1991c29e.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "\n SELECT\n card_types.id,\n card_types.card_set_id,\n tile_types.category AS \"category: _\",\n tile_types.houses,\n tile_types.employs\n FROM card_types\n INNER JOIN tile_types ON tile_types.id = card_types.id\n ", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "id", 9 + "type_info": "Text" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "card_set_id", 14 + "type_info": "Text" 15 + }, 16 + { 17 + "ordinal": 2, 18 + "name": "category: _", 19 + "type_info": { 20 + "Custom": { 21 + "name": "tile_category", 22 + "kind": { 23 + "Enum": [ 24 + "residential", 25 + "production", 26 + "amenity", 27 + "source", 28 + "trade", 29 + "transportation" 30 + ] 31 + } 32 + } 33 + } 34 + }, 35 + { 36 + "ordinal": 3, 37 + "name": "houses", 38 + "type_info": "Int4" 39 + }, 40 + { 41 + "ordinal": 4, 42 + "name": "employs", 43 + "type_info": "Int4" 44 + } 45 + ], 46 + "parameters": { 47 + "Left": [] 48 + }, 49 + "nullable": [ 50 + false, 51 + false, 52 + false, 53 + false, 54 + false 55 + ] 56 + }, 57 + "hash": "30cd5f21a915094c10d64a49666a4da02a6639d9647116b529ed8acc1991c29e" 58 + }
+26
.sqlx/query-57caef20de8bc585562b6efcb5ad75b9e269dc2cee14e391874f0dad18362e02.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "\n SELECT card_types.id, card_types.card_set_id\n FROM card_types\n INNER JOIN species ON species.id = card_types.id\n ", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "id", 9 + "type_info": "Text" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "card_set_id", 14 + "type_info": "Text" 15 + } 16 + ], 17 + "parameters": { 18 + "Left": [] 19 + }, 20 + "nullable": [ 21 + false, 22 + false 23 + ] 24 + }, 25 + "hash": "57caef20de8bc585562b6efcb5ad75b9e269dc2cee14e391874f0dad18362e02" 26 + }
+32
.sqlx/query-734d17f0277afa8566a68b6670f731f422df6e0baa7807caec3e3c42dba4e8cb.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "INSERT INTO accounts (id) VALUES ($1) RETURNING id", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "id", 9 + "type_info": { 10 + "Custom": { 11 + "name": "citext", 12 + "kind": "Simple" 13 + } 14 + } 15 + } 16 + ], 17 + "parameters": { 18 + "Left": [ 19 + { 20 + "Custom": { 21 + "name": "citext", 22 + "kind": "Simple" 23 + } 24 + } 25 + ] 26 + }, 27 + "nullable": [ 28 + false 29 + ] 30 + }, 31 + "hash": "734d17f0277afa8566a68b6670f731f422df6e0baa7807caec3e3c42dba4e8cb" 32 + }
+33
.sqlx/query-f884df8ea6b4dfaafc2cef8a3febecdd14e8ba5c177d24c21ab4b3fd1be7af17.json
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT id, name FROM fields WHERE account_id = $1", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "id", 9 + "type_info": "Int8" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "name", 14 + "type_info": "Text" 15 + } 16 + ], 17 + "parameters": { 18 + "Left": [ 19 + { 20 + "Custom": { 21 + "name": "citext", 22 + "kind": "Simple" 23 + } 24 + } 25 + ] 26 + }, 27 + "nullable": [ 28 + false, 29 + false 30 + ] 31 + }, 32 + "hash": "f884df8ea6b4dfaafc2cef8a3febecdd14e8ba5c177d24c21ab4b3fd1be7af17" 33 + }
-42
Cargo.lock
··· 155 155 "tokio", 156 156 "tokio-stream", 157 157 "tracing", 158 - "ts-rs", 159 158 "utoipa", 160 159 "uuid", 161 160 ] ··· 1646 1645 ] 1647 1646 1648 1647 [[package]] 1649 - name = "termcolor" 1650 - version = "1.4.1" 1651 - source = "registry+https://github.com/rust-lang/crates.io-index" 1652 - checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 1653 - dependencies = [ 1654 - "winapi-util", 1655 - ] 1656 - 1657 - [[package]] 1658 1648 name = "thiserror" 1659 1649 version = "2.0.18" 1660 1650 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1821 1811 checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" 1822 1812 dependencies = [ 1823 1813 "once_cell", 1824 - ] 1825 - 1826 - [[package]] 1827 - name = "ts-rs" 1828 - version = "12.0.1" 1829 - source = "registry+https://github.com/rust-lang/crates.io-index" 1830 - checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8" 1831 - dependencies = [ 1832 - "thiserror", 1833 - "ts-rs-macros", 1834 - "uuid", 1835 - ] 1836 - 1837 - [[package]] 1838 - name = "ts-rs-macros" 1839 - version = "12.0.1" 1840 - source = "registry+https://github.com/rust-lang/crates.io-index" 1841 - checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa" 1842 - dependencies = [ 1843 - "proc-macro2", 1844 - "quote", 1845 - "syn", 1846 - "termcolor", 1847 1814 ] 1848 1815 1849 1816 [[package]] ··· 2053 2020 dependencies = [ 2054 2021 "libredox", 2055 2022 "wasite", 2056 - ] 2057 - 2058 - [[package]] 2059 - name = "winapi-util" 2060 - version = "0.1.11" 2061 - source = "registry+https://github.com/rust-lang/crates.io-index" 2062 - checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" 2063 - dependencies = [ 2064 - "windows-sys 0.61.2", 2065 2023 ] 2066 2024 2067 2025 [[package]]
-1
Cargo.toml
··· 20 20 tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread"] } 21 21 tokio-stream = { version = "0.1.18", features = ["sync"] } 22 22 tracing = "0.1.44" 23 - ts-rs = { version = "12.0.1", features = ["uuid-impl"] } 24 23 utoipa = "5.4.0" 25 24 uuid = { version = "1.20.0", features = ["serde", "v7"] }
+3 -5
Justfile
··· 7 7 [group: "dev"] 8 8 fix: fmt 9 9 10 - export TS_RS_EXPORT_DIR := "app/src/lib/appserver/types" 11 - 12 10 export CONCURRENTLY_KILL_OTHERS := "true" 13 11 export CONCURRENTLY_PAD_PREFIX := "true" 14 12 export CONCURRENTLY_PREFIX_COLORS := "auto" ··· 66 64 67 65 [group: "dev"] 68 66 generate: 69 - cargo test export_bindings 67 + cargo sqlx prepare 70 68 cd app && npx svelte-kit sync 71 - cd app && npx prettier --write ../{{TS_RS_EXPORT_DIR}} 72 69 73 70 [group: "dev"] 74 - fmt: 71 + fmt: && generate 75 72 sqlx-fmt format 76 73 cargo fmt 77 74 cd app && npx prettier --write . ··· 79 76 [group: "dev"] 80 77 test: 81 78 cargo test 79 + cd app && npm test 82 80 83 81 [group: "database"] 84 82 migrate:
-23
api/.github/workflows/test.yml
··· 1 - name: test 2 - 3 - on: 4 - push: 5 - branches: 6 - - master 7 - - main 8 - pull_request: 9 - 10 - jobs: 11 - test: 12 - runs-on: ubuntu-latest 13 - steps: 14 - - uses: actions/checkout@v4 15 - - uses: erlef/setup-beam@v1 16 - with: 17 - otp-version: "28" 18 - gleam-version: "1.13.0" 19 - rebar3-version: "3" 20 - # elixir-version: "1" 21 - - run: gleam deps download 22 - - run: gleam test 23 - - run: gleam format --check src test
-4
api/.gitignore
··· 1 - *.beam 2 - *.ez 3 - /build 4 - erl_crash.dump
-25
api/README.md
··· 1 - # cartography_api 2 - 3 - [![Package Version](https://img.shields.io/hexpm/v/cartography_api)](https://hex.pm/packages/cartography_api) 4 - [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/cartography_api/) 5 - 6 - ```sh 7 - gleam add cartography_api@1 8 - ``` 9 - 10 - ```gleam 11 - import cartography_api 12 - 13 - pub fn main() -> Nil { 14 - // TODO: An example of the project in use 15 - } 16 - ``` 17 - 18 - Further documentation can be found at <https://hexdocs.pm/cartography_api>. 19 - 20 - ## Development 21 - 22 - ```sh 23 - gleam run # Run the project 24 - gleam test # Run the tests 25 - ```
-22
api/gleam.toml
··· 1 - name = "cartography_api" 2 - version = "1.0.0" 3 - 4 - # Fill out these fields if you intend to generate HTML documentation or publish 5 - # your project to the Hex package manager. 6 - # 7 - # description = "" 8 - # licences = ["Apache-2.0"] 9 - # repository = { type = "github", user = "", repo = "" } 10 - # links = [{ title = "Website", href = "" }] 11 - # 12 - # For a full reference of all the available options, you can have a look at 13 - # https://gleam.run/writing-gleam/gleam-toml/. 14 - 15 - [dependencies] 16 - gleam_stdlib = ">= 0.44.0 and < 2.0.0" 17 - gleam_json = ">= 3.1.0 and < 4.0.0" 18 - squirtle = ">= 1.4.0 and < 2.0.0" 19 - youid = ">= 1.5.1 and < 2.0.0" 20 - 21 - [dev-dependencies] 22 - gleeunit = ">= 1.0.0 and < 2.0.0"
-19
api/manifest.toml
··· 1 - # This file was generated by Gleam 2 - # You typically do not need to edit this file 3 - 4 - packages = [ 5 - { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, 6 - { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, 7 - { name = "gleam_stdlib", version = "0.68.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F7FAEBD8EF260664E86A46C8DBA23508D1D11BB3BCC6EE1B89B3BC3E5C83FF1E" }, 8 - { name = "gleam_time", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "56DB0EF9433826D3B99DB0B4AF7A2BFED13D09755EC64B1DAAB46F804A9AD47D" }, 9 - { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, 10 - { name = "squirtle", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "squirtle", source = "hex", outer_checksum = "B4DCF7ED3E829E1DB6163F3E18677EF0862DF3F4CE076D98628EBB8D9BC974BC" }, 11 - { name = "youid", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_stdlib", "gleam_time"], otp_app = "youid", source = "hex", outer_checksum = "580E909FD704DB16416D5CB080618EDC2DA0F1BE4D21B490C0683335E3FFC5AF" }, 12 - ] 13 - 14 - [requirements] 15 - gleam_json = { version = ">= 3.1.0 and < 4.0.0" } 16 - gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 17 - gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 18 - squirtle = { version = ">= 1.4.0 and < 2.0.0" } 19 - youid = { version = ">= 1.5.1 and < 2.0.0" }
-25
api/src/cartography_api/account.gleam
··· 1 - import gleam/dynamic/decode 2 - import gleam/json.{type Json} 3 - 4 - pub type Account { 5 - Account(id: String) 6 - } 7 - 8 - pub fn to_json(account: Account) -> Json { 9 - json.object([#("id", json.string(account.id))]) 10 - } 11 - 12 - pub fn to_string(account: Account) -> String { 13 - account 14 - |> to_json() 15 - |> json.to_string() 16 - } 17 - 18 - pub fn decoder() -> decode.Decoder(Account) { 19 - use id <- decode.field("id", decode.string) 20 - decode.success(Account(id:)) 21 - } 22 - 23 - pub fn from_string(string: String) -> Result(Account, json.DecodeError) { 24 - json.parse(string, decoder()) 25 - }
-30
api/src/cartography_api/field.gleam
··· 1 - import cartography_api/game_state 2 - import gleam/dynamic/decode 3 - import gleam/json.{type Json} 4 - 5 - pub type Field { 6 - Field(id: game_state.FieldId, name: String) 7 - } 8 - 9 - pub fn to_json(field: Field) -> Json { 10 - json.object([ 11 - #("id", json.int(field.id.id)), 12 - #("name", json.string(field.name)), 13 - ]) 14 - } 15 - 16 - pub fn to_string(field: Field) -> String { 17 - field 18 - |> to_json() 19 - |> json.to_string() 20 - } 21 - 22 - pub fn decoder() -> decode.Decoder(Field) { 23 - use id <- decode.field("id", decode.map(decode.int, game_state.FieldId)) 24 - use name <- decode.field("name", decode.string) 25 - decode.success(Field(id:, name:)) 26 - } 27 - 28 - pub fn from_string(string: String) -> Result(Field, json.DecodeError) { 29 - json.parse(string, decoder()) 30 - }
-227
api/src/cartography_api/game_state.gleam
··· 1 - import gleam/dynamic/decode 2 - import gleam/json.{type Json} 3 - import gleam/list 4 - import gleam/option.{type Option} 5 - import squirtle 6 - 7 - /// Defines a shared game state data model, which the server manages on behalf of a client 8 - /// as a definitive source of truth. 9 - pub type GameState { 10 - GameState(deck: Deck, field: Field) 11 - } 12 - 13 - pub type CardId { 14 - TileId(id: Int) 15 - CitizenId(id: Int) 16 - } 17 - 18 - pub type CardTypeId { 19 - CardTypeId(id: String) 20 - TileTypeId(id: String) 21 - SpeciesId(id: String) 22 - } 23 - 24 - pub type FieldId { 25 - FieldId(id: Int) 26 - } 27 - 28 - pub type Deck { 29 - Deck(tiles: List(Tile), citizens: List(Citizen)) 30 - } 31 - 32 - pub type Tile { 33 - Tile(id: CardId, tile_type_id: CardTypeId, name: String) 34 - } 35 - 36 - pub type Citizen { 37 - Citizen( 38 - id: CardId, 39 - species_id: CardTypeId, 40 - name: String, 41 - home_tile_id: Option(CardId), 42 - ) 43 - } 44 - 45 - pub type Field { 46 - Field(name: String, tiles: List(FieldTile), citizens: List(FieldCitizen)) 47 - } 48 - 49 - pub type FieldTile { 50 - FieldTile(id: CardId, x: Int, y: Int) 51 - } 52 - 53 - pub type FieldCitizen { 54 - FieldCitizen(id: CardId, x: Int, y: Int) 55 - } 56 - 57 - pub fn to_json(game_state: GameState) -> Json { 58 - json.object([ 59 - #( 60 - "deck", 61 - json.object([ 62 - #( 63 - "tiles", 64 - game_state.deck.tiles 65 - |> list.map(fn(card) { 66 - case card { 67 - Tile(TileId(id), TileTypeId(tile_type_id), name) -> 68 - json.object([ 69 - #("id", json.int(id)), 70 - #("tile_type_id", json.string(tile_type_id)), 71 - #("name", json.string(name)), 72 - ]) 73 - _ -> panic as "unreachable" 74 - } 75 - }) 76 - |> json.preprocessed_array(), 77 - ), 78 - #( 79 - "citizens", 80 - game_state.deck.citizens 81 - |> list.map(fn(card) { 82 - case card { 83 - Citizen( 84 - CitizenId(id), 85 - SpeciesId(species_id), 86 - name, 87 - home_tile_id, 88 - ) -> 89 - json.object([ 90 - #("id", json.int(id)), 91 - #("species_id", json.string(species_id)), 92 - #("name", json.string(name)), 93 - #( 94 - "home_tile_id", 95 - json.nullable(home_tile_id, fn(id) { json.int(id.id) }), 96 - ), 97 - ]) 98 - _ -> panic as "unreachable" 99 - } 100 - }) 101 - |> json.preprocessed_array(), 102 - ), 103 - ]), 104 - ), 105 - #( 106 - "field", 107 - json.object([ 108 - #("name", json.string(game_state.field.name)), 109 - #( 110 - "tiles", 111 - game_state.field.tiles 112 - |> list.map(fn(tile) { 113 - json.object([ 114 - #("id", json.int(tile.id.id)), 115 - #("x", json.int(tile.x)), 116 - #("y", json.int(tile.y)), 117 - ]) 118 - }) 119 - |> json.preprocessed_array(), 120 - ), 121 - #( 122 - "citizens", 123 - game_state.field.citizens 124 - |> list.map(fn(citizen) { 125 - json.object([ 126 - #("id", json.int(citizen.id.id)), 127 - #("x", json.int(citizen.x)), 128 - #("y", json.int(citizen.y)), 129 - ]) 130 - }) 131 - |> json.preprocessed_array(), 132 - ), 133 - ]), 134 - ), 135 - ]) 136 - } 137 - 138 - pub fn to_string(game_state: GameState) -> String { 139 - game_state 140 - |> to_json() 141 - |> json.to_string() 142 - } 143 - 144 - pub fn decoder() -> decode.Decoder(GameState) { 145 - use deck <- decode.field("deck", { 146 - use tiles <- decode.field( 147 - "tiles", 148 - decode.list({ 149 - use id <- decode.field("id", decode.map(decode.int, TileId)) 150 - use tile_type_id <- decode.field( 151 - "tile_type_id", 152 - decode.map(decode.string, TileTypeId), 153 - ) 154 - use name <- decode.field("name", decode.string) 155 - decode.success(Tile(id:, tile_type_id:, name:)) 156 - }), 157 - ) 158 - use citizens <- decode.field( 159 - "citizens", 160 - decode.list({ 161 - use id <- decode.field("id", decode.map(decode.int, CitizenId)) 162 - use species_id <- decode.field( 163 - "species_id", 164 - decode.map(decode.string, SpeciesId), 165 - ) 166 - use name <- decode.field("name", decode.string) 167 - use home_tile_id <- decode.field( 168 - "home_tile_id", 169 - decode.optional(decode.map(decode.int, TileId)), 170 - ) 171 - decode.success(Citizen(id:, species_id:, name:, home_tile_id:)) 172 - }), 173 - ) 174 - decode.success(Deck(tiles:, citizens:)) 175 - }) 176 - use field <- decode.field("field", { 177 - use name <- decode.field("name", decode.string) 178 - use tiles <- decode.field( 179 - "tiles", 180 - decode.list({ 181 - use id <- decode.field("id", decode.map(decode.int, TileId)) 182 - use x <- decode.field("x", decode.int) 183 - use y <- decode.field("y", decode.int) 184 - decode.success(FieldTile(id:, x:, y:)) 185 - }), 186 - ) 187 - use citizens <- decode.field( 188 - "citizens", 189 - decode.list({ 190 - use id <- decode.field("id", decode.map(decode.int, CitizenId)) 191 - use x <- decode.field("x", decode.int) 192 - use y <- decode.field("y", decode.int) 193 - decode.success(FieldCitizen(id:, x:, y:)) 194 - }), 195 - ) 196 - decode.success(Field(name:, tiles:, citizens:)) 197 - }) 198 - decode.success(GameState(deck:, field:)) 199 - } 200 - 201 - pub fn diff(previous: GameState, next: GameState) -> List(squirtle.Patch) { 202 - let assert Ok(previous) = 203 - previous 204 - |> to_string() 205 - |> squirtle.json_value_parse() 206 - let assert Ok(next) = 207 - next 208 - |> to_string() 209 - |> squirtle.json_value_parse() 210 - squirtle.diff(previous, next) 211 - } 212 - 213 - pub fn patch( 214 - previous: GameState, 215 - patches: List(squirtle.Patch), 216 - ) -> Result(GameState, List(decode.DecodeError)) { 217 - let assert Ok(previous) = 218 - previous 219 - |> to_string() 220 - |> squirtle.json_value_parse() 221 - let assert Ok(next) = 222 - // NOTE: this one might fail if the patches are bad, should handle that better, 223 - // but realistically if there are bad patches, it means we have lost the server. 224 - previous 225 - |> squirtle.patch(patches) 226 - squirtle.json_value_decode(next, decoder()) 227 - }
-39
api/src/cartography_api/internal/repr.gleam
··· 1 - import gleam/dynamic/decode.{type Decoder} 2 - import gleam/json.{type Json} 3 - import youid/uuid.{type Uuid} 4 - 5 - pub fn struct(payload: Json, tag: String) -> Json { 6 - json.object([ 7 - #("#type", json.string("struct")), 8 - #("#tag", json.string(tag)), 9 - #("#payload", payload), 10 - ]) 11 - } 12 - 13 - pub fn struct_tag(fallback: t, cb: fn(String) -> Decoder(t)) -> Decoder(t) { 14 - use ty <- decode.field("#type", decode.string) 15 - case ty { 16 - "struct" -> { 17 - use tag <- decode.field("#tag", decode.string) 18 - cb(tag) 19 - } 20 - _ -> { 21 - decode.failure(fallback, "struct") 22 - } 23 - } 24 - } 25 - 26 - pub fn struct_payload( 27 - decoder: Decoder(f), 28 - cb: fn(f) -> Decoder(t), 29 - ) -> Decoder(t) { 30 - decode.field("#payload", decoder, cb) 31 - } 32 - 33 - pub fn uuid() -> Decoder(Uuid) { 34 - use str <- decode.then(decode.string) 35 - case uuid.from_string(str) { 36 - Ok(uuid) -> decode.success(uuid) 37 - Error(Nil) -> decode.failure(uuid.nil, "uuid") 38 - } 39 - }
-82
api/src/cartography_api/request.gleam
··· 1 - import cartography_api/game_state.{type CardTypeId, type FieldId} 2 - import cartography_api/internal/repr 3 - import gleam/dynamic/decode 4 - import gleam/json 5 - import youid/uuid.{type Uuid} 6 - 7 - pub type Message { 8 - Message(id: Uuid, request: Request) 9 - } 10 - 11 - pub fn message(request: Request, id: String) -> Message { 12 - let assert Ok(id) = uuid.from_string(id) 13 - Message(id:, request:) 14 - } 15 - 16 - /// A request is sent from the client to the server. 17 - pub type Request { 18 - Authenticate(auth_token: String) 19 - ListFields 20 - WatchField(field_id: FieldId) 21 - Unsubscribe 22 - DebugAddCard(card_id: CardTypeId) 23 - } 24 - 25 - pub fn to_json(message: Message) -> json.Json { 26 - let Message(id, request) = message 27 - let request = case request { 28 - Authenticate(auth_token) -> 29 - json.string(auth_token) 30 - |> repr.struct("Authenticate") 31 - ListFields -> repr.struct(json.null(), "ListFields") 32 - WatchField(field_id) -> 33 - json.int(field_id.id) 34 - |> repr.struct("WatchField") 35 - Unsubscribe -> repr.struct(json.null(), "Unsubscribe") 36 - DebugAddCard(card_id) -> 37 - json.string(card_id.id) 38 - |> repr.struct("DebugAddCard") 39 - } 40 - json.object([#("id", json.string(uuid.to_string(id))), #("request", request)]) 41 - } 42 - 43 - pub fn to_string(message: Message) -> String { 44 - message 45 - |> to_json() 46 - |> json.to_string() 47 - } 48 - 49 - pub fn decoder() { 50 - use id <- decode.field("id", repr.uuid()) 51 - use request <- decode.field("request", { 52 - use tag <- repr.struct_tag(Unsubscribe) 53 - case tag { 54 - "Authenticate" -> { 55 - use payload <- repr.struct_payload(decode.string) 56 - decode.success(Authenticate(payload)) 57 - } 58 - "ListFields" -> { 59 - decode.success(ListFields) 60 - } 61 - "WatchField" -> { 62 - use payload <- repr.struct_payload(decode.int) 63 - decode.success(WatchField(game_state.FieldId(payload))) 64 - } 65 - "Unsubscribe" -> { 66 - decode.success(Unsubscribe) 67 - } 68 - "DebugAddCard" -> { 69 - use payload <- repr.struct_payload(decode.string) 70 - decode.success(DebugAddCard(game_state.CardTypeId(payload))) 71 - } 72 - _ -> { 73 - decode.failure(Unsubscribe, "valid #tag") 74 - } 75 - } 76 - }) 77 - decode.success(Message(id:, request:)) 78 - } 79 - 80 - pub fn from_string(string: String) -> Result(Message, json.DecodeError) { 81 - json.parse(string, decoder()) 82 - }
-96
api/src/cartography_api/response.gleam
··· 1 - import cartography_api/account.{type Account} 2 - import cartography_api/field.{type Field} 3 - import cartography_api/game_state.{type GameState} 4 - import cartography_api/internal/repr 5 - import gleam/dynamic/decode 6 - import gleam/json 7 - import gleam/list 8 - import squirtle.{type Patch} 9 - import youid/uuid.{type Uuid} 10 - 11 - pub type Message { 12 - Message(nonce: Int, id: Uuid, response: Response) 13 - } 14 - 15 - pub fn message(response: Response, id: Uuid, nonce: Int) -> Message { 16 - Message(nonce:, id:, response:) 17 - } 18 - 19 - /// A response is sent from the server to the client. 20 - /// 21 - /// A response does not necessarily respond to something, it might just be a pushed notification. 22 - pub type Response { 23 - Authenticated(Account) 24 - Fields(List(Field)) 25 - PutData(GameState) 26 - PatchData(List(Patch)) 27 - } 28 - 29 - pub fn to_json(message: Message) -> json.Json { 30 - let Message(nonce, id, response) = message 31 - let response = case response { 32 - Authenticated(account) -> 33 - account.to_json(account) 34 - |> repr.struct("Authenticated") 35 - Fields(fields) -> 36 - fields 37 - |> json.array(field.to_json) 38 - |> repr.struct("Fields") 39 - PutData(game_state) -> 40 - game_state.to_json(game_state) 41 - |> repr.struct("PutData") 42 - PatchData(patches) -> 43 - patches 44 - |> list.map(squirtle.patch_to_json_value) 45 - |> list.map(squirtle.json_value_to_gleam_json) 46 - |> json.preprocessed_array() 47 - |> repr.struct("PatchData") 48 - } 49 - json.object([ 50 - #("nonce", json.int(nonce)), 51 - #("id", json.string(uuid.to_string(id))), 52 - #("response", response), 53 - ]) 54 - } 55 - 56 - pub fn to_string(message: Message) -> String { 57 - message 58 - |> to_json() 59 - |> json.to_string() 60 - } 61 - 62 - pub fn decoder() { 63 - use nonce <- decode.field("nonce", decode.int) 64 - use id <- decode.field("id", repr.uuid()) 65 - use response <- decode.field("response", { 66 - use tag <- repr.struct_tag(PatchData([])) 67 - case tag { 68 - "Authenticated" -> { 69 - use payload <- repr.struct_payload(account.decoder()) 70 - decode.success(Authenticated(payload)) 71 - } 72 - "PutData" -> { 73 - use payload <- repr.struct_payload(game_state.decoder()) 74 - decode.success(PutData(payload)) 75 - } 76 - "Fields" -> { 77 - use payload <- repr.struct_payload(decode.list(field.decoder())) 78 - decode.success(Fields(payload)) 79 - } 80 - "PatchData" -> { 81 - use payload <- repr.struct_payload( 82 - decode.list(squirtle.patch_decoder()), 83 - ) 84 - decode.success(PatchData(payload)) 85 - } 86 - _ -> { 87 - decode.failure(PatchData([]), "valid #tag") 88 - } 89 - } 90 - }) 91 - decode.success(Message(nonce:, id:, response:)) 92 - } 93 - 94 - pub fn from_string(string: String) -> Result(Message, json.DecodeError) { 95 - json.parse(string, decoder()) 96 - }
-5
api/test/cartography_api_test.gleam
··· 1 - import gleeunit 2 - 3 - pub fn main() -> Nil { 4 - gleeunit.main() 5 - }
-52
api/test/game_state_test.gleam
··· 1 - import cartography_api/game_state 2 - import gleam/json 3 - import gleam/option 4 - 5 - pub fn game_state_round_trip_test() { 6 - let game_state = 7 - game_state.GameState( 8 - deck: game_state.Deck( 9 - tiles: [ 10 - game_state.Tile( 11 - id: game_state.TileId(3), 12 - tile_type_id: game_state.TileTypeId("bakery"), 13 - name: "Bakery 1", 14 - ), 15 - game_state.Tile( 16 - id: game_state.TileId(4), 17 - tile_type_id: game_state.TileTypeId("house"), 18 - name: "House 1", 19 - ), 20 - ], 21 - citizens: [ 22 - game_state.Citizen( 23 - id: game_state.CitizenId(1), 24 - species_id: game_state.SpeciesId("cat"), 25 - name: "Panda", 26 - home_tile_id: option.None, 27 - ), 28 - game_state.Citizen( 29 - id: game_state.CitizenId(2), 30 - species_id: game_state.SpeciesId("cat"), 31 - name: "Natto", 32 - home_tile_id: option.Some(game_state.TileId(4)), 33 - ), 34 - ], 35 - ), 36 - field: game_state.Field( 37 - name: "The Field", 38 - tiles: [ 39 - game_state.FieldTile(id: game_state.TileId(3), x: 1, y: 1), 40 - game_state.FieldTile(id: game_state.TileId(4), x: 2, y: 1), 41 - ], 42 - citizens: [ 43 - game_state.FieldCitizen(id: game_state.CitizenId(2), x: 2, y: 1), 44 - ], 45 - ), 46 - ) 47 - 48 - let json = game_state.to_string(game_state) 49 - let assert Ok(game_state_again) = json.parse(json, game_state.decoder()) 50 - 51 - assert game_state == game_state_again 52 - }
+1 -3
mise.toml
··· 1 1 [tools] 2 - erlang = "latest" 3 - gleam = "latest" 4 2 node = "22.21" 5 - rebar = "latest" 3 + rust = "latest"
-4
server/.gitignore
··· 1 - *.beam 2 - *.ez 3 - /build 4 - erl_crash.dump
-32
server/gleam.toml
··· 1 - name = "server" 2 - version = "1.0.0" 3 - 4 - # Fill out these fields if you intend to generate HTML documentation or publish 5 - # your project to the Hex package manager. 6 - # 7 - # description = "" 8 - # licences = ["Apache-2.0"] 9 - # repository = { type = "github", user = "", repo = "" } 10 - # links = [{ title = "Website", href = "" }] 11 - # 12 - # For a full reference of all the available options, you can have a look at 13 - # https://gleam.run/writing-gleam/gleam-toml/. 14 - 15 - [dependencies] 16 - gleam_stdlib = ">= 0.44.0 and < 2.0.0" 17 - gleam_otp = ">= 1.2.0 and < 2.0.0" 18 - pog = ">= 4.1.0 and < 5.0.0" 19 - envoy = ">= 1.1.0 and < 2.0.0" 20 - gleam_erlang = ">= 1.3.0 and < 2.0.0" 21 - mist = ">= 5.0.4 and < 6.0.0" 22 - gleam_json = ">= 3.1.0 and < 4.0.0" 23 - gleam_http = ">= 4.3.0 and < 5.0.0" 24 - gleam_time = ">= 1.6.0 and < 2.0.0" 25 - palabres = ">= 1.0.3 and < 2.0.0" 26 - squirrel = ">= 4.6.0 and < 5.0.0" 27 - cartography_api = { path = "../api" } 28 - squirtle = ">= 1.4.0 and < 2.0.0" 29 - youid = ">= 1.5.1 and < 2.0.0" 30 - 31 - [dev-dependencies] 32 - gleeunit = ">= 1.0.0 and < 2.0.0"
-65
server/manifest.toml
··· 1 - # This file was generated by Gleam 2 - # You typically do not need to edit this file 3 - 4 - packages = [ 5 - { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, 6 - { name = "backoff", version = "1.1.6", build_tools = ["rebar3"], requirements = [], otp_app = "backoff", source = "hex", outer_checksum = "CF0CFFF8995FB20562F822E5CC47D8CCF664C5ECDC26A684CBE85C225F9D7C39" }, 7 - { name = "cartography_api", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "squirtle", "youid"], source = "local", path = "../api" }, 8 - { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" }, 9 - { name = "eval", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "eval", source = "hex", outer_checksum = "264DAF4B49DF807F303CA4A4E4EBC012070429E40BE384C58FE094C4958F9BDA" }, 10 - { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, 11 - { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, 12 - { name = "glam", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "237C2CE218A2A0A5D46D625F8EF5B78F964BC91018B78D692B17E1AB84295229" }, 13 - { name = "gleam_community_ansi", version = "1.4.4", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "1B3AEA6074AB34D5F0674744F36DDC7290303A03295507E2DEC61EDD6F5777FE" }, 14 - { name = "gleam_community_colour", version = "2.0.4", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "6DB4665555D7D2B27F0EA32EF47E8BEBC4303821765F9C73D483F38EE24894F0" }, 15 - { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, 16 - { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, 17 - { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, 18 - { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, 19 - { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, 20 - { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, 21 - { name = "gleam_stdlib", version = "0.68.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F7FAEBD8EF260664E86A46C8DBA23508D1D11BB3BCC6EE1B89B3BC3E5C83FF1E" }, 22 - { name = "gleam_time", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "56DB0EF9433826D3B99DB0B4AF7A2BFED13D09755EC64B1DAAB46F804A9AD47D" }, 23 - { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, 24 - { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, 25 - { name = "glexer", version = "2.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "splitter"], otp_app = "glexer", source = "hex", outer_checksum = "41D8D2E855AEA87ADC94B7AF26A5FEA3C90268D4CF2CCBBD64FD6863714EE085" }, 26 - { name = "glisten", version = "8.0.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "86B838196592D9EBDE7A1D2369AE3A51E568F7DD2D168706C463C42D17B95312" }, 27 - { name = "gramps", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "8B7195978FBFD30B43DF791A8A272041B81E45D245314D7A41FC57237AA882A0" }, 28 - { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 29 - { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, 30 - { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, 31 - { name = "mist", version = "5.0.4", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7CED4B2D81FD547ADB093D97B9928B9419A7F58B8562A30A6CC17A252B31AD05" }, 32 - { name = "mug", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "mug", source = "hex", outer_checksum = "C01279D98E40371DA23461774B63F0E3581B8F1396049D881B0C7EB32799D93F" }, 33 - { name = "non_empty_list", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "non_empty_list", source = "hex", outer_checksum = "1CA43D18C07E98E9ED5A60D9CB2FFE0FF40DEFFA45D58A3FF589589F05658F7B" }, 34 - { name = "opentelemetry_api", version = "1.5.0", build_tools = ["rebar3", "mix"], requirements = [], otp_app = "opentelemetry_api", source = "hex", outer_checksum = "F53EC8A1337AE4A487D43AC89DA4BD3A3C99DDF576655D071DEED8B56A2D5DDA" }, 35 - { name = "palabres", version = "1.0.4", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib"], otp_app = "palabres", source = "hex", outer_checksum = "2809B7C10AE929E82454B4A6A0FB7D02073C5A509F9A63EE9F7B268A75D98965" }, 36 - { name = "pg_types", version = "0.6.0", build_tools = ["rebar3"], requirements = [], otp_app = "pg_types", source = "hex", outer_checksum = "9949A4849DD13408FA249AB7B745E0D2DFDB9532AEE2B9722326E33CD082A778" }, 37 - { name = "pgo", version = "0.20.0", build_tools = ["rebar3"], requirements = ["backoff", "opentelemetry_api", "pg_types"], otp_app = "pgo", source = "hex", outer_checksum = "2F11E6649CEB38E569EF56B16BE1D04874AE5B11A02867080A2817CE423C683B" }, 38 - { name = "pog", version = "4.1.0", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_otp", "gleam_stdlib", "gleam_time", "pgo"], otp_app = "pog", source = "hex", outer_checksum = "E4AFBA39A5FAA2E77291836C9683ADE882E65A06AB28CA7D61AE7A3AD61EBBD5" }, 39 - { name = "simplifile", version = "2.3.2", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "E049B4DACD4D206D87843BCF4C775A50AE0F50A52031A2FFB40C9ED07D6EC70A" }, 40 - { name = "splitter", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "3DFD6B6C49E61EDAF6F7B27A42054A17CFF6CA2135FF553D0CB61C234D281DD0" }, 41 - { name = "squirrel", version = "4.6.0", build_tools = ["gleam"], requirements = ["argv", "envoy", "eval", "filepath", "glam", "gleam_community_ansi", "gleam_crypto", "gleam_json", "gleam_regexp", "gleam_stdlib", "gleam_time", "glexer", "justin", "mug", "non_empty_list", "pog", "simplifile", "term_size", "tom", "tote", "youid"], otp_app = "squirrel", source = "hex", outer_checksum = "0ED10A868BDD1A5D4B68D99CD1C72DC3F23C6E36E16D33454C5F0C31BAC9CB1E" }, 42 - { name = "squirtle", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "squirtle", source = "hex", outer_checksum = "B4DCF7ED3E829E1DB6163F3E18677EF0862DF3F4CE076D98628EBB8D9BC974BC" }, 43 - { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, 44 - { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, 45 - { name = "tom", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "90791DA4AACE637E30081FE77049B8DB850FBC8CACC31515376BCC4E59BE1DD2" }, 46 - { name = "tote", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tote", source = "hex", outer_checksum = "A249892E26A53C668897F8D47845B0007EEE07707A1A03437487F0CD5A452CA5" }, 47 - { name = "youid", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_stdlib", "gleam_time"], otp_app = "youid", source = "hex", outer_checksum = "580E909FD704DB16416D5CB080618EDC2DA0F1BE4D21B490C0683335E3FFC5AF" }, 48 - ] 49 - 50 - [requirements] 51 - cartography_api = { path = "../api" } 52 - envoy = { version = ">= 1.1.0 and < 2.0.0" } 53 - gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" } 54 - gleam_http = { version = ">= 4.3.0 and < 5.0.0" } 55 - gleam_json = { version = ">= 3.1.0 and < 4.0.0" } 56 - gleam_otp = { version = ">= 1.2.0 and < 2.0.0" } 57 - gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 58 - gleam_time = { version = ">= 1.6.0 and < 2.0.0" } 59 - gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 60 - mist = { version = ">= 5.0.4 and < 6.0.0" } 61 - palabres = { version = ">= 1.0.3 and < 2.0.0" } 62 - pog = { version = ">= 4.1.0 and < 5.0.0" } 63 - squirrel = { version = ">= 4.6.0 and < 5.0.0" } 64 - squirtle = { version = ">= 1.4.0 and < 2.0.0" } 65 - youid = { version = ">= 1.5.1 and < 2.0.0" }
-151
server/src/actor/game_state_watcher.gleam
··· 1 - import bus 2 - import cartography_api/game_state 3 - import db/game_state as db_game_state 4 - import db/rows 5 - import db/sql 6 - import gleam/erlang/process 7 - import gleam/option 8 - import gleam/otp/actor 9 - import gleam/result 10 - import gleam/string 11 - import mist 12 - import pog 13 - import squirtle 14 - import youid/uuid 15 - 16 - pub type Init { 17 - Init( 18 - conn: mist.WebsocketConnection, 19 - message_id: uuid.Uuid, 20 - account_id: String, 21 - field_id: game_state.FieldId, 22 - ) 23 - } 24 - 25 - type State { 26 - State( 27 - conn: mist.WebsocketConnection, 28 - db: process.Name(pog.Message), 29 - message_id: uuid.Uuid, 30 - account_id: String, 31 - field_id: game_state.FieldId, 32 - game_state: game_state.GameState, 33 - ) 34 - } 35 - 36 - pub type Message { 37 - CardCreated(card_id: game_state.CardId) 38 - Stop 39 - } 40 - 41 - pub fn start(db: process.Name(pog.Message), bus: bus.Bus, init: Init) { 42 - actor.new_with_initialiser(50, fn(sub) { 43 - let selector = 44 - process.new_selector() 45 - |> process.select(sub) 46 - |> process.select_map( 47 - bus.on_card_account(bus, init.account_id), 48 - CardCreated, 49 - ) 50 - 51 - use game_state <- result.try(db_game_state.load( 52 - pog.named_connection(db), 53 - init.field_id, 54 - )) 55 - 56 - State( 57 - conn: init.conn, 58 - field_id: init.field_id, 59 - message_id: init.message_id, 60 - account_id: init.account_id, 61 - db:, 62 - game_state:, 63 - ) 64 - |> actor.initialised() 65 - |> actor.selecting(selector) 66 - |> actor.returning(sub) 67 - |> Ok() 68 - }) 69 - |> actor.on_message(handle_message) 70 - |> actor.start() 71 - } 72 - 73 - fn handle_stop(message, cb: fn() -> actor.Next(s, m)) { 74 - case message { 75 - Stop -> actor.stop() 76 - _ -> cb() 77 - } 78 - } 79 - 80 - fn handle_message(state: State, message: Message) -> actor.Next(State, Message) { 81 - use <- handle_stop(message) 82 - let new_state = case message { 83 - Stop -> panic as "unreachable" 84 - CardCreated(game_state.TileId(tile_id)) -> { 85 - use tile <- result.try( 86 - state.db 87 - |> pog.named_connection() 88 - |> sql.get_tile(tile_id) 89 - |> result.map_error(string.inspect), 90 - ) 91 - use tile <- rows.one_or(tile, "created card (tile) not found") 92 - state.game_state 93 - |> add_tile_to_deck(game_state.Tile( 94 - id: game_state.TileId(tile.id), 95 - name: tile.name, 96 - tile_type_id: game_state.TileTypeId(tile.tile_type_id), 97 - )) 98 - |> fn(gs) { State(..state, game_state: gs) } 99 - |> Ok() 100 - } 101 - CardCreated(game_state.CitizenId(citizen_id)) -> { 102 - use citizen <- result.try( 103 - state.db 104 - |> pog.named_connection() 105 - |> sql.get_citizen(citizen_id) 106 - |> result.map_error(string.inspect), 107 - ) 108 - use citizen <- rows.one_or(citizen, "created card (citizen) not found") 109 - state.game_state 110 - |> add_citizen_to_deck(game_state.Citizen( 111 - id: game_state.CitizenId(citizen.id), 112 - name: citizen.name, 113 - species_id: game_state.SpeciesId(citizen.species_id), 114 - home_tile_id: option.map(citizen.home_tile_id, game_state.TileId), 115 - )) 116 - |> fn(gs) { State(..state, game_state: gs) } 117 - |> Ok() 118 - } 119 - } 120 - let result = { 121 - use new_state <- result.try(new_state) 122 - use Nil <- result.try( 123 - game_state.diff(state.game_state, new_state.game_state) 124 - |> squirtle.patches_to_string() 125 - |> mist.send_text_frame(state.conn, _) 126 - |> result.map_error(string.inspect), 127 - ) 128 - Ok(state) 129 - } 130 - case result { 131 - Ok(state) -> actor.continue(state) 132 - Error(error) -> actor.stop_abnormal(error) 133 - } 134 - } 135 - 136 - fn add_tile_to_deck(state: game_state.GameState, tile: game_state.Tile) { 137 - game_state.GameState( 138 - ..state, 139 - deck: game_state.Deck(..state.deck, tiles: [tile, ..state.deck.tiles]), 140 - ) 141 - } 142 - 143 - fn add_citizen_to_deck(state: game_state.GameState, citizen: game_state.Citizen) { 144 - game_state.GameState( 145 - ..state, 146 - deck: game_state.Deck(..state.deck, citizens: [ 147 - citizen, 148 - ..state.deck.citizens 149 - ]), 150 - ) 151 - }
-33
server/src/bus.gleam
··· 1 - import cartography_api/game_state.{type CardId} 2 - import gleam/erlang/process 3 - import gleam/otp/static_supervisor 4 - import pubsub 5 - 6 - pub opaque type Bus { 7 - Bus(card_accounts_channel: process.Name(pubsub.Message(String, CardId))) 8 - } 9 - 10 - pub fn supervised() { 11 - let card_accounts_channel = process.new_name("card_accounts_channel") 12 - 13 - let child_spec = 14 - static_supervisor.new(static_supervisor.OneForOne) 15 - |> static_supervisor.add( 16 - pubsub.new() 17 - |> pubsub.named(card_accounts_channel) 18 - |> pubsub.supervised(), 19 - ) 20 - |> static_supervisor.supervised() 21 - 22 - #(child_spec, Bus(card_accounts_channel:)) 23 - } 24 - 25 - pub fn notify_card_account(bus: Bus, account_id: String, card_id: CardId) { 26 - process.named_subject(bus.card_accounts_channel) 27 - |> pubsub.broadcast(account_id, card_id) 28 - } 29 - 30 - pub fn on_card_account(bus: Bus, account_id: String) -> process.Subject(CardId) { 31 - process.named_subject(bus.card_accounts_channel) 32 - |> pubsub.subscribe(account_id) 33 - }
-101
server/src/db/game_state.gleam
··· 1 - import cartography_api/game_state 2 - import db/rows 3 - import db/sql 4 - import gleam/list 5 - import gleam/option 6 - import gleam/pair 7 - import gleam/result 8 - import gleam/string 9 - import pog 10 - 11 - fn to_tile(tile: sql.GetPlayerDeckRow) -> game_state.Tile { 12 - case tile { 13 - sql.GetPlayerDeckRow( 14 - id, 15 - name, 16 - tile_type_id: option.Some(tile_type_id), 17 - species_id: option.None, 18 - home_tile_id: option.None, 19 - ) -> 20 - game_state.Tile( 21 - id: game_state.TileId(id), 22 - tile_type_id: game_state.TileTypeId(tile_type_id), 23 - name:, 24 - ) 25 - _ -> panic as "unreachable" 26 - } 27 - } 28 - 29 - fn to_citizen(citizen: sql.GetPlayerDeckRow) -> game_state.Citizen { 30 - case citizen { 31 - sql.GetPlayerDeckRow( 32 - id, 33 - name, 34 - tile_type_id: option.None, 35 - species_id: option.Some(species_id), 36 - home_tile_id:, 37 - ) -> 38 - game_state.Citizen( 39 - id: game_state.CitizenId(id), 40 - species_id: game_state.SpeciesId(species_id), 41 - name:, 42 - home_tile_id: option.map(home_tile_id, game_state.TileId), 43 - ) 44 - _ -> panic as "unreachable" 45 - } 46 - } 47 - 48 - fn to_field_tile(tile: sql.GetFieldTilesRow) -> game_state.FieldTile { 49 - game_state.FieldTile( 50 - id: game_state.TileId(tile.tile_id), 51 - x: tile.grid_x, 52 - y: tile.grid_y, 53 - ) 54 - } 55 - 56 - fn to_field_citizen(citizen: sql.GetFieldCitizensRow) -> game_state.FieldCitizen { 57 - game_state.FieldCitizen( 58 - id: game_state.CitizenId(citizen.citizen_id), 59 - x: citizen.grid_x, 60 - y: citizen.grid_y, 61 - ) 62 - } 63 - 64 - pub fn load(db: pog.Connection, field_id: game_state.FieldId) { 65 - use field <- result.try( 66 - db 67 - |> sql.get_field_by_id(field_id.id) 68 - |> result.map_error(string.inspect), 69 - ) 70 - use field <- rows.one_or(field, "field not found") 71 - use #(tiles, citizens) <- result.try( 72 - sql.get_player_deck(db, field.account_id) 73 - |> result.map(fn(result) { 74 - list.partition(result.rows, fn(card) { option.is_some(card.tile_type_id) }) 75 - |> pair.map_first(list.map(_, to_tile)) 76 - |> pair.map_second(list.map(_, to_citizen)) 77 - }) 78 - |> result.map_error(string.inspect), 79 - ) 80 - 81 - use field_tiles <- result.try( 82 - sql.get_field_tiles(db, field_id.id) 83 - |> result.map(fn(result) { list.map(result.rows, to_field_tile) }) 84 - |> result.map_error(string.inspect), 85 - ) 86 - 87 - use field_citizens <- result.try( 88 - sql.get_field_citizens(db, field_id.id) 89 - |> result.map(fn(result) { list.map(result.rows, to_field_citizen) }) 90 - |> result.map_error(string.inspect), 91 - ) 92 - 93 - Ok(game_state.GameState( 94 - deck: game_state.Deck(tiles:, citizens:), 95 - field: game_state.Field( 96 - name: field.name, 97 - tiles: field_tiles, 98 - citizens: field_citizens, 99 - ), 100 - )) 101 - }
-63
server/src/db/rows.gleam
··· 1 - import gleam/dynamic/decode 2 - import gleam/json 3 - import gleam/option 4 - import gleam/result 5 - import pog 6 - 7 - pub type Error(e) { 8 - TooManyRows 9 - NoRows 10 - QueryError(pog.QueryError) 11 - HandlerError(e) 12 - } 13 - 14 - pub fn one_or_none( 15 - result: pog.Returned(t), 16 - with_row: fn(option.Option(t)) -> Result(u, Error(e)), 17 - ) -> Result(u, Error(e)) { 18 - case result.rows { 19 - [] -> with_row(option.None) 20 - [row] -> with_row(option.Some(row)) 21 - _ -> Error(TooManyRows) 22 - } 23 - } 24 - 25 - pub fn one( 26 - result: pog.Returned(t), 27 - with_row: fn(t) -> Result(u, Error(e)), 28 - ) -> Result(u, Error(e)) { 29 - case result.rows { 30 - [] -> Error(NoRows) 31 - [row] -> with_row(row) 32 - _ -> Error(TooManyRows) 33 - } 34 - } 35 - 36 - pub fn one_or( 37 - result: pog.Returned(t), 38 - error: e, 39 - with_row: fn(t) -> Result(u, e), 40 - ) -> Result(u, e) { 41 - case result.rows { 42 - [] -> Error(error) 43 - [row] -> with_row(row) 44 - _ -> Error(error) 45 - } 46 - } 47 - 48 - pub fn execute( 49 - query: pog.Query(t), 50 - database: pog.Connection, 51 - with_rows: fn(pog.Returned(t)) -> Result(u, Error(e)), 52 - ) -> Result(u, Error(e)) { 53 - use rows <- result.try( 54 - pog.execute(query, database) |> result.map_error(QueryError), 55 - ) 56 - with_rows(rows) 57 - } 58 - 59 - pub fn json(decoder: decode.Decoder(t)) { 60 - use json_string <- decode.then(decode.string) 61 - let assert Ok(result) = json.parse(json_string, decoder) 62 - decode.success(result) 63 - }
-651
server/src/db/sql.gleam
··· 1 - //// This module contains the code to run the sql queries defined in 2 - //// `./src/db/sql`. 3 - //// > 🐿️ This module was generated automatically using v4.6.0 of 4 - //// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 5 - //// 6 - 7 - import gleam/dynamic/decode 8 - import gleam/option.{type Option} 9 - import pog 10 - 11 - /// A row you get from running the `create_account` query 12 - /// defined in `./src/db/sql/create_account.sql`. 13 - /// 14 - /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 15 - /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 16 - /// 17 - pub type CreateAccountRow { 18 - CreateAccountRow(id: String) 19 - } 20 - 21 - /// Runs the `create_account` query 22 - /// defined in `./src/db/sql/create_account.sql`. 23 - /// 24 - /// > 🐿️ This function was generated automatically using v4.6.0 of 25 - /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 26 - /// 27 - pub fn create_account( 28 - db: pog.Connection, 29 - arg_1: String, 30 - ) -> Result(pog.Returned(CreateAccountRow), pog.QueryError) { 31 - let decoder = { 32 - use id <- decode.field(0, decode.string) 33 - decode.success(CreateAccountRow(id:)) 34 - } 35 - 36 - "INSERT INTO 37 - accounts (id) 38 - VALUES 39 - ($1) 40 - ON CONFLICT DO NOTHING 41 - RETURNING 42 - * 43 - " 44 - |> pog.query 45 - |> pog.parameter(pog.text(arg_1)) 46 - |> pog.returning(decoder) 47 - |> pog.execute(db) 48 - } 49 - 50 - /// A row you get from running the `create_citizen` query 51 - /// defined in `./src/db/sql/create_citizen.sql`. 52 - /// 53 - /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 54 - /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 55 - /// 56 - pub type CreateCitizenRow { 57 - CreateCitizenRow(card_id: Int, account_id: String) 58 - } 59 - 60 - /// Runs the `create_citizen` query 61 - /// defined in `./src/db/sql/create_citizen.sql`. 62 - /// 63 - /// > 🐿️ This function was generated automatically using v4.6.0 of 64 - /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 65 - /// 66 - pub fn create_citizen( 67 - db: pog.Connection, 68 - arg_1: String, 69 - arg_2: String, 70 - ) -> Result(pog.Returned(CreateCitizenRow), pog.QueryError) { 71 - let decoder = { 72 - use card_id <- decode.field(0, decode.int) 73 - use account_id <- decode.field(1, decode.string) 74 - decode.success(CreateCitizenRow(card_id:, account_id:)) 75 - } 76 - 77 - "WITH 78 - cards_inserted AS ( 79 - INSERT INTO 80 - cards (card_type_id) 81 - VALUES 82 - ($1) 83 - RETURNING 84 - * 85 - ), 86 - citizens_inserted AS ( 87 - INSERT INTO 88 - citizens (id, species_id, name) 89 - SELECT 90 - card.id, 91 - card.card_type_id, 92 - '' 93 - FROM 94 - cards_inserted card 95 - ) 96 - INSERT INTO 97 - card_accounts (card_id, account_id) 98 - SELECT 99 - card.id, 100 - $2 101 - FROM 102 - cards_inserted card 103 - RETURNING 104 - *; 105 - " 106 - |> pog.query 107 - |> pog.parameter(pog.text(arg_1)) 108 - |> pog.parameter(pog.text(arg_2)) 109 - |> pog.returning(decoder) 110 - |> pog.execute(db) 111 - } 112 - 113 - /// A row you get from running the `create_tile` query 114 - /// defined in `./src/db/sql/create_tile.sql`. 115 - /// 116 - /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 117 - /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 118 - /// 119 - pub type CreateTileRow { 120 - CreateTileRow(card_id: Int, account_id: String) 121 - } 122 - 123 - /// Runs the `create_tile` query 124 - /// defined in `./src/db/sql/create_tile.sql`. 125 - /// 126 - /// > 🐿️ This function was generated automatically using v4.6.0 of 127 - /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 128 - /// 129 - pub fn create_tile( 130 - db: pog.Connection, 131 - arg_1: String, 132 - arg_2: String, 133 - ) -> Result(pog.Returned(CreateTileRow), pog.QueryError) { 134 - let decoder = { 135 - use card_id <- decode.field(0, decode.int) 136 - use account_id <- decode.field(1, decode.string) 137 - decode.success(CreateTileRow(card_id:, account_id:)) 138 - } 139 - 140 - "WITH 141 - cards_inserted AS ( 142 - INSERT INTO 143 - cards (card_type_id) 144 - VALUES 145 - ($1) 146 - RETURNING 147 - * 148 - ), 149 - tiles_inserted AS ( 150 - INSERT INTO 151 - tiles (id, tile_type_id, name) 152 - SELECT 153 - card.id, 154 - card.card_type_id, 155 - '' 156 - FROM 157 - cards_inserted card 158 - ) 159 - INSERT INTO 160 - card_accounts (card_id, account_id) 161 - SELECT 162 - card.id, 163 - $2 164 - FROM 165 - cards_inserted card 166 - RETURNING 167 - *; 168 - " 169 - |> pog.query 170 - |> pog.parameter(pog.text(arg_1)) 171 - |> pog.parameter(pog.text(arg_2)) 172 - |> pog.returning(decoder) 173 - |> pog.execute(db) 174 - } 175 - 176 - /// A row you get from running the `get_account` query 177 - /// defined in `./src/db/sql/get_account.sql`. 178 - /// 179 - /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 180 - /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 181 - /// 182 - pub type GetAccountRow { 183 - GetAccountRow(id: String) 184 - } 185 - 186 - /// Runs the `get_account` query 187 - /// defined in `./src/db/sql/get_account.sql`. 188 - /// 189 - /// > 🐿️ This function was generated automatically using v4.6.0 of 190 - /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 191 - /// 192 - pub fn get_account( 193 - db: pog.Connection, 194 - arg_1: String, 195 - ) -> Result(pog.Returned(GetAccountRow), pog.QueryError) { 196 - let decoder = { 197 - use id <- decode.field(0, decode.string) 198 - decode.success(GetAccountRow(id:)) 199 - } 200 - 201 - "SELECT 202 - * 203 - FROM 204 - accounts 205 - WHERE 206 - id = $1 207 - " 208 - |> pog.query 209 - |> pog.parameter(pog.text(arg_1)) 210 - |> pog.returning(decoder) 211 - |> pog.execute(db) 212 - } 213 - 214 - /// A row you get from running the `get_card_type` query 215 - /// defined in `./src/db/sql/get_card_type.sql`. 216 - /// 217 - /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 218 - /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 219 - /// 220 - pub type GetCardTypeRow { 221 - GetCardTypeRow(id: String, card_set_id: String, class: CardClass) 222 - } 223 - 224 - /// Runs the `get_card_type` query 225 - /// defined in `./src/db/sql/get_card_type.sql`. 226 - /// 227 - /// > 🐿️ This function was generated automatically using v4.6.0 of 228 - /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 229 - /// 230 - pub fn get_card_type( 231 - db: pog.Connection, 232 - arg_1: String, 233 - ) -> Result(pog.Returned(GetCardTypeRow), pog.QueryError) { 234 - let decoder = { 235 - use id <- decode.field(0, decode.string) 236 - use card_set_id <- decode.field(1, decode.string) 237 - use class <- decode.field(2, card_class_decoder()) 238 - decode.success(GetCardTypeRow(id:, card_set_id:, class:)) 239 - } 240 - 241 - "SELECT 242 - * 243 - FROM 244 - card_types 245 - WHERE 246 - id = $1; 247 - " 248 - |> pog.query 249 - |> pog.parameter(pog.text(arg_1)) 250 - |> pog.returning(decoder) 251 - |> pog.execute(db) 252 - } 253 - 254 - /// A row you get from running the `get_citizen` query 255 - /// defined in `./src/db/sql/get_citizen.sql`. 256 - /// 257 - /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 258 - /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 259 - /// 260 - pub type GetCitizenRow { 261 - GetCitizenRow( 262 - species_id: String, 263 - name: String, 264 - home_tile_id: Option(Int), 265 - id: Int, 266 - ) 267 - } 268 - 269 - /// Runs the `get_citizen` query 270 - /// defined in `./src/db/sql/get_citizen.sql`. 271 - /// 272 - /// > 🐿️ This function was generated automatically using v4.6.0 of 273 - /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 274 - /// 275 - pub fn get_citizen( 276 - db: pog.Connection, 277 - arg_1: Int, 278 - ) -> Result(pog.Returned(GetCitizenRow), pog.QueryError) { 279 - let decoder = { 280 - use species_id <- decode.field(0, decode.string) 281 - use name <- decode.field(1, decode.string) 282 - use home_tile_id <- decode.field(2, decode.optional(decode.int)) 283 - use id <- decode.field(3, decode.int) 284 - decode.success(GetCitizenRow(species_id:, name:, home_tile_id:, id:)) 285 - } 286 - 287 - "SELECT 288 - * 289 - FROM 290 - citizens 291 - WHERE 292 - id = $1; 293 - " 294 - |> pog.query 295 - |> pog.parameter(pog.int(arg_1)) 296 - |> pog.returning(decoder) 297 - |> pog.execute(db) 298 - } 299 - 300 - /// A row you get from running the `get_field_and_tiles_by_id` query 301 - /// defined in `./src/db/sql/get_field_and_tiles_by_id.sql`. 302 - /// 303 - /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 304 - /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 305 - /// 306 - pub type GetFieldAndTilesByIdRow { 307 - GetFieldAndTilesByIdRow(field: String, field_tiles: String) 308 - } 309 - 310 - /// Runs the `get_field_and_tiles_by_id` query 311 - /// defined in `./src/db/sql/get_field_and_tiles_by_id.sql`. 312 - /// 313 - /// > 🐿️ This function was generated automatically using v4.6.0 of 314 - /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 315 - /// 316 - pub fn get_field_and_tiles_by_id( 317 - db: pog.Connection, 318 - arg_1: Int, 319 - ) -> Result(pog.Returned(GetFieldAndTilesByIdRow), pog.QueryError) { 320 - let decoder = { 321 - use field <- decode.field(0, decode.string) 322 - use field_tiles <- decode.field(1, decode.string) 323 - decode.success(GetFieldAndTilesByIdRow(field:, field_tiles:)) 324 - } 325 - 326 - "SELECT 327 - to_json(fields.*) AS field, 328 - json_arrayagg (field_tiles.* ABSENT ON NULL) AS field_tiles 329 - FROM 330 - fields 331 - LEFT JOIN field_tiles ON field_tiles.field_id = fields.id 332 - WHERE 333 - fields.id = $1 334 - GROUP BY 335 - fields.id 336 - " 337 - |> pog.query 338 - |> pog.parameter(pog.int(arg_1)) 339 - |> pog.returning(decoder) 340 - |> pog.execute(db) 341 - } 342 - 343 - /// A row you get from running the `get_field_by_id` query 344 - /// defined in `./src/db/sql/get_field_by_id.sql`. 345 - /// 346 - /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 347 - /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 348 - /// 349 - pub type GetFieldByIdRow { 350 - GetFieldByIdRow(id: Int, name: String, account_id: String) 351 - } 352 - 353 - /// Runs the `get_field_by_id` query 354 - /// defined in `./src/db/sql/get_field_by_id.sql`. 355 - /// 356 - /// > 🐿️ This function was generated automatically using v4.6.0 of 357 - /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 358 - /// 359 - pub fn get_field_by_id( 360 - db: pog.Connection, 361 - arg_1: Int, 362 - ) -> Result(pog.Returned(GetFieldByIdRow), pog.QueryError) { 363 - let decoder = { 364 - use id <- decode.field(0, decode.int) 365 - use name <- decode.field(1, decode.string) 366 - use account_id <- decode.field(2, decode.string) 367 - decode.success(GetFieldByIdRow(id:, name:, account_id:)) 368 - } 369 - 370 - "SELECT 371 - * 372 - FROM 373 - fields 374 - WHERE 375 - id = $1 376 - " 377 - |> pog.query 378 - |> pog.parameter(pog.int(arg_1)) 379 - |> pog.returning(decoder) 380 - |> pog.execute(db) 381 - } 382 - 383 - /// A row you get from running the `get_field_citizens` query 384 - /// defined in `./src/db/sql/get_field_citizens.sql`. 385 - /// 386 - /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 387 - /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 388 - /// 389 - pub type GetFieldCitizensRow { 390 - GetFieldCitizensRow( 391 - citizen_id: Int, 392 - account_id: String, 393 - field_id: Int, 394 - grid_x: Int, 395 - grid_y: Int, 396 - ) 397 - } 398 - 399 - /// Runs the `get_field_citizens` query 400 - /// defined in `./src/db/sql/get_field_citizens.sql`. 401 - /// 402 - /// > 🐿️ This function was generated automatically using v4.6.0 of 403 - /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 404 - /// 405 - pub fn get_field_citizens( 406 - db: pog.Connection, 407 - arg_1: Int, 408 - ) -> Result(pog.Returned(GetFieldCitizensRow), pog.QueryError) { 409 - let decoder = { 410 - use citizen_id <- decode.field(0, decode.int) 411 - use account_id <- decode.field(1, decode.string) 412 - use field_id <- decode.field(2, decode.int) 413 - use grid_x <- decode.field(3, decode.int) 414 - use grid_y <- decode.field(4, decode.int) 415 - decode.success(GetFieldCitizensRow( 416 - citizen_id:, 417 - account_id:, 418 - field_id:, 419 - grid_x:, 420 - grid_y:, 421 - )) 422 - } 423 - 424 - "SELECT 425 - * 426 - FROM 427 - field_citizens 428 - WHERE 429 - field_citizens.field_id = $1; 430 - " 431 - |> pog.query 432 - |> pog.parameter(pog.int(arg_1)) 433 - |> pog.returning(decoder) 434 - |> pog.execute(db) 435 - } 436 - 437 - /// A row you get from running the `get_field_tiles` query 438 - /// defined in `./src/db/sql/get_field_tiles.sql`. 439 - /// 440 - /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 441 - /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 442 - /// 443 - pub type GetFieldTilesRow { 444 - GetFieldTilesRow( 445 - tile_id: Int, 446 - account_id: String, 447 - field_id: Int, 448 - grid_x: Int, 449 - grid_y: Int, 450 - ) 451 - } 452 - 453 - /// Runs the `get_field_tiles` query 454 - /// defined in `./src/db/sql/get_field_tiles.sql`. 455 - /// 456 - /// > 🐿️ This function was generated automatically using v4.6.0 of 457 - /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 458 - /// 459 - pub fn get_field_tiles( 460 - db: pog.Connection, 461 - arg_1: Int, 462 - ) -> Result(pog.Returned(GetFieldTilesRow), pog.QueryError) { 463 - let decoder = { 464 - use tile_id <- decode.field(0, decode.int) 465 - use account_id <- decode.field(1, decode.string) 466 - use field_id <- decode.field(2, decode.int) 467 - use grid_x <- decode.field(3, decode.int) 468 - use grid_y <- decode.field(4, decode.int) 469 - decode.success(GetFieldTilesRow( 470 - tile_id:, 471 - account_id:, 472 - field_id:, 473 - grid_x:, 474 - grid_y:, 475 - )) 476 - } 477 - 478 - "SELECT 479 - * 480 - FROM 481 - field_tiles 482 - WHERE 483 - field_tiles.field_id = $1; 484 - " 485 - |> pog.query 486 - |> pog.parameter(pog.int(arg_1)) 487 - |> pog.returning(decoder) 488 - |> pog.execute(db) 489 - } 490 - 491 - /// A row you get from running the `get_player_deck` query 492 - /// defined in `./src/db/sql/get_player_deck.sql`. 493 - /// 494 - /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 495 - /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 496 - /// 497 - pub type GetPlayerDeckRow { 498 - GetPlayerDeckRow( 499 - id: Int, 500 - name: String, 501 - tile_type_id: Option(String), 502 - species_id: Option(String), 503 - home_tile_id: Option(Int), 504 - ) 505 - } 506 - 507 - /// Runs the `get_player_deck` query 508 - /// defined in `./src/db/sql/get_player_deck.sql`. 509 - /// 510 - /// > 🐿️ This function was generated automatically using v4.6.0 of 511 - /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 512 - /// 513 - pub fn get_player_deck( 514 - db: pog.Connection, 515 - arg_1: String, 516 - ) -> Result(pog.Returned(GetPlayerDeckRow), pog.QueryError) { 517 - let decoder = { 518 - use id <- decode.field(0, decode.int) 519 - use name <- decode.field(1, decode.string) 520 - use tile_type_id <- decode.field(2, decode.optional(decode.string)) 521 - use species_id <- decode.field(3, decode.optional(decode.string)) 522 - use home_tile_id <- decode.field(4, decode.optional(decode.int)) 523 - decode.success(GetPlayerDeckRow( 524 - id:, 525 - name:, 526 - tile_type_id:, 527 - species_id:, 528 - home_tile_id:, 529 - )) 530 - } 531 - 532 - "SELECT 533 - cards.id as id, 534 - coalesce(tiles.name, citizens.name) as name, 535 - tiles.tile_type_id, 536 - citizens.species_id, 537 - citizens.home_tile_id 538 - FROM 539 - cards 540 - INNER JOIN card_accounts ON card_accounts.card_id = cards.id 541 - LEFT OUTER JOIN citizens ON cards.id = citizens.id 542 - LEFT OUTER JOIN tiles ON cards.id = tiles.id 543 - WHERE 544 - card_accounts.account_id = $1; 545 - " 546 - |> pog.query 547 - |> pog.parameter(pog.text(arg_1)) 548 - |> pog.returning(decoder) 549 - |> pog.execute(db) 550 - } 551 - 552 - /// A row you get from running the `get_tile` query 553 - /// defined in `./src/db/sql/get_tile.sql`. 554 - /// 555 - /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 556 - /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 557 - /// 558 - pub type GetTileRow { 559 - GetTileRow(id: Int, tile_type_id: String, name: String) 560 - } 561 - 562 - /// Runs the `get_tile` query 563 - /// defined in `./src/db/sql/get_tile.sql`. 564 - /// 565 - /// > 🐿️ This function was generated automatically using v4.6.0 of 566 - /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 567 - /// 568 - pub fn get_tile( 569 - db: pog.Connection, 570 - arg_1: Int, 571 - ) -> Result(pog.Returned(GetTileRow), pog.QueryError) { 572 - let decoder = { 573 - use id <- decode.field(0, decode.int) 574 - use tile_type_id <- decode.field(1, decode.string) 575 - use name <- decode.field(2, decode.string) 576 - decode.success(GetTileRow(id:, tile_type_id:, name:)) 577 - } 578 - 579 - "SELECT 580 - * 581 - FROM 582 - tiles 583 - WHERE 584 - id = $1; 585 - " 586 - |> pog.query 587 - |> pog.parameter(pog.int(arg_1)) 588 - |> pog.returning(decoder) 589 - |> pog.execute(db) 590 - } 591 - 592 - /// A row you get from running the `list_fields_for_account` query 593 - /// defined in `./src/db/sql/list_fields_for_account.sql`. 594 - /// 595 - /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 596 - /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 597 - /// 598 - pub type ListFieldsForAccountRow { 599 - ListFieldsForAccountRow(id: Int, name: String, account_id: String) 600 - } 601 - 602 - /// Runs the `list_fields_for_account` query 603 - /// defined in `./src/db/sql/list_fields_for_account.sql`. 604 - /// 605 - /// > 🐿️ This function was generated automatically using v4.6.0 of 606 - /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 607 - /// 608 - pub fn list_fields_for_account( 609 - db: pog.Connection, 610 - arg_1: String, 611 - ) -> Result(pog.Returned(ListFieldsForAccountRow), pog.QueryError) { 612 - let decoder = { 613 - use id <- decode.field(0, decode.int) 614 - use name <- decode.field(1, decode.string) 615 - use account_id <- decode.field(2, decode.string) 616 - decode.success(ListFieldsForAccountRow(id:, name:, account_id:)) 617 - } 618 - 619 - "SELECT 620 - * 621 - FROM 622 - fields 623 - WHERE 624 - account_id = $1 625 - " 626 - |> pog.query 627 - |> pog.parameter(pog.text(arg_1)) 628 - |> pog.returning(decoder) 629 - |> pog.execute(db) 630 - } 631 - 632 - // --- Enums ------------------------------------------------------------------- 633 - 634 - /// Corresponds to the Postgres `card_class` enum. 635 - /// 636 - /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 637 - /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 638 - /// 639 - pub type CardClass { 640 - Citizen 641 - Tile 642 - } 643 - 644 - fn card_class_decoder() -> decode.Decoder(CardClass) { 645 - use card_class <- decode.then(decode.string) 646 - case card_class { 647 - "citizen" -> decode.success(Citizen) 648 - "tile" -> decode.success(Tile) 649 - _ -> decode.failure(Citizen, "CardClass") 650 - } 651 - }
-7
server/src/db/sql/create_account.sql
··· 1 - INSERT INTO 2 - accounts (id) 3 - VALUES 4 - ($1) 5 - ON CONFLICT DO NOTHING 6 - RETURNING 7 - *
-28
server/src/db/sql/create_citizen.sql
··· 1 - WITH 2 - cards_inserted AS ( 3 - INSERT INTO 4 - cards (card_type_id) 5 - VALUES 6 - ($1) 7 - RETURNING 8 - * 9 - ), 10 - citizens_inserted AS ( 11 - INSERT INTO 12 - citizens (id, species_id, name) 13 - SELECT 14 - card.id, 15 - card.card_type_id, 16 - '' 17 - FROM 18 - cards_inserted card 19 - ) 20 - INSERT INTO 21 - card_accounts (card_id, account_id) 22 - SELECT 23 - card.id, 24 - $2 25 - FROM 26 - cards_inserted card 27 - RETURNING 28 - *;
-28
server/src/db/sql/create_tile.sql
··· 1 - WITH 2 - cards_inserted AS ( 3 - INSERT INTO 4 - cards (card_type_id) 5 - VALUES 6 - ($1) 7 - RETURNING 8 - * 9 - ), 10 - tiles_inserted AS ( 11 - INSERT INTO 12 - tiles (id, tile_type_id, name) 13 - SELECT 14 - card.id, 15 - card.card_type_id, 16 - '' 17 - FROM 18 - cards_inserted card 19 - ) 20 - INSERT INTO 21 - card_accounts (card_id, account_id) 22 - SELECT 23 - card.id, 24 - $2 25 - FROM 26 - cards_inserted card 27 - RETURNING 28 - *;
-6
server/src/db/sql/get_account.sql
··· 1 - SELECT 2 - * 3 - FROM 4 - accounts 5 - WHERE 6 - id = $1
-6
server/src/db/sql/get_card_type.sql
··· 1 - SELECT 2 - * 3 - FROM 4 - card_types 5 - WHERE 6 - id = $1;
-6
server/src/db/sql/get_citizen.sql
··· 1 - SELECT 2 - * 3 - FROM 4 - citizens 5 - WHERE 6 - id = $1;
-10
server/src/db/sql/get_field_and_tiles_by_id.sql
··· 1 - SELECT 2 - to_json(fields.*) AS field, 3 - json_arrayagg (field_tiles.* ABSENT ON NULL) AS field_tiles 4 - FROM 5 - fields 6 - LEFT JOIN field_tiles ON field_tiles.field_id = fields.id 7 - WHERE 8 - fields.id = $1 9 - GROUP BY 10 - fields.id
-6
server/src/db/sql/get_field_by_id.sql
··· 1 - SELECT 2 - * 3 - FROM 4 - fields 5 - WHERE 6 - id = $1
-6
server/src/db/sql/get_field_citizens.sql
··· 1 - SELECT 2 - * 3 - FROM 4 - field_citizens 5 - WHERE 6 - field_citizens.field_id = $1;
-6
server/src/db/sql/get_field_tiles.sql
··· 1 - SELECT 2 - * 3 - FROM 4 - field_tiles 5 - WHERE 6 - field_tiles.field_id = $1;
-13
server/src/db/sql/get_player_deck.sql
··· 1 - SELECT 2 - cards.id as id, 3 - coalesce(tiles.name, citizens.name) as name, 4 - tiles.tile_type_id, 5 - citizens.species_id, 6 - citizens.home_tile_id 7 - FROM 8 - cards 9 - INNER JOIN card_accounts ON card_accounts.card_id = cards.id 10 - LEFT OUTER JOIN citizens ON cards.id = citizens.id 11 - LEFT OUTER JOIN tiles ON cards.id = tiles.id 12 - WHERE 13 - card_accounts.account_id = $1;
-6
server/src/db/sql/get_tile.sql
··· 1 - SELECT 2 - * 3 - FROM 4 - tiles 5 - WHERE 6 - id = $1;
-6
server/src/db/sql/list_fields_for_account.sql
··· 1 - SELECT 2 - * 3 - FROM 4 - fields 5 - WHERE 6 - account_id = $1
-45
server/src/handlers/authenticate_handler.gleam
··· 1 - import cartography_api/account 2 - import cartography_api/response 3 - import db/rows 4 - import db/sql 5 - import gleam/option 6 - import gleam/result 7 - import gleam/string 8 - import mist 9 - import websocket/state 10 - import youid/uuid 11 - 12 - pub fn handle( 13 - st: state.State, 14 - conn: mist.WebsocketConnection, 15 - message_id: uuid.Uuid, 16 - account_id: String, 17 - ) -> Result(mist.Next(state.State, _msg), String) { 18 - { 19 - let assert Ok(acc) = sql.create_account(state.db(st), account_id) 20 - use acc <- rows.one_or_none(acc) 21 - let assert Ok(account_id) = case acc { 22 - option.Some(acc) -> Ok(acc.id) 23 - option.None -> { 24 - let assert Ok(acc) = sql.get_account(state.db(st), account_id) 25 - use acc <- rows.one(acc) 26 - Ok(acc.id) 27 - } 28 - } 29 - 30 - let message = 31 - account.Account(id: account_id) 32 - |> response.Authenticated() 33 - |> response.message(message_id, 0) 34 - |> response.to_string() 35 - use _ <- result.map( 36 - mist.send_text_frame(conn, message) 37 - |> result.map_error(rows.HandlerError), 38 - ) 39 - state.authenticate(st, account_id) 40 - |> mist.continue() 41 - |> Ok() 42 - } 43 - |> result.map_error(string.inspect) 44 - |> result.flatten() 45 - }
-44
server/src/handlers/debug_add_card_handler.gleam
··· 1 - import bus 2 - import cartography_api/game_state 3 - import db/rows 4 - import db/sql 5 - import gleam/result 6 - import gleam/string 7 - import mist 8 - import websocket/state 9 - 10 - pub fn handle( 11 - st: state.State, 12 - card_type_id: game_state.CardTypeId, 13 - ) -> Result(mist.Next(state.State, _msg), String) { 14 - use account_id <- state.account_id(st) 15 - { 16 - let assert Ok(card_type) = 17 - state.db(st) 18 - |> sql.get_card_type(card_type_id.id) 19 - use card_type <- rows.one(card_type) 20 - 21 - use card_id <- result.try(case card_type.class { 22 - sql.Citizen -> { 23 - let assert Ok(citizen) = 24 - state.db(st) 25 - |> sql.create_citizen(account_id, card_type.id) 26 - use citizen <- rows.one(citizen) 27 - Ok(game_state.CitizenId(citizen.card_id)) 28 - } 29 - sql.Tile -> { 30 - let assert Ok(tile) = 31 - state.db(st) 32 - |> sql.create_tile(account_id, card_type.id) 33 - use tile <- rows.one(tile) 34 - Ok(game_state.TileId(tile.card_id)) 35 - } 36 - }) 37 - 38 - state.bus(st) 39 - |> bus.notify_card_account(account_id, card_id) 40 - 41 - Ok(mist.continue(st)) 42 - } 43 - |> result.map_error(string.inspect) 44 - }
-33
server/src/handlers/list_fields_handler.gleam
··· 1 - import cartography_api/field 2 - import cartography_api/game_state 3 - import cartography_api/response 4 - import db/sql 5 - import gleam/list 6 - import gleam/result 7 - import gleam/string 8 - import mist 9 - import websocket/state 10 - import youid/uuid 11 - 12 - pub fn handle( 13 - st: state.State, 14 - conn: mist.WebsocketConnection, 15 - message_id: uuid.Uuid, 16 - ) -> Result(mist.Next(state.State, _msg), String) { 17 - use account_id <- state.account_id(st) 18 - { 19 - let assert Ok(result) = 20 - sql.list_fields_for_account(state.db(st), account_id) 21 - let message = 22 - result.rows 23 - |> list.map(fn(row) { 24 - field.Field(id: game_state.FieldId(row.id), name: row.name) 25 - }) 26 - |> response.Fields() 27 - |> response.message(message_id, 0) 28 - |> response.to_string() 29 - use _ <- result.try(mist.send_text_frame(conn, message)) 30 - Ok(mist.continue(st)) 31 - } 32 - |> result.map_error(string.inspect) 33 - }
-12
server/src/handlers/unsubscribe_handler.gleam
··· 1 - import mist 2 - import websocket/state 3 - import youid/uuid 4 - 5 - pub fn handle( 6 - st: state.State, 7 - message_id: uuid.Uuid, 8 - ) -> Result(mist.Next(state.State, _msg), String) { 9 - state.unsubscribe(st, message_id) 10 - |> mist.continue() 11 - |> Ok() 12 - }
-31
server/src/handlers/watch_field_handler.gleam
··· 1 - import actor/game_state_watcher 2 - import cartography_api/game_state 3 - import gleam/erlang/process 4 - import gleam/result 5 - import gleam/string 6 - import mist 7 - import websocket/state 8 - import youid/uuid 9 - 10 - pub fn handle( 11 - st: state.State, 12 - conn: mist.WebsocketConnection, 13 - message_id: uuid.Uuid, 14 - field_id: game_state.FieldId, 15 - ) -> Result(mist.Next(state.State, _msg), String) { 16 - use account_id <- state.account_id(st) 17 - { 18 - use field_watcher <- result.try(state.start_game_state_watcher( 19 - st, 20 - game_state_watcher.Init(conn:, message_id:, account_id:, field_id:), 21 - )) 22 - 23 - st 24 - |> state.add_subscription(message_id, fn() { 25 - process.send(field_watcher.data, game_state_watcher.Stop) 26 - }) 27 - |> mist.continue() 28 - |> Ok() 29 - } 30 - |> result.map_error(string.inspect) 31 - }
-135
server/src/json_websocket.gleam
··· 1 - import gleam/dynamic/decode 2 - import gleam/http/request 3 - import gleam/json 4 - import gleam/option 5 - import gleam/result 6 - import gleam/string 7 - import gleam/time/calendar 8 - import gleam/time/duration 9 - import gleam/time/timestamp 10 - import mist.{type WebsocketConnection, type WebsocketMessage, Text} 11 - import palabres 12 - 13 - fn as_text(message: WebsocketMessage(_msg)) { 14 - case message { 15 - Text(text) -> Ok(text) 16 - _ -> Error("only text messages are supported") 17 - } 18 - } 19 - 20 - fn parse_message( 21 - text: String, 22 - decoder: decode.Decoder(event), 23 - cb: fn(event) -> Result(t, String), 24 - ) -> Result(t, String) { 25 - let before = timestamp.system_time() 26 - 27 - let parsed = 28 - json.parse(from: text, using: decoder) 29 - |> result.map_error(string.inspect) 30 - 31 - case parsed { 32 - Ok(message) -> { 33 - palabres.info("message received") 34 - |> palabres.string( 35 - "timestamp", 36 - timestamp.to_rfc3339(before, calendar.utc_offset), 37 - ) 38 - |> palabres.string("socket_message", string.inspect(message)) 39 - |> palabres.log() 40 - 41 - let result = cb(message) 42 - 43 - let after = timestamp.system_time() 44 - let elapsed = timestamp.difference(before, after) 45 - palabres.info("message handled") 46 - |> palabres.string( 47 - "timestamp", 48 - timestamp.to_rfc3339(before, calendar.utc_offset), 49 - ) 50 - |> palabres.string("duration", duration.to_iso8601_string(elapsed)) 51 - |> palabres.string("socket_message", string.inspect(message)) 52 - |> palabres.log() 53 - 54 - result 55 - } 56 - Error(error) -> { 57 - let after = timestamp.system_time() 58 - let elapsed = timestamp.difference(before, after) 59 - palabres.info("invalid message not handled") 60 - |> palabres.string( 61 - "timestamp", 62 - timestamp.to_rfc3339(before, calendar.utc_offset), 63 - ) 64 - |> palabres.string("duration", duration.to_iso8601_string(elapsed)) 65 - |> palabres.string("socket_message", text) 66 - |> palabres.log() 67 - 68 - Error(error) 69 - } 70 - } 71 - } 72 - 73 - pub opaque type Builder(state, event) { 74 - Builder( 75 - init: state, 76 - decoder: decode.Decoder(event), 77 - on_message: fn(state, event, WebsocketConnection) -> mist.Next(state, event), 78 - ) 79 - } 80 - 81 - pub fn new(init: state) -> Builder(state, Nil) { 82 - Builder(init:, decoder: decode.success(Nil), on_message: fn(state, _, _) { 83 - mist.continue(state) 84 - }) 85 - } 86 - 87 - pub fn message( 88 - builder: Builder(state, _msg), 89 - decoder: decode.Decoder(event), 90 - on_message: fn(state, event, WebsocketConnection) -> mist.Next(state, event), 91 - ) -> Builder(state, event) { 92 - Builder(..builder, decoder:, on_message:) 93 - } 94 - 95 - pub fn start( 96 - builder: Builder(state, event), 97 - request: request.Request(mist.Connection), 98 - ) { 99 - palabres.info("websocket connection received") 100 - |> palabres.string( 101 - "timestamp", 102 - timestamp.to_rfc3339(timestamp.system_time(), calendar.utc_offset), 103 - ) 104 - |> palabres.log() 105 - 106 - mist.websocket( 107 - request: request, 108 - handler: fn(state, message, conn) { 109 - let response = { 110 - use text <- result.try(as_text(message)) 111 - use message <- parse_message(text, builder.decoder) 112 - Ok(builder.on_message(state, message, conn)) 113 - } 114 - case response { 115 - Ok(next) -> next 116 - Error(error) -> { 117 - palabres.error("websocket handler failed") 118 - |> palabres.string("error", error) 119 - |> palabres.log() 120 - mist.stop_abnormal(error) 121 - } 122 - } 123 - }, 124 - on_init: fn(_conn) { #(builder.init, option.None) }, 125 - on_close: fn(_state) { 126 - palabres.info("websocket connection closed") 127 - |> palabres.string( 128 - "timestamp", 129 - timestamp.to_rfc3339(timestamp.system_time(), calendar.utc_offset), 130 - ) 131 - |> palabres.log() 132 - Nil 133 - }, 134 - ) 135 - }
-200
server/src/pubsub.gleam
··· 1 - import gleam/dict.{type Dict} 2 - import gleam/erlang/process.{type Down, type Monitor, type Pid, type Subject} 3 - import gleam/option 4 - import gleam/otp/actor 5 - import gleam/otp/supervision 6 - import gleam/result 7 - import gleam/set.{type Set} 8 - import util 9 - 10 - pub opaque type Message(channel, message) { 11 - Subscribe(channel, Subject(message)) 12 - Unsubscribe(Subject(message)) 13 - Hangup(Down) 14 - Broadcast(channel, message) 15 - Stop 16 - } 17 - 18 - pub type PubSub(channel, message) 19 - 20 - type State(channel, message) { 21 - State( 22 - channels: Dict(channel, Set(Subject(message))), 23 - monitors: Dict(Pid, #(Monitor, Set(Subject(message)))), 24 - ) 25 - } 26 - 27 - pub fn start(config: Config(channel, message)) { 28 - actor.new_with_initialiser(10, fn(sub) { 29 - let selector = 30 - process.new_selector() 31 - |> process.select(sub) 32 - |> process.select_monitors(Hangup) 33 - 34 - actor.initialised(State(channels: dict.new(), monitors: dict.new())) 35 - |> actor.selecting(selector) 36 - |> actor.returning(sub) 37 - |> Ok() 38 - }) 39 - |> util.with_some(config.name, actor.named) 40 - |> actor.on_message(on_message) 41 - |> actor.start() 42 - } 43 - 44 - type Name(channel, message) = 45 - process.Name(Message(channel, message)) 46 - 47 - pub opaque type Config(channel, message) { 48 - Config(name: option.Option(Name(channel, message))) 49 - } 50 - 51 - pub fn supervised(config: Config(channel, message)) { 52 - supervision.supervisor(fn() { start(config) }) 53 - } 54 - 55 - pub fn new() { 56 - Config(name: option.None) 57 - } 58 - 59 - pub fn named(_config: Config(_c, _m), name: Name(channel, message)) { 60 - Config(name: option.Some(name)) 61 - } 62 - 63 - fn on_message( 64 - state: State(channel, message), 65 - message: Message(channel, message), 66 - ) -> actor.Next(State(channel, message), Message(channel, message)) { 67 - case message { 68 - Broadcast(channel, message) -> { 69 - dict.get(state.channels, channel) 70 - |> result.lazy_unwrap(set.new) 71 - |> set.each(process.send(_, message)) 72 - actor.continue(state) 73 - } 74 - Subscribe(channel, subject) -> 75 - handle_subscribe(state, channel, subject) 76 - |> actor.continue() 77 - Unsubscribe(subject) -> 78 - handle_unsubscribe(state, subject) 79 - |> actor.continue() 80 - Hangup(down) -> 81 - handle_hangup(state, down) 82 - |> actor.continue() 83 - Stop -> actor.stop() 84 - } 85 - } 86 - 87 - fn add_listener( 88 - state: State(channel, message), 89 - channel: channel, 90 - subject: Subject(message), 91 - ) { 92 - let channels = 93 - state.channels 94 - |> dict.get(channel) 95 - |> result.lazy_unwrap(set.new) 96 - |> set.insert(subject) 97 - |> dict.insert(state.channels, channel, _) 98 - State(..state, channels:) 99 - } 100 - 101 - fn add_monitor(state: State(channel, message), subject: Subject(message)) { 102 - let assert Ok(pid) = process.subject_owner(subject) 103 - let monitors = case dict.get(state.monitors, pid) { 104 - Ok(#(monitor, subjects)) -> 105 - dict.insert(state.monitors, pid, #(monitor, set.insert(subjects, subject))) 106 - Error(Nil) -> 107 - dict.insert(state.monitors, pid, #( 108 - process.monitor(pid), 109 - set.new() |> set.insert(subject), 110 - )) 111 - } 112 - State(..state, monitors:) 113 - } 114 - 115 - fn handle_subscribe( 116 - state: State(channel, message), 117 - channel: channel, 118 - subject: Subject(message), 119 - ) -> State(channel, message) { 120 - state 121 - |> add_listener(channel, subject) 122 - |> add_monitor(subject) 123 - } 124 - 125 - fn remove_listener(state: State(channel, message), subject: Subject(message)) { 126 - let channels = 127 - dict.map_values(state.channels, fn(_, subs) { set.delete(subs, subject) }) 128 - State(..state, channels:) 129 - } 130 - 131 - fn remove_subject(state: State(channel, message), subject: Subject(message)) { 132 - let assert Ok(pid) = process.subject_owner(subject) 133 - { 134 - use #(monitor, subjects) <- result.map(dict.get(state.monitors, pid)) 135 - let subjects = set.delete(subjects, subject) 136 - let monitors = case set.is_empty(subjects) { 137 - True -> { 138 - process.demonitor_process(monitor) 139 - dict.delete(state.monitors, pid) 140 - } 141 - False -> dict.insert(state.monitors, pid, #(monitor, subjects)) 142 - } 143 - State(..state, monitors:) 144 - } 145 - |> result.unwrap(state) 146 - } 147 - 148 - fn handle_unsubscribe( 149 - state: State(channel, message), 150 - subject: Subject(message), 151 - ) -> State(channel, message) { 152 - state 153 - |> remove_listener(subject) 154 - |> remove_subject(subject) 155 - } 156 - 157 - fn remove_monitor(state: State(channel, message), pid: Pid) { 158 - State(..state, monitors: dict.delete(state.monitors, pid)) 159 - } 160 - 161 - fn handle_hangup( 162 - state: State(channel, message), 163 - down: Down, 164 - ) -> State(channel, message) { 165 - case down { 166 - process.ProcessDown(monitor, pid, _reason) -> { 167 - process.demonitor_process(monitor) 168 - let assert Ok(#(_, subjects)) = dict.get(state.monitors, pid) 169 - subjects 170 - |> set.fold(state, remove_listener) 171 - |> remove_monitor(pid) 172 - } 173 - process.PortDown(..) -> panic as "unreachable" 174 - } 175 - } 176 - 177 - pub fn broadcast( 178 - pubsub: Subject(Message(channel, message)), 179 - channel: channel, 180 - message: message, 181 - ) { 182 - process.send(pubsub, Broadcast(channel, message)) 183 - } 184 - 185 - pub fn subscribe(pubsub: Subject(Message(channel, message)), channel: channel) { 186 - let subject = process.new_subject() 187 - process.send(pubsub, Subscribe(channel, subject)) 188 - subject 189 - } 190 - 191 - pub fn unsubscribe( 192 - pubsub: Subject(Message(channel, message)), 193 - subject: Subject(message), 194 - ) { 195 - process.send(pubsub, Unsubscribe(subject)) 196 - } 197 - 198 - pub fn stop(pubsub: Subject(Message(channel, message))) { 199 - process.send(pubsub, Stop) 200 - }
-59
server/src/server.gleam
··· 1 - import actor/game_state_watcher 2 - import bus 3 - import envoy 4 - import gleam/erlang/process 5 - import gleam/int 6 - import gleam/otp/factory_supervisor 7 - import gleam/otp/static_supervisor 8 - import gleam/result 9 - import mist 10 - import palabres 11 - import palabres/options 12 - import pog 13 - import server/context 14 - import server/router 15 - 16 - pub fn main() -> Nil { 17 - options.defaults() 18 - |> options.color(True) 19 - |> options.json(True) 20 - |> options.output(to: options.stdout()) 21 - |> palabres.configure() 22 - 23 - let port = 24 - envoy.get("PORT") 25 - |> result.try(int.parse) 26 - |> result.unwrap(12_000) 27 - 28 - let assert Ok(database_url) = envoy.get("DATABASE_URL") 29 - let db = process.new_name("database") 30 - let assert Ok(db_config) = pog.url_config(db, database_url) 31 - let database = 32 - db_config 33 - |> pog.pool_size(10) 34 - |> pog.supervised() 35 - 36 - let #(bus_process, bus) = bus.supervised() 37 - 38 - let game_state_watchers = process.new_name("game_state_watchers") 39 - let factory = 40 - factory_supervisor.worker_child(game_state_watcher.start(db, bus, _)) 41 - |> factory_supervisor.named(game_state_watchers) 42 - |> factory_supervisor.supervised() 43 - 44 - let context = context.Context(db:, bus:, game_state_watchers:) 45 - let server = 46 - mist.new(router.handler(_, context)) 47 - |> mist.port(port) 48 - |> mist.supervised() 49 - 50 - let assert Ok(_) = 51 - static_supervisor.new(static_supervisor.OneForOne) 52 - |> static_supervisor.add(database) 53 - |> static_supervisor.add(bus_process) 54 - |> static_supervisor.add(factory) 55 - |> static_supervisor.add(server) 56 - |> static_supervisor.start() 57 - 58 - process.sleep_forever() 59 - }
-28
server/src/server/context.gleam
··· 1 - import actor/game_state_watcher 2 - import bus 3 - import gleam/erlang/process.{type Name, type Subject} 4 - import gleam/otp/factory_supervisor 5 - import pog 6 - 7 - pub type Context { 8 - Context( 9 - db: Name(pog.Message), 10 - bus: bus.Bus, 11 - game_state_watchers: Name( 12 - factory_supervisor.Message( 13 - game_state_watcher.Init, 14 - Subject(game_state_watcher.Message), 15 - ), 16 - ), 17 - ) 18 - } 19 - 20 - pub fn db(ctx: Context) -> pog.Connection { 21 - pog.named_connection(ctx.db) 22 - } 23 - 24 - pub fn start_game_state_watcher(ctx: Context, init: game_state_watcher.Init) { 25 - ctx.game_state_watchers 26 - |> factory_supervisor.get_by_name() 27 - |> factory_supervisor.start_child(init) 28 - }
-22
server/src/server/router.gleam
··· 1 - import gleam/bytes_tree 2 - import gleam/http/request 3 - import gleam/http/response 4 - import mist 5 - import server/context.{type Context} 6 - import websocket/handler 7 - 8 - pub fn handler(req: request.Request(mist.Connection), context: Context) { 9 - case request.path_segments(req) { 10 - ["websocket"] -> { 11 - case handler.start(req, context) { 12 - Ok(response) -> response 13 - Error(_) -> 14 - response.new(400) 15 - |> response.set_body(mist.Bytes(bytes_tree.new())) 16 - } 17 - } 18 - _ -> 19 - response.new(404) 20 - |> response.set_body(mist.Bytes(bytes_tree.new())) 21 - } 22 - }
-8
server/src/util.gleam
··· 1 - import gleam/option 2 - 3 - pub fn with_some(v: v, option: option.Option(t), do: fn(v, t) -> v) -> v { 4 - case option { 5 - option.Some(t) -> do(v, t) 6 - option.None -> v 7 - } 8 - }
-78
server/src/websocket/handler.gleam
··· 1 - import cartography_api/request 2 - import envoy 3 - import gleam/http/request as http 4 - import gleam/http/response 5 - import gleam/list 6 - import gleam/result 7 - import gleam/string 8 - import handlers/authenticate_handler 9 - import handlers/debug_add_card_handler 10 - import handlers/list_fields_handler 11 - import handlers/unsubscribe_handler 12 - import handlers/watch_field_handler 13 - import json_websocket 14 - import mist.{type WebsocketConnection} 15 - import palabres 16 - import server/context.{type Context} 17 - import websocket/state 18 - 19 - fn handle_message( 20 - state: state.State, 21 - message: request.Message, 22 - conn: WebsocketConnection, 23 - ) -> mist.Next(state.State, _msg) { 24 - let response = { 25 - case message.request { 26 - request.Authenticate(id) -> 27 - authenticate_handler.handle(state, conn, message.id, id) 28 - request.ListFields -> list_fields_handler.handle(state, conn, message.id) 29 - request.WatchField(field_id) -> 30 - watch_field_handler.handle(state, conn, message.id, field_id) 31 - request.DebugAddCard(card_id) -> 32 - debug_add_card_handler.handle(state, card_id) 33 - request.Unsubscribe -> unsubscribe_handler.handle(state, message.id) 34 - } 35 - } 36 - case response { 37 - Ok(next) -> next 38 - Error(error) -> { 39 - palabres.error("websocket handler failed") 40 - |> palabres.string("error", error) 41 - |> palabres.log() 42 - mist.stop_abnormal(error) 43 - } 44 - } 45 - } 46 - 47 - pub fn start(request: http.Request(mist.Connection), context: Context) { 48 - use protocol <- result.try(http.get_header(request, "sec-websocket-protocol")) 49 - start_with_protocol(string.split(protocol, on: ","), request, context) 50 - } 51 - 52 - fn start_with_protocol( 53 - protocol: List(String), 54 - request: http.Request(mist.Connection), 55 - context: Context, 56 - ) { 57 - let supported = 58 - envoy.get("WEBSOCKET_PROTOCOLS") 59 - |> result.unwrap("json") 60 - |> string.split(on: ",") 61 - 62 - let allow_json = list.contains(supported, "json") 63 - 64 - case protocol { 65 - [] -> Error(Nil) 66 - ["v1-json.cartography.app", ..] if allow_json -> 67 - state.new(context) 68 - |> json_websocket.new() 69 - |> json_websocket.message(request.decoder(), handle_message) 70 - |> json_websocket.start(request) 71 - |> response.set_header( 72 - "sec-websocket-protocol", 73 - "v1-json.cartography.app", 74 - ) 75 - |> Ok() 76 - [_, ..rest] -> start_with_protocol(rest, request, context) 77 - } 78 - }
-61
server/src/websocket/state.gleam
··· 1 - import actor/game_state_watcher 2 - import bus 3 - import gleam/dict.{type Dict} 4 - import gleam/option.{type Option} 5 - import pog 6 - import server/context.{type Context} 7 - import youid/uuid.{type Uuid} 8 - 9 - pub opaque type State { 10 - State( 11 - context: Context, 12 - account_id: Option(String), 13 - subscriptions: Dict(Uuid, fn() -> Nil), 14 - ) 15 - } 16 - 17 - pub fn new(context: context.Context) -> State { 18 - State(context:, account_id: option.None, subscriptions: dict.new()) 19 - } 20 - 21 - pub fn account_id( 22 - state: State, 23 - with_account_id: fn(String) -> Result(t, String), 24 - ) -> Result(t, String) { 25 - case state.account_id { 26 - option.Some(id) -> with_account_id(id) 27 - option.None -> Error("socket not authenticated") 28 - } 29 - } 30 - 31 - pub fn authenticate(state: State, account_id: String) -> State { 32 - State(..state, account_id: option.Some(account_id)) 33 - } 34 - 35 - pub fn db(state: State) -> pog.Connection { 36 - context.db(state.context) 37 - } 38 - 39 - pub fn bus(state: State) -> bus.Bus { 40 - state.context.bus 41 - } 42 - 43 - pub fn add_subscription(state: State, id: Uuid, subscription: fn() -> Nil) { 44 - State( 45 - ..state, 46 - subscriptions: dict.insert(state.subscriptions, id, subscription), 47 - ) 48 - } 49 - 50 - pub fn unsubscribe(state: State, id: Uuid) { 51 - case dict.get(state.subscriptions, id) { 52 - Ok(sub) -> sub() 53 - Error(_) -> Nil 54 - } 55 - State(..state, subscriptions: dict.delete(state.subscriptions, id)) 56 - } 57 - 58 - pub fn start_game_state_watcher(state: State, init: game_state_watcher.Init) { 59 - state.context 60 - |> context.start_game_state_watcher(init) 61 - }
-63
server/test/pubsub_test.gleam
··· 1 - import gleam/erlang/process 2 - import pubsub 3 - 4 - type Channel { 5 - One 6 - Two 7 - } 8 - 9 - pub fn pubsub_received_test() { 10 - let assert Ok(pubsub_actor) = pubsub.start(pubsub.new()) 11 - let pubsub_subject = pubsub_actor.data 12 - 13 - let subscription = pubsub.subscribe(pubsub_subject, One) 14 - 15 - pubsub.broadcast(pubsub_subject, One, 1) 16 - pubsub.broadcast(pubsub_subject, Two, 2) 17 - 18 - let assert Ok(1) = process.receive(subscription, 1) 19 - let assert Error(_) = process.receive(subscription, 1) 20 - 21 - pubsub.stop(pubsub_subject) 22 - } 23 - 24 - pub fn pubsub_multiple_test() { 25 - let assert Ok(pubsub_actor) = pubsub.start(pubsub.new()) 26 - let pubsub_subject = pubsub_actor.data 27 - 28 - let subscription_1 = pubsub.subscribe(pubsub_subject, One) 29 - let subscription_2 = pubsub.subscribe(pubsub_subject, One) 30 - 31 - pubsub.broadcast(pubsub_subject, One, 1) 32 - 33 - let assert Ok(1) = process.receive(subscription_1, 1) 34 - let assert Ok(1) = process.receive(subscription_2, 1) 35 - 36 - pubsub.stop(pubsub_subject) 37 - } 38 - 39 - pub fn pubsub_unsubscribe_test() { 40 - let assert Ok(pubsub_actor) = pubsub.start(pubsub.new()) 41 - let pubsub_subject = pubsub_actor.data 42 - 43 - let subscription = pubsub.subscribe(pubsub_subject, One) 44 - pubsub.broadcast(pubsub_subject, One, 1) 45 - pubsub.unsubscribe(pubsub_subject, subscription) 46 - pubsub.broadcast(pubsub_subject, One, 2) 47 - 48 - let assert Ok(1) = process.receive(subscription, 1) 49 - let assert Error(_) = process.receive(subscription, 1) 50 - 51 - pubsub.stop(pubsub_subject) 52 - } 53 - 54 - pub fn pubsub_reunsubscribe_test() { 55 - let assert Ok(pubsub_actor) = pubsub.start(pubsub.new()) 56 - let pubsub_subject = pubsub_actor.data 57 - 58 - let subscription = pubsub.subscribe(pubsub_subject, One) 59 - pubsub.unsubscribe(pubsub_subject, subscription) 60 - pubsub.unsubscribe(pubsub_subject, subscription) 61 - 62 - pubsub.stop(pubsub_subject) 63 - }
-5
server/test/server_test.gleam
··· 1 - import gleeunit 2 - 3 - pub fn main() -> Nil { 4 - gleeunit.main() 5 - }
+2 -5
src/actor/player_socket/mod.rs
··· 1 1 use crate::dto::{Account, Field}; 2 2 use serde::{Deserialize, Serialize}; 3 - use ts_rs::TS; 4 3 5 - #[derive(Serialize, Deserialize, Clone, Debug, TS)] 6 - #[ts(export)] 4 + #[derive(Serialize, Deserialize, Clone, Debug)] 7 5 #[serde(tag = "type", content = "data")] 8 6 pub enum Request { 9 7 Authenticate(String), 10 8 ListFields, 11 9 } 12 10 13 - #[derive(Serialize, Deserialize, Clone, Debug, TS)] 14 - #[ts(export)] 11 + #[derive(Serialize, Deserialize, Clone, Debug)] 15 12 #[serde(tag = "type", content = "data")] 16 13 pub enum Response { 17 14 Authenticated(Account),
+1 -3
src/api/ws.rs
··· 6 6 use serde::{Deserialize, Serialize}; 7 7 use sqlx::PgPool; 8 8 use tracing::Instrument; 9 - use ts_rs::TS; 10 9 use uuid::Uuid; 11 10 12 - #[derive(Serialize, Deserialize, Clone, Debug, TS)] 13 - #[ts(export)] 11 + #[derive(Serialize, Deserialize, Clone, Debug)] 14 12 pub struct ProtocolV1Message<T> { 15 13 pub id: Uuid, 16 14 #[serde(flatten)]
+2 -3
src/db.rs
··· 1 1 use serde::{Deserialize, Serialize}; 2 - use ts_rs::TS; 3 2 use utoipa::ToSchema; 4 3 5 4 #[derive(sqlx::Type)] 6 5 #[sqlx(type_name = "card_class", rename_all = "lowercase")] 7 - #[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, Debug, ToSchema, TS)] 6 + #[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, Debug, ToSchema)] 8 7 pub enum CardClass { 9 8 Tile, 10 9 Citizen, ··· 12 11 13 12 #[derive(sqlx::Type)] 14 13 #[sqlx(type_name = "tile_category", rename_all = "lowercase")] 15 - #[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, Debug, ToSchema, TS)] 14 + #[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, Debug, ToSchema)] 16 15 pub enum TileCategory { 17 16 Residential, 18 17 Production,
+5 -6
src/dto/mod.rs
··· 1 1 use crate::db::TileCategory; 2 2 use serde::{Deserialize, Serialize}; 3 - use ts_rs::TS; 4 3 use utoipa::ToSchema; 5 4 6 - #[derive(Serialize, Deserialize, PartialEq, Clone, Debug, ToSchema, TS)] 5 + #[derive(Serialize, Deserialize, PartialEq, Clone, Debug, ToSchema)] 7 6 pub struct Account { 8 7 pub id: String, 9 8 } 10 9 11 - #[derive(Serialize, Deserialize, PartialEq, Clone, Debug, ToSchema, TS)] 10 + #[derive(Serialize, Deserialize, PartialEq, Clone, Debug, ToSchema)] 12 11 pub struct Field { 13 12 pub id: i64, 14 13 pub name: String, 15 14 } 16 15 17 - #[derive(Serialize, Deserialize, PartialEq, Clone, Debug, ToSchema, TS)] 16 + #[derive(Serialize, Deserialize, PartialEq, Clone, Debug, ToSchema)] 18 17 #[serde(tag = "class")] 19 18 pub enum CardType { 20 19 Tile(TileType), 21 20 Citizen(Species), 22 21 } 23 22 24 - #[derive(Serialize, Deserialize, PartialEq, Clone, Debug, ToSchema, TS)] 23 + #[derive(Serialize, Deserialize, PartialEq, Clone, Debug, ToSchema)] 25 24 pub struct TileType { 26 25 pub id: String, 27 26 pub card_set_id: String, ··· 30 29 pub employs: i32, 31 30 } 32 31 33 - #[derive(Serialize, Deserialize, PartialEq, Clone, Debug, ToSchema, TS)] 32 + #[derive(Serialize, Deserialize, PartialEq, Clone, Debug, ToSchema)] 34 33 pub struct Species { 35 34 pub id: String, 36 35 pub card_set_id: String,