Type-safe GraphQL client generator for Gleam

sans-io

Changed files
+470 -3078
birdie_snapshots
example
examples
src
+1
.gitignore
··· 2 2 *.ez 3 3 build/ 4 4 erl_crash.dump 5 + .claude
-331
CLAUDE.md
··· 1 - # Claude Context: Squall 2 - 3 - ## Project Overview 4 - 5 - **Squall** is a type-safe GraphQL client generator for Gleam, inspired by [squirrel](https://github.com/giacomocavalieri/squirrel). It follows a convention-over-configuration approach where developers write `.gql` files in `src/**/graphql/` directories, and Squall generates fully type-safe Gleam code with proper types and JSON decoders. 6 - 7 - ## Architecture 8 - 9 - ### Core Philosophy 10 - 11 - 1. **Convention over Configuration**: No config files, just drop `.gql` files in the right place 12 - 2. **Type Safety First**: All generated code is fully typed based on GraphQL schema 13 - 3. **Isomorphic by Design**: Generated client code works on any Gleam target (Erlang, JavaScript/Browser, JavaScript/Node) 14 - 4. **Test-Driven Development**: Every feature has tests written first 15 - 5. **Modular Design**: Clear separation of concerns across modules 16 - 17 - ### Module Breakdown 18 - 19 - ``` 20 - src/squall/ 21 - ├── squall.gleam # Public API & Client type 22 - ├── adapter.gleam # HTTP adapter interface 23 - ├── adapter/ 24 - │ ├── erlang.gleam # Erlang HTTP adapter (uses gleam_httpc) 25 - │ └── javascript.gleam # JavaScript HTTP adapter (uses Fetch API) 26 - └── internal/ 27 - ├── error.gleam # Comprehensive error types 28 - ├── discovery.gleam # File discovery and validation 29 - ├── parser.gleam # GraphQL lexer + recursive descent parser 30 - ├── schema.gleam # GraphQL schema introspection 31 - ├── type_mapping.gleam # GraphQL to Gleam type conversion 32 - └── codegen.gleam # Code generation with glam 33 - ``` 34 - 35 - ### Data Flow 36 - 37 - ``` 38 - .gql files → discovery → parser → type_mapping → codegen → .gleam files 39 - 40 - schema (from endpoint introspection) 41 - ``` 42 - 43 - ## Key Design Patterns 44 - 45 - ### 1. Error Handling 46 - 47 - All modules use a central `Error` type in `error.gleam`. Errors are: 48 - - Descriptive with file paths and line numbers where applicable 49 - - Converted to strings via `error.to_string()` for CLI output 50 - - Propagated using `Result(T, Error)` 51 - 52 - ### 2. Type Safety 53 - 54 - GraphQL types map to Gleam types: 55 - - `String`, `Int`, `Float`, `Boolean`, `ID` → Built-in types 56 - - `Type` (nullable) → `Option(Type)` 57 - - `Type!` (non-null) → `Type` 58 - - `[Type]` → `List(Type)` 59 - - Custom objects → Custom Gleam types 60 - 61 - ### 3. HTTP Adapter Pattern 62 - 63 - **Why Adapters?** 64 - To make generated code isomorphic (work across Gleam targets), Squall uses an adapter pattern for HTTP requests: 65 - 66 - - **`squall.Client`** contains a `send_request: HttpAdapter` field 67 - - **`HttpAdapter`** is a function type: `fn(Request(String)) -> Result(Response(String), String)` 68 - - **Platform-specific adapters**: 69 - - `squall/adapter/erlang.gleam` - Uses `gleam_httpc` for Erlang target 70 - - `squall/adapter/javascript.gleam` - Uses Fetch API for JavaScript targets (browser/Node) 71 - - **Generated code** calls `client.send_request(req)` instead of directly using `httpc` 72 - 73 - **Usage Example:** 74 - ```gleam 75 - // Erlang target 76 - import squall 77 - let client = squall.new_erlang_client("https://api.example.com/graphql", []) 78 - 79 - // JavaScript target 80 - import squall 81 - let client = squall.new_javascript_client("https://api.example.com/graphql", []) 82 - 83 - // Generic (with explicit adapter) 84 - import squall 85 - import squall/adapter/erlang 86 - let client = squall.new_client("https://api.example.com/graphql", [], erlang.adapter()) 87 - ``` 88 - 89 - ### 4. Testing Strategy 90 - 91 - **TDD Approach:** 92 - 1. Write test first (in `test/` directory) 93 - 2. Run test (it should fail) 94 - 3. Implement feature 95 - 4. Run test (it should pass) 96 - 5. Refactor if needed 97 - 98 - **Test Types:** 99 - - **Unit tests**: Each module has its own test file 100 - - **Snapshot tests**: Using `birdie` for code generation validation 101 - - **Integration tests**: Test file embedded in `test/fixtures/graphql/` 102 - 103 - ### 5. Code Generation 104 - 105 - Uses the `glam` library for pretty-printing. Generated code includes: 106 - - Type definitions (custom types for responses and inputs) 107 - - JSON decoders (using `gleam/dynamic`) 108 - - JSON serializers (for converting types back to JSON) 109 - - HTTP client functions (using `squall.Client` with adapter pattern) 110 - 111 - **Key Feature**: Generated code does NOT directly import `gleam_httpc` or any platform-specific HTTP library. Instead, it uses `client.send_request()`, making it work across all Gleam targets. 112 - 113 - ## File Conventions 114 - 115 - ### GraphQL Files 116 - 117 - - **Location**: `src/**/graphql/*.gql` 118 - - **Naming**: Filename becomes function name (must be valid Gleam identifier) 119 - - ✅ `get_user.gql` → `get_user()` 120 - - ✅ `create_post.gql` → `create_post()` 121 - - ❌ `get-user.gql` (hyphens not allowed) 122 - - ❌ `123user.gql` (can't start with number) 123 - 124 - ### Generated Files 125 - 126 - - **Output**: Same directory as `.gql` file, same name with `.gleam` extension 127 - - **Pattern**: `src/**/graphql/operation_name.gql` → `src/**/graphql/operation_name.gleam` 128 - 129 - ## Development Workflow 130 - 131 - ### Adding a New Feature 132 - 133 - 1. **Write test first** in appropriate test file 134 - 2. **Run tests**: `gleam test` (should fail) 135 - 3. **Implement feature** in appropriate module 136 - 4. **Run tests**: `gleam test` (should pass) 137 - 5. **Update snapshots if needed**: `BIRDIE_ACCEPT=1 gleam test` 138 - 139 - ### Testing 140 - 141 - ```bash 142 - # Run all tests 143 - gleam test 144 - 145 - # Accept birdie snapshots 146 - BIRDIE_ACCEPT=1 gleam test 147 - 148 - # Build the project 149 - gleam build 150 - 151 - # Run CLI 152 - gleam run -m squall generate <endpoint> 153 - ``` 154 - 155 - ## Important Implementation Details 156 - 157 - ### Parser (`parser.gleam`) 158 - 159 - - **Two-phase**: Lexer (tokenization) → Parser (AST building) 160 - - **Recursive descent**: Standard parsing technique 161 - - **AST Types**: `Operation`, `Selection`, `TypeRef`, `Variable`, `Value` 162 - - **Handles**: Queries, Mutations, Subscriptions, Variables, Arguments, Nested selections 163 - 164 - ### Schema (`schema.gleam`) 165 - 166 - - **Introspection**: Parses GraphQL introspection JSON 167 - - **Types**: `Type`, `Field`, `InputValue`, `TypeRef` 168 - - **Schema**: Contains query/mutation/subscription types + all schema types 169 - - **Validation**: Checks for required fields, proper structure 170 - 171 - ### Type Mapping (`type_mapping.gleam`) 172 - 173 - - **GleamType**: Internal representation of Gleam types 174 - - **Conversion**: GraphQL TypeRef → GleamType 175 - - **Nullable handling**: Automatically wraps nullable types in `Option` 176 - - **Lists**: Properly handles `[Type]`, `[Type!]`, `[Type]!`, `[Type!]!` 177 - 178 - ### Code Generation (`codegen.gleam`) 179 - 180 - - **Response Types**: Generated for each operation 181 - - **Decoders**: JSON decoders using `gleam/dynamic` 182 - - **Functions**: Type-safe functions with proper parameters 183 - - **Imports**: Automatically includes required imports 184 - 185 - ## Common Tasks 186 - 187 - ### Add Support for a New GraphQL Feature 188 - 189 - Example: Adding fragment support 190 - 191 - 1. **Update Parser**: 192 - - Add fragment tokens to lexer 193 - - Add fragment AST types 194 - - Add fragment parsing logic 195 - 196 - 2. **Update Code Generation**: 197 - - Handle fragments in selection sets 198 - - Generate fragment helper functions 199 - 200 - 3. **Write Tests**: 201 - ```gleam 202 - // In parser_test.gleam 203 - pub fn parse_fragment_test() { 204 - let source = "fragment UserFields on User { id name }" 205 - let result = parser.parse(source) 206 - should.be_ok(result) 207 - } 208 - ``` 209 - 210 - 4. **Add Snapshot Test**: 211 - ```gleam 212 - // In codegen_test.gleam 213 - pub fn generate_with_fragments_test() { 214 - // Setup and generate code 215 - code |> birdie.snap(title: "Query with fragments") 216 - } 217 - ``` 218 - 219 - ### Fix a Type Mapping Issue 220 - 221 - 1. **Locate the issue** in `type_mapping.gleam` 222 - 2. **Write a failing test** in `type_mapping_test.gleam` 223 - 3. **Fix the mapping** logic 224 - 4. **Verify test passes** 225 - 226 - ### Update Generated Code Format 227 - 228 - 1. **Modify** `codegen.gleam` 229 - 2. **Run tests**: They will fail with snapshot differences 230 - 3. **Review the diff** to ensure it's correct 231 - 4. **Accept snapshots**: `BIRDIE_ACCEPT=1 gleam test` 232 - 233 - ## Isomorphic Architecture Benefits 234 - 235 - ✅ **Achieved:** 236 - - Generated client code works on Erlang, JavaScript/Browser, and JavaScript/Node 237 - - No platform-specific imports in generated code 238 - - Users can easily create custom adapters for other HTTP libraries 239 - - Generator (CLI) remains Erlang-only (appropriate for build tools) 240 - 241 - 🎯 **How It Works:** 242 - 1. **Generation Time**: CLI runs on Erlang, uses `gleam_httpc` for schema introspection 243 - 2. **Runtime**: Generated code uses `squall.Client` with platform-specific adapter 244 - 3. **User Code**: Chooses adapter based on target (Erlang vs JavaScript) 245 - 246 - ## Known Limitations 247 - 248 - 1. **Fragments**: Not yet implemented 249 - 2. **Directives**: Not yet handled (@include, @skip, etc.) 250 - 3. **Custom Scalars**: Most map to String by default (except JSON which maps to `json.Json`) 251 - 4. **Introspection**: Doesn't handle deeply nested types beyond 5 levels 252 - 5. **JavaScript FFI**: Fetch adapter requires testing on actual JavaScript runtime 253 - 254 - ## Code Style 255 - 256 - ### Gleam Conventions 257 - 258 - - **Functions**: `snake_case` 259 - - **Types**: `PascalCase` 260 - - **Constants**: `snake_case` 261 - - **Pipes**: Use `|>` for chaining 262 - - **Pattern Matching**: Prefer `case` over nested `if` 263 - 264 - ### Project Conventions 265 - 266 - - **Naming**: Be descriptive, avoid abbreviations 267 - - **Comments**: Explain "why" not "what" 268 - - **Error Messages**: Include file paths and suggestions when possible 269 - - **Tests**: One test per feature, descriptive test names 270 - 271 - ## Troubleshooting 272 - 273 - ### Tests Not Running 274 - 275 - - Check `test/squall_test.gleam` - it manually calls all tests 276 - - Ensure test files don't have their own `main()` functions 277 - - Test functions must end with `_test` 278 - 279 - ### Snapshot Tests Failing 280 - 281 - - Run `BIRDIE_ACCEPT=1 gleam test` to accept new snapshots 282 - - Review `.new` files in `birdie_snapshots/` before accepting 283 - - Snapshots are in `birdie_snapshots/*.accepted` 284 - 285 - ### Parser Errors 286 - 287 - - Check token definitions in `parser.gleam` 288 - - Ensure lexer handles all GraphQL syntax 289 - - Test with simple queries first, then complex ones 290 - 291 - ## Future Improvements 292 - 293 - ### High Priority 294 - 295 - - [ ] Test JavaScript adapter on actual JavaScript runtime (browser & Node.js) 296 - - [ ] Add fragment support 297 - - [ ] Handle GraphQL directives 298 - - [ ] Custom scalar type mapping with user configuration 299 - 300 - ### Medium Priority 301 - 302 - - [ ] Multiple endpoint support 303 - - [ ] Watch mode for development 304 - - [ ] Better error messages with source code snippets 305 - - [ ] Cache schema introspection results 306 - - [ ] WebSocket adapter for subscriptions 307 - 308 - ### Low Priority 309 - 310 - - [ ] IDE integration 311 - - [ ] GraphQL validation against schema 312 - - [ ] Performance optimizations 313 - - [ ] Parallel file processing 314 - 315 - ## References 316 - 317 - - **Gleam**: https://gleam.run/ 318 - - **Squirrel**: https://github.com/giacomocavalieri/squirrel 319 - - **GraphQL**: https://graphql.org/ 320 - - **Rick and Morty API**: https://rickandmortyapi.com/graphql (used for testing) 321 - 322 - ## Questions? 323 - 324 - When working on this project: 325 - 326 - 1. **Follow TDD**: Always write tests first 327 - 2. **Check existing patterns**: Look at similar features for guidance 328 - 3. **Update tests**: If changing behavior, update snapshots 329 - 4. **Ask for context**: Use this document as a starting point 330 - 331 - Happy coding! 🌊
+67 -303
README.md
··· 1 - # 🌊 Squall 1 + # Squall 2 2 3 - A type-safe **isomorphic** GraphQL client generator for Gleam. 3 + A type-safe **sans-io** GraphQL client generator for Gleam. 4 4 5 - Squall parses `.gql` files in your project, introspects your GraphQL endpoint, and generates fully type-safe Gleam code that works on **both Erlang and JavaScript** targets. Write your queries once, run them anywhere! 5 + > This project is in early development and may contain bugs/unsupported GraphQL queries. 6 6 7 - > **⚠️ Warning**: This project is in early development and may contain bugs. Also hasn't been published yet. 7 + ## What is Sans-IO? 8 8 9 - ## Features 9 + Squall generates functions to **build HTTP requests** and **parse HTTP responses**, but doesn't send them. You control the HTTP layer. 10 10 11 - ✨ **Isomorphic** - Works on Erlang (backend) and JavaScript (browser/Node.js) 12 - - Type-safe code generation from GraphQL schema 13 - - Convention over configuration - `.gql` files in `src/**/graphql/` directories 14 - - Schema introspection from GraphQL endpoints 15 - - Supports queries, mutations, and subscriptions 16 - - Target-specific HTTP adapters (gleam_httpc for Erlang, Fetch API for JavaScript) 17 - - Client abstraction with authentication and custom headers 11 + **Benefits:** 12 + - Works on any Gleam target (Erlang, JavaScript, future targets) 13 + - Use any HTTP client you want 14 + - Easy to test without mocking 15 + - Full control over retries, timeouts, logging 18 16 19 17 ## Installation 20 18 21 - Add squall to your `gleam.toml`: 22 - 23 - ```toml 24 - [dependencies] 25 - squall = "0.1.0" 26 - 27 - # If using JavaScript target, also add: 28 - gleam_javascript = ">= 0.3.0 and < 2.0.0" 19 + ```bash 20 + gleam add squall 29 21 ``` 30 22 31 - The `gleam_javascript` dependency provides Promise support needed for async operations on the JavaScript target. 32 - 33 23 ## Quick Start 34 24 35 - ### 1. Create a GraphQL query file 36 - 37 - Create a file at `src/my_app/graphql/get_character.gql`: 25 + 1. **Create a `.gql` file** at `src/my_app/graphql/get_user.gql`: 38 26 39 27 ```graphql 40 - query GetCharacter($id: ID!) { 41 - character(id: $id) { 28 + query GetUser($id: ID!) { 29 + user(id: $id) { 42 30 id 43 31 name 44 - status 45 - species 32 + email 46 33 } 47 34 } 48 35 ``` 49 36 50 - ### 2. Generate type-safe code 37 + 2. **Generate code:** 51 38 52 39 ```bash 53 - gleam run -m squall generate https://rickandmortyapi.com/graphql 40 + gleam run -m squall generate https://api.example.com/graphql 54 41 ``` 55 42 56 - ### 3. Use the generated code 43 + 3. **Use it:** 57 44 58 45 ```gleam 59 46 import squall 60 - import my_app/graphql/get_character 47 + import my_app/graphql/get_user 48 + import gleam/httpc 61 49 62 50 pub fn main() { 63 - // Create a client for your target 64 - // On Erlang: 65 - let client = squall.new_erlang_client( 66 - "https://rickandmortyapi.com/graphql", 67 - [] 68 - ) 69 - 70 - // On JavaScript: 71 - // let client = squall.new_javascript_client( 72 - // "https://rickandmortyapi.com/graphql", 73 - // [] 74 - // ) 75 - 76 - // Use the generated function - works on both targets! 77 - case get_character.get_character(client, "1") { 78 - Ok(response) -> { 79 - io.println("Character: " <> response.character.name) 80 - } 81 - Error(err) -> { 82 - io.println("Error: " <> err) 83 - } 84 - } 85 - } 86 - ``` 51 + // 1. Create client (just config) 52 + let client = squall.new("https://api.example.com/graphql", []) 87 53 88 - **Note**: On JavaScript, the generated function returns a `Promise(Result(...))` instead of `Result(...)`. See the [examples](./examples/) for how to handle both targets. 54 + // 2. Build request (no I/O) 55 + let assert Ok(request) = get_user.get_user(client, "123") 89 56 90 - ## Creating a Client 57 + // 3. Send request (you control this) 58 + let assert Ok(response) = httpc.send(request) 91 59 92 - Squall provides target-specific client constructors: 60 + // 4. Parse response (no I/O) 61 + let assert Ok(data) = get_user.parse_get_user_response(response.body) 93 62 94 - ### Erlang Target 95 - 96 - ```gleam 97 - import squall 98 - 99 - // Basic client 100 - let client = squall.new_erlang_client( 101 - "https://api.example.com/graphql", 102 - [#("X-Custom-Header", "value")] 103 - ) 104 - 105 - // With bearer token authentication 106 - let client = squall.new_erlang_client_with_auth( 107 - "https://api.example.com/graphql", 108 - "your-api-token-here" 109 - ) 110 - ``` 111 - 112 - ### JavaScript Target 113 - 114 - ```gleam 115 - import squall 116 - 117 - // Basic client 118 - let client = squall.new_javascript_client( 119 - "https://api.example.com/graphql", 120 - [#("X-Custom-Header", "value")] 121 - ) 122 - 123 - // With bearer token authentication 124 - let client = squall.new_javascript_client_with_auth( 125 - "https://api.example.com/graphql", 126 - "your-api-token-here" 127 - ) 128 - ``` 129 - 130 - ### Isomorphic Code 131 - 132 - For code that works on both targets, use `@target()` conditionals: 133 - 134 - ```gleam 135 - import squall 136 - 137 - @target(erlang) 138 - fn create_client() -> squall.Client { 139 - squall.new_erlang_client("https://api.example.com/graphql", []) 140 - } 141 - 142 - @target(javascript) 143 - fn create_client() -> squall.Client { 144 - squall.new_javascript_client("https://api.example.com/graphql", []) 145 - } 146 - 147 - pub fn main() { 148 - let client = create_client() // Works on both targets! 149 - // ... use the client 63 + // 5. Use the data 64 + io.debug(data.user) 150 65 } 151 66 ``` 152 67 153 - ## Isomorphic Architecture 154 - 155 - Squall's isomorphic design means you write your GraphQL queries once, and they work on both Erlang and JavaScript: 156 - 157 - ### What's Different Between Targets? 158 - 159 - | Aspect | Erlang | JavaScript | 160 - |--------|--------|------------| 161 - | **HTTP Client** | `gleam_httpc` | Fetch API | 162 - | **Return Type** | `Result(T, String)` | `Promise(Result(T, String))` | 163 - | **Execution** | Synchronous | Asynchronous | 164 - | **Client Constructor** | `new_erlang_client()` | `new_javascript_client()` | 165 - 166 - ### What's the Same? 167 - 168 - ✅ **GraphQL queries** - Same `.gql` files 169 - ✅ **Generated types** - Identical response types 170 - ✅ **Generated decoders** - Same JSON parsing logic 171 - ✅ **Business logic** - Share response handling code 172 - ✅ **Generated functions** - Same function signatures (except return type) 173 - 174 - The only code you need to write differently is: 175 - 1. Client creation (use `@target()` conditionals) 176 - 2. Promise handling on JavaScript (use `promise.await()`) 177 - 178 - Everything else is truly isomorphic! 179 - 180 68 ## How It Works 181 69 182 - Squall follows a simple workflow: 183 - 184 - 1. **Discovery**: Finds all `.gql` files in `src/**/graphql/` directories 185 - 2. **Parsing**: Parses GraphQL operations (queries, mutations, subscriptions) 186 - 3. **Introspection**: Fetches the GraphQL schema from your endpoint 187 - 4. **Type Mapping**: Maps GraphQL types to Gleam types 188 - 5. **Code Generation**: Generates: 189 - - Custom types for responses 190 - - JSON decoders 191 - - Type-safe functions with proper parameters 192 - 193 - ## Project Structure 194 - 195 - ``` 196 - your-project/ 197 - ├── src/ 198 - │ └── my_app/ 199 - │ └── graphql/ 200 - │ ├── get_user.gql # Your GraphQL query 201 - │ └── get_user.gleam # Generated code 202 - └── gleam.toml 203 - ``` 204 - 205 - ## Examples 206 - 207 - Check out the [examples](./examples/) directory for complete working examples: 70 + For each `.gql` file, Squall generates two functions: 208 71 209 - - **[01-erlang](./examples/01-erlang/)** - Erlang/OTP backend example 210 - - **[02-javascript](./examples/02-javascript/)** - JavaScript (Node.js/Browser) example 211 - - **[03-isomorphic](./examples/03-isomorphic/)** - Cross-platform example that runs on both! 72 + ```gleam 73 + // Builds HTTP request - no I/O 74 + pub fn get_user(client: Client, id: String) -> Result(Request(String), String) 212 75 213 - ## Generated Code 214 - 215 - For a query like: 216 - 217 - ```graphql 218 - query GetUser($id: ID!) { 219 - user(id: $id) { 220 - id 221 - name 222 - email 223 - } 224 - } 76 + // Parses response - no I/O 77 + pub fn parse_get_user_response(body: String) -> Result(GetUserResponse, String) 225 78 ``` 226 79 227 - Squall generates type-safe Gleam code with: 80 + You send the request with your own HTTP client. 228 81 229 - **Response Types:** 230 - ```gleam 231 - pub type GetUserResponse { 232 - GetUserResponse(user: Option(User)) 233 - } 82 + ## JavaScript Target 234 83 235 - pub type User { 236 - User( 237 - id: String, 238 - name: Option(String), 239 - email: Option(String) 240 - ) 241 - } 242 - ``` 84 + Same generated code works on JavaScript. Just use a different HTTP client: 243 85 244 - **Decoders:** 245 86 ```gleam 246 - fn get_user_response_decoder() -> decode.Decoder(GetUserResponse) { 247 - // Auto-generated JSON decoder 248 - } 249 - ``` 87 + import gleam/fetch 88 + import gleam/javascript/promise 250 89 251 - **Query Function:** 252 - ```gleam 253 - // On Erlang - returns Result 254 - pub fn get_user( 255 - client: squall.Client, 256 - id: String, 257 - ) -> Result(GetUserResponse, String) { 258 - squall.execute_query(client, query, variables, decoder) 259 - } 90 + let client = squall.new("https://api.example.com/graphql", []) 91 + let assert Ok(request) = get_user.get_user(client, "123") 260 92 261 - // On JavaScript - returns Promise(Result) 262 - pub fn get_user( 263 - client: squall.Client, 264 - id: String, 265 - ) -> Promise(Result(GetUserResponse, String)) { 266 - squall.execute_query(client, query, variables, decoder) 267 - } 93 + fetch.send(request) 94 + |> promise.try_await(fetch.read_text_body) 95 + |> promise.map(fn(result) { 96 + case result { 97 + Ok(response) -> { 98 + let assert Ok(data) = get_user.parse_get_user_response(response.body) 99 + io.debug(data.user) 100 + } 101 + Error(_) -> io.println("Request failed") 102 + } 103 + }) 268 104 ``` 269 105 270 106 ## Type Mapping ··· 283 119 284 120 ## CLI Commands 285 121 286 - ### Generate Code 287 - 288 122 ```bash 289 - # With explicit endpoint 290 - gleam run -m squall generate https://api.example.com/graphql 123 + # Generate code 124 + gleam run -m squall generate <endpoint> 291 125 292 - # Using environment variable 293 - export GRAPHQL_ENDPOINT=https://api.example.com/graphql 294 - gleam run -m squall generate 126 + # Example 127 + gleam run -m squall generate https://rickandmortyapi.com/graphql 295 128 ``` 296 129 297 - ## Architecture 298 - 299 - Squall is built with a modular, isomorphic architecture: 300 - 301 - ### Core Modules 302 - - **`discovery`**: Finds and reads `.gql` files 303 - - **`parser`**: Lexes and parses GraphQL operations 304 - - **`schema`**: Introspects and parses GraphQL schemas 305 - - **`type_mapping`**: Maps GraphQL types to Gleam types 306 - - **`codegen`**: Generates platform-agnostic Gleam code 307 - - **`error`**: Comprehensive error handling 308 - 309 - ### HTTP Adapter Pattern 310 - - **`adapter`**: Defines the HTTP adapter interface 311 - - **`adapter/erlang`**: Erlang implementation using `gleam_httpc` 312 - - **`adapter/javascript`**: JavaScript implementation using Fetch API 313 - 314 - The generated code calls `squall.execute_query()`, which automatically uses the correct HTTP adapter for your target platform. 315 - 316 - ## Development 130 + ## Examples 317 131 318 - ### Running Tests 132 + See the [example/](./example/) directory for a complete working example. 319 133 320 - ```bash 321 - gleam test 322 - ``` 134 + ## Inspiration 323 135 324 - ### Project Structure 325 - 326 - ``` 327 - squall/ 328 - ├── src/ 329 - │ ├── squall.gleam # CLI entry point & execute_query 330 - │ └── squall/ 331 - │ ├── adapter.gleam # HTTP adapter interface 332 - │ ├── adapter/ 333 - │ │ ├── erlang.gleam # Erlang HTTP adapter 334 - │ │ └── javascript.gleam # JavaScript HTTP adapter 335 - │ └── internal/ 336 - │ ├── discovery.gleam # File discovery 337 - │ ├── parser.gleam # GraphQL parser 338 - │ ├── schema.gleam # Schema introspection 339 - │ ├── type_mapping.gleam # Type conversion 340 - │ ├── codegen.gleam # Code generation 341 - │ └── error.gleam # Error types 342 - ├── test/ 343 - │ ├── discovery_test.gleam 344 - │ ├── parser_test.gleam 345 - │ ├── schema_test.gleam 346 - │ ├── type_mapping_test.gleam 347 - │ └── codegen_test.gleam 348 - ├── birdie_snapshots/ # Snapshot tests 349 - └── examples/ # Working examples 350 - ├── 01-erlang/ # Erlang example 351 - ├── 02-javascript/ # JavaScript example 352 - └── 03-isomorphic/ # Cross-platform example 353 - ``` 136 + - [bucket](https://github.com/lpil/bucket) - Gleam S3 client with sans-io pattern 137 + - [squirrel](https://github.com/giacomocavalieri/squirrel) - Original GraphQL client for Gleam 138 + - [Sans-IO pattern](https://sans-io.readthedocs.io/) 354 139 355 140 ## Roadmap 356 141 357 - ### Completed ✅ 358 - - [x] Query support 359 - - [x] Mutation support 360 - - [x] Subscription support 361 - - [x] Type-safe code generation 362 - - [x] Schema introspection 363 - - [x] Client abstraction with headers/authentication 364 - - [x] **Isomorphic support** (Erlang + JavaScript targets) 365 - - [x] HTTP adapter pattern 366 - - [x] Target-conditional compilation 367 - 368 - ### In Progress 🚧 369 142 - [ ] Fragment support 370 - - [ ] Directive handling (`@include`, `@skip`, etc.) 143 + - [ ] Directive handling (`@include`, `@skip`) 371 144 - [ ] Custom scalar type mapping 372 - - [ ] Schema caching for faster regeneration 373 - 374 - ## Contributing 375 - 376 - Contributions are welcome! Please: 377 - 378 - 1. Follow TDD principles 379 - 2. Add tests for new features 380 - 3. Update snapshots when changing code generation 381 - 4. Follow Gleam style guidelines 145 + - [ ] Schema caching 382 146 383 147 ## License 384 148
+7 -5
birdie_snapshots/mutation_function_generation.accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Mutation function generation 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_mutation_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option} ··· 41 40 json.object([#("createUser", json.nullable(input.create_user, user_to_json))]) 42 41 } 43 42 44 - pub fn create_user(client: squall.Client, name: String) { 45 - squall.execute_query( 43 + pub fn create_user(client: squall.Client, name: String) -> Result(Request(String), String) { 44 + squall.prepare_request( 46 45 client, 47 46 "mutation CreateUser($name: String!) { createUser(name: $name) { id name } }", 48 47 json.object([#("name", json.string(name))]), 49 - create_user_response_decoder(), 50 48 ) 51 49 } 52 50 51 + pub fn parse_create_user_response(body: String) -> Result(CreateUserResponse, String) { 52 + squall.parse_response(body, create_user_response_decoder()) 53 + } 54 +
+7 -5
birdie_snapshots/mutation_with_input_object_variable.accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Mutation with InputObject variable 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_mutation_with_input_object_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option, Some, None} ··· 71 70 ) 72 71 } 73 72 74 - pub fn update_profile(client: squall.Client, input: ProfileInput) { 75 - squall.execute_query( 73 + pub fn update_profile(client: squall.Client, input: ProfileInput) -> Result(Request(String), String) { 74 + squall.prepare_request( 76 75 client, 77 76 "mutation UpdateProfile($input: ProfileInput!) { updateProfile(input: $input) { id displayName } }", 78 77 json.object([#("input", profile_input_to_json(input))]), 79 - update_profile_response_decoder(), 80 78 ) 81 79 } 82 80 81 + pub fn parse_update_profile_response(body: String) -> Result(UpdateProfileResponse, String) { 82 + squall.parse_response(body, update_profile_response_decoder()) 83 + } 84 +
+7 -5
birdie_snapshots/mutation_with_json_scalar_in_input_object.accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Mutation with JSON scalar in InputObject 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_mutation_with_json_input_field_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option, Some, None} ··· 72 71 ) 73 72 } 74 73 75 - pub fn update_settings(client: squall.Client, input: SettingsInput) { 76 - squall.execute_query( 74 + pub fn update_settings(client: squall.Client, input: SettingsInput) -> Result(Request(String), String) { 75 + squall.prepare_request( 77 76 client, 78 77 "mutation UpdateSettings($input: SettingsInput!) { updateSettings(input: $input) { id metadata } }", 79 78 json.object([#("input", settings_input_to_json(input))]), 80 - update_settings_response_decoder(), 81 79 ) 82 80 } 83 81 82 + pub fn parse_update_settings_response(body: String) -> Result(UpdateSettingsResponse, String) { 83 + squall.parse_response(body, update_settings_response_decoder()) 84 + } 85 +
+7 -5
birdie_snapshots/mutation_with_nested_input_object_types.accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Mutation with nested InputObject types 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_mutation_with_nested_input_object_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option, Some, None} ··· 97 96 ) 98 97 } 99 98 100 - pub fn update_profile(client: squall.Client, input: ProfileInput) { 101 - squall.execute_query( 99 + pub fn update_profile(client: squall.Client, input: ProfileInput) -> Result(Request(String), String) { 100 + squall.prepare_request( 102 101 client, 103 102 "mutation UpdateProfile($input: ProfileInput!) { updateProfile(input: $input) { id displayName } }", 104 103 json.object([#("input", profile_input_to_json(input))]), 105 - update_profile_response_decoder(), 106 104 ) 107 105 } 108 106 107 + pub fn parse_update_profile_response(body: String) -> Result(UpdateProfileResponse, String) { 108 + squall.parse_response(body, update_profile_response_decoder()) 109 + } 110 +
+7 -5
birdie_snapshots/mutation_with_optional_input_object_fields_(imports_some,_none).accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Mutation with optional InputObject fields (imports Some, None) 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_mutation_with_optional_input_fields_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option, Some, None} ··· 86 85 ) 87 86 } 88 87 89 - pub fn create_profile(client: squall.Client, input: ProfileInput) { 90 - squall.execute_query( 88 + pub fn create_profile(client: squall.Client, input: ProfileInput) -> Result(Request(String), String) { 89 + squall.prepare_request( 91 90 client, 92 91 "mutation CreateProfile($input: ProfileInput!) { createProfile(input: $input) { id displayName description } }", 93 92 json.object([#("input", profile_input_to_json(input))]), 94 - create_profile_response_decoder(), 95 93 ) 96 94 } 97 95 96 + pub fn parse_create_profile_response(body: String) -> Result(CreateProfileResponse, String) { 97 + squall.parse_response(body, create_profile_response_decoder()) 98 + } 99 +
+7 -5
birdie_snapshots/query_with_all_non_nullable_fields_(no_option_import).accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Query with all non-nullable fields (no Option import) 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_query_with_all_non_nullable_fields_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 ··· 42 41 json.object([#("product", product_to_json(input.product))]) 43 42 } 44 43 45 - pub fn get_product(client: squall.Client) { 46 - squall.execute_query( 44 + pub fn get_product(client: squall.Client) -> Result(Request(String), String) { 45 + squall.prepare_request( 47 46 client, 48 47 "query GetProduct { product { id name price } }", 49 48 json.object([]), 50 - get_product_response_decoder(), 51 49 ) 52 50 } 53 51 52 + pub fn parse_get_product_response(body: String) -> Result(GetProductResponse, String) { 53 + squall.parse_response(body, get_product_response_decoder()) 54 + } 55 +
+7 -5
birdie_snapshots/query_with_inline_array_arguments.accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Query with inline array arguments 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_inline_array_arguments_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option} ··· 48 47 ) 49 48 } 50 49 51 - pub fn get_episodes(client: squall.Client) { 52 - squall.execute_query( 50 + pub fn get_episodes(client: squall.Client) -> Result(Request(String), String) { 51 + squall.prepare_request( 53 52 client, 54 53 "query GetEpisodes { episodesByIds(ids: [1, 2, 3]) { id name } }", 55 54 json.object([]), 56 - get_episodes_response_decoder(), 57 55 ) 58 56 } 59 57 58 + pub fn parse_get_episodes_response(body: String) -> Result(GetEpisodesResponse, String) { 59 + squall.parse_response(body, get_episodes_response_decoder()) 60 + } 61 +
+7 -5
birdie_snapshots/query_with_inline_object_arguments.accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Query with inline object arguments 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_inline_object_arguments_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option} ··· 65 64 ) 66 65 } 67 66 68 - pub fn get_characters(client: squall.Client) { 69 - squall.execute_query( 67 + pub fn get_characters(client: squall.Client) -> Result(Request(String), String) { 68 + squall.prepare_request( 70 69 client, 71 70 "query GetCharacters { characters(filter: { name: \"rick\", status: \"alive\" }) { results { id name } } }", 72 71 json.object([]), 73 - get_characters_response_decoder(), 74 72 ) 75 73 } 76 74 75 + pub fn parse_get_characters_response(body: String) -> Result(GetCharactersResponse, String) { 76 + squall.parse_response(body, get_characters_response_decoder()) 77 + } 78 +
+7 -5
birdie_snapshots/query_with_inline_scalar_arguments.accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Query with inline scalar arguments 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_inline_scalar_arguments_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option} ··· 45 44 ) 46 45 } 47 46 48 - pub fn get_character(client: squall.Client) { 49 - squall.execute_query( 47 + pub fn get_character(client: squall.Client) -> Result(Request(String), String) { 48 + squall.prepare_request( 50 49 client, 51 50 "query GetCharacter { character(id: 1) { id name } }", 52 51 json.object([]), 53 - get_character_response_decoder(), 54 52 ) 55 53 } 56 54 55 + pub fn parse_get_character_response(body: String) -> Result(GetCharacterResponse, String) { 56 + squall.parse_response(body, get_character_response_decoder()) 57 + } 58 +
+7 -5
birdie_snapshots/query_with_json_scalar_field.accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Query with JSON scalar field 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_query_with_json_scalar_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option} ··· 44 43 json.object([#("profile", json.nullable(input.profile, profile_to_json))]) 45 44 } 46 45 47 - pub fn get_profile(client: squall.Client) { 48 - squall.execute_query( 46 + pub fn get_profile(client: squall.Client) -> Result(Request(String), String) { 47 + squall.prepare_request( 49 48 client, 50 49 "query GetProfile { profile { id displayName metadata } }", 51 50 json.object([]), 52 - get_profile_response_decoder(), 53 51 ) 54 52 } 55 53 54 + pub fn parse_get_profile_response(body: String) -> Result(GetProfileResponse, String) { 55 + squall.parse_response(body, get_profile_response_decoder()) 56 + } 57 +
+7 -5
birdie_snapshots/query_with_multiple_root_fields_and_mixed_arguments.accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Query with multiple root fields and mixed arguments 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_multiple_root_fields_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option} ··· 115 114 ) 116 115 } 117 116 118 - pub fn multi_query(client: squall.Client) { 119 - squall.execute_query( 117 + pub fn multi_query(client: squall.Client) -> Result(Request(String), String) { 118 + squall.prepare_request( 120 119 client, 121 120 "query MultiQuery { characters(page: 2, filter: { name: \"rick\" }) { info { count } results { name } } location(id: 1) { id } episodesByIds(ids: [1, 2]) { id } }", 122 121 json.object([]), 123 - multi_query_response_decoder(), 124 122 ) 125 123 } 126 124 125 + pub fn parse_multi_query_response(body: String) -> Result(MultiQueryResponse, String) { 126 + squall.parse_response(body, multi_query_response_decoder()) 127 + } 128 +
+7 -5
birdie_snapshots/query_with_nested_types_generation.accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Query with nested types generation 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_query_with_nested_types_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option} ··· 47 46 ) 48 47 } 49 48 50 - pub fn get_character(client: squall.Client, id: String) { 51 - squall.execute_query( 49 + pub fn get_character(client: squall.Client, id: String) -> Result(Request(String), String) { 50 + squall.prepare_request( 52 51 client, 53 52 "query GetCharacter($id: ID!) { character(id: $id) { id name status } }", 54 53 json.object([#("id", json.string(id))]), 55 - get_character_response_decoder(), 56 54 ) 57 55 } 58 56 57 + pub fn parse_get_character_response(body: String) -> Result(GetCharacterResponse, String) { 58 + squall.parse_response(body, get_character_response_decoder()) 59 + } 60 +
+7 -5
birdie_snapshots/query_with_optional_response_fields_(no_some,_none_imports).accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Query with optional response fields (no Some, None imports) 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_query_with_optional_response_fields_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option} ··· 47 46 json.object([#("profile", json.nullable(input.profile, profile_to_json))]) 48 47 } 49 48 50 - pub fn get_profile(client: squall.Client) { 51 - squall.execute_query( 49 + pub fn get_profile(client: squall.Client) -> Result(Request(String), String) { 50 + squall.prepare_request( 52 51 client, 53 52 "query GetProfile { profile { id displayName description } }", 54 53 json.object([]), 55 - get_profile_response_decoder(), 56 54 ) 57 55 } 58 56 57 + pub fn parse_get_profile_response(body: String) -> Result(GetProfileResponse, String) { 58 + squall.parse_response(body, get_profile_response_decoder()) 59 + } 60 +
+7 -5
birdie_snapshots/query_with_variables_function_generation.accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Query with variables function generation 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_query_with_variables_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option} ··· 41 40 json.object([#("user", json.nullable(input.user, user_to_json))]) 42 41 } 43 42 44 - pub fn get_user(client: squall.Client, id: String) { 45 - squall.execute_query( 43 + pub fn get_user(client: squall.Client, id: String) -> Result(Request(String), String) { 44 + squall.prepare_request( 46 45 client, 47 46 "query GetUser($id: ID!) { user(id: $id) { id name } }", 48 47 json.object([#("id", json.string(id))]), 49 - get_user_response_decoder(), 50 48 ) 51 49 } 52 50 51 + pub fn parse_get_user_response(body: String) -> Result(GetUserResponse, String) { 52 + squall.parse_response(body, get_user_response_decoder()) 53 + } 54 +
+7 -5
birdie_snapshots/response_serializer_for_simple_type.accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Response serializer for simple type 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_response_serializer_simple_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option} ··· 41 40 json.object([#("user", json.nullable(input.user, user_to_json))]) 42 41 } 43 42 44 - pub fn get_user(client: squall.Client) { 45 - squall.execute_query( 43 + pub fn get_user(client: squall.Client) -> Result(Request(String), String) { 44 + squall.prepare_request( 46 45 client, 47 46 "query GetUser { user { id name } }", 48 47 json.object([]), 49 - get_user_response_decoder(), 50 48 ) 51 49 } 52 50 51 + pub fn parse_get_user_response(body: String) -> Result(GetUserResponse, String) { 52 + squall.parse_response(body, get_user_response_decoder()) 53 + } 54 +
+7 -5
birdie_snapshots/response_serializer_with_all_scalar_types.accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Response serializer with all scalar types 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_response_serializer_with_all_scalars_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option} ··· 64 63 json.object([#("product", json.nullable(input.product, product_to_json))]) 65 64 } 66 65 67 - pub fn get_product(client: squall.Client) { 68 - squall.execute_query( 66 + pub fn get_product(client: squall.Client) -> Result(Request(String), String) { 67 + squall.prepare_request( 69 68 client, 70 69 "query GetProduct { product { id name price inStock rating metadata } }", 71 70 json.object([]), 72 - get_product_response_decoder(), 73 71 ) 74 72 } 75 73 74 + pub fn parse_get_product_response(body: String) -> Result(GetProductResponse, String) { 75 + squall.parse_response(body, get_product_response_decoder()) 76 + } 77 +
+7 -5
birdie_snapshots/response_serializer_with_lists.accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Response serializer with lists 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_response_serializer_with_lists_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option} ··· 48 47 ) 49 48 } 50 49 51 - pub fn get_users(client: squall.Client) { 52 - squall.execute_query( 50 + pub fn get_users(client: squall.Client) -> Result(Request(String), String) { 51 + squall.prepare_request( 53 52 client, 54 53 "query GetUsers { users { id name } }", 55 54 json.object([]), 56 - get_users_response_decoder(), 57 55 ) 58 56 } 59 57 58 + pub fn parse_get_users_response(body: String) -> Result(GetUsersResponse, String) { 59 + squall.parse_response(body, get_users_response_decoder()) 60 + } 61 +
+7 -5
birdie_snapshots/response_serializer_with_nested_types.accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Response serializer with nested types 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_response_serializer_with_nested_types_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option} ··· 62 61 json.object([#("user", json.nullable(input.user, user_to_json))]) 63 62 } 64 63 65 - pub fn get_user(client: squall.Client) { 66 - squall.execute_query( 64 + pub fn get_user(client: squall.Client) -> Result(Request(String), String) { 65 + squall.prepare_request( 67 66 client, 68 67 "query GetUser { user { id name location { city country } } }", 69 68 json.object([]), 70 - get_user_response_decoder(), 71 69 ) 72 70 } 73 71 72 + pub fn parse_get_user_response(body: String) -> Result(GetUserResponse, String) { 73 + squall.parse_response(body, get_user_response_decoder()) 74 + } 75 +
+7 -5
birdie_snapshots/response_serializer_with_optional_fields.accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Response serializer with optional fields 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_response_serializer_with_optional_fields_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option} ··· 43 42 json.object([#("user", json.nullable(input.user, user_to_json))]) 44 43 } 45 44 46 - pub fn get_user(client: squall.Client) { 47 - squall.execute_query( 45 + pub fn get_user(client: squall.Client) -> Result(Request(String), String) { 46 + squall.prepare_request( 48 47 client, 49 48 "query GetUser { user { id name email } }", 50 49 json.object([]), 51 - get_user_response_decoder(), 52 50 ) 53 51 } 54 52 53 + pub fn parse_get_user_response(body: String) -> Result(GetUserResponse, String) { 54 + squall.parse_response(body, get_user_response_decoder()) 55 + } 56 +
+7 -5
birdie_snapshots/simple_query_function_generation.accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Simple query function generation 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_simple_query_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option} ··· 41 40 json.object([#("user", json.nullable(input.user, user_to_json))]) 42 41 } 43 42 44 - pub fn get_user(client: squall.Client) { 45 - squall.execute_query( 43 + pub fn get_user(client: squall.Client) -> Result(Request(String), String) { 44 + squall.prepare_request( 46 45 client, 47 46 "query GetUser { user { id name } }", 48 47 json.object([]), 49 - get_user_response_decoder(), 50 48 ) 51 49 } 52 50 51 + pub fn parse_get_user_response(body: String) -> Result(GetUserResponse, String) { 52 + squall.parse_response(body, get_user_response_decoder()) 53 + } 54 +
+7 -5
birdie_snapshots/type_with_reserved_keywords.accepted
··· 1 1 --- 2 2 version: 1.4.1 3 3 title: Type with reserved keywords 4 - file: ./test/codegen_test.gleam 5 - test_name: generate_with_reserved_keywords_test 6 4 --- 7 5 import gleam/dynamic/decode 6 + import gleam/http/request.{type Request} 8 7 import gleam/json 9 8 import squall 10 9 import gleam/option.{type Option} ··· 50 49 json.object([#("item", json.nullable(input.item, item_to_json))]) 51 50 } 52 51 53 - pub fn get_item(client: squall.Client) { 54 - squall.execute_query( 52 + pub fn get_item(client: squall.Client) -> Result(Request(String), String) { 53 + squall.prepare_request( 55 54 client, 56 55 "query GetItem { item { id type case let } }", 57 56 json.object([]), 58 - get_item_response_decoder(), 59 57 ) 60 58 } 61 59 60 + pub fn parse_get_item_response(body: String) -> Result(GetItemResponse, String) { 61 + squall.parse_response(body, get_item_response_decoder()) 62 + } 63 +
+5
example/README.md
··· 1 + # example 2 + 3 + ```sh 4 + gleam run # Run the project 5 + ```
+17
example/gleam.toml
··· 1 + name = "example" 2 + version = "1.0.0" 3 + 4 + [dependencies] 5 + gleam_stdlib = ">= 0.34.0 and < 2.0.0" 6 + gleam_json = ">= 3.0.0 and < 4.0.0" 7 + gleam_http = ">= 4.3.0 and < 5.0.0" 8 + squall = { path = "../" } 9 + 10 + # For Erlang target (default) 11 + gleam_httpc = ">= 5.0.0 and < 6.0.0" 12 + 13 + # For JavaScript target: comment out httpc above and uncomment this 14 + # gleam_fetch = ">= 1.0.0 and < 2.0.0" 15 + 16 + [dev-dependencies] 17 + gleeunit = ">= 1.0.0 and < 2.0.0"
+43
example/src/example.gleam
··· 1 + import gleam/http/request 2 + import gleam/io 3 + import gleam/result 4 + 5 + import squall 6 + 7 + // Import the generated GraphQL code 8 + import graphql/get_character 9 + 10 + // For Erlang target (default) 11 + import gleam/httpc 12 + 13 + // For JavaScript target: comment out httpc above and uncomment these 14 + // import gleam/javascript/promise 15 + // import gleam/fetch 16 + 17 + pub fn main() { 18 + let client = squall.new("https://rickandmortyapi.com/graphql", []) 19 + let assert Ok(request) = get_character.get_character(client, "1") 20 + let assert Ok(body) = send_erlang(request) 21 + io.println(body) 22 + } 23 + 24 + // ==================== ERLANG HTTP CLIENT ==================== 25 + fn send_erlang(request: request.Request(String)) -> Result(String, String) { 26 + httpc.send(request) 27 + |> result.map(fn(resp) { resp.body }) 28 + |> result.map_error(fn(_) { "HTTP request failed" }) 29 + } 30 + // ==================== JAVASCRIPT HTTP CLIENT ==================== 31 + // Uncomment this function when using JavaScript target 32 + // 33 + // fn send_javascript( 34 + // request: Request(String), 35 + // ) -> promise.Promise(Result(String, String)) { 36 + // fetch.send(request) 37 + // |> promise.try_await(fetch.read_text_body) 38 + // |> promise.map(fn(result) { 39 + // result 40 + // |> result.map(fn(resp) { resp.body }) 41 + // |> result.map_error(fn(_) { "HTTP request failed" }) 42 + // }) 43 + // }
+66
example/src/graphql/get_character.gleam
··· 1 + import gleam/dynamic/decode 2 + import gleam/http/request.{type Request} 3 + import gleam/json 4 + import gleam/option.{type Option} 5 + import squall 6 + 7 + pub type Character { 8 + Character( 9 + id: Option(String), 10 + name: Option(String), 11 + status: Option(String), 12 + species: Option(String), 13 + ) 14 + } 15 + 16 + pub fn character_decoder() -> decode.Decoder(Character) { 17 + use id <- decode.field("id", decode.optional(decode.string)) 18 + use name <- decode.field("name", decode.optional(decode.string)) 19 + use status <- decode.field("status", decode.optional(decode.string)) 20 + use species <- decode.field("species", decode.optional(decode.string)) 21 + decode.success(Character(id: id, name: name, status: status, species: species)) 22 + } 23 + 24 + pub fn character_to_json(input: Character) -> json.Json { 25 + json.object([ 26 + #("id", json.nullable(input.id, json.string)), 27 + #("name", json.nullable(input.name, json.string)), 28 + #("status", json.nullable(input.status, json.string)), 29 + #("species", json.nullable(input.species, json.string)), 30 + ]) 31 + } 32 + 33 + pub type GetCharacterResponse { 34 + GetCharacterResponse(character: Option(Character)) 35 + } 36 + 37 + pub fn get_character_response_decoder() -> decode.Decoder(GetCharacterResponse) { 38 + use character <- decode.field( 39 + "character", 40 + decode.optional(character_decoder()), 41 + ) 42 + decode.success(GetCharacterResponse(character: character)) 43 + } 44 + 45 + pub fn get_character_response_to_json(input: GetCharacterResponse) -> json.Json { 46 + json.object([ 47 + #("character", json.nullable(input.character, character_to_json)), 48 + ]) 49 + } 50 + 51 + pub fn get_character( 52 + client: squall.Client, 53 + id: String, 54 + ) -> Result(Request(String), String) { 55 + squall.prepare_request( 56 + client, 57 + "query GetCharacter($id: ID!) { character(id: $id) { id name status species } }", 58 + json.object([#("id", json.string(id))]), 59 + ) 60 + } 61 + 62 + pub fn parse_get_character_response( 63 + body: String, 64 + ) -> Result(GetCharacterResponse, String) { 65 + squall.parse_response(body, get_character_response_decoder()) 66 + }
-54
examples/01-erlang/README.md
··· 1 - # Squall Erlang Example 2 - 3 - This example demonstrates using Squall-generated GraphQL clients on the **Erlang/OTP** target. 4 - 5 - ## What's Inside 6 - 7 - - `erlang_example.gleam` - Multi-field query example 8 - - `example_with_vars.gleam` - Query with GraphQL variables 9 - - `src/graphql/*.gql` - GraphQL query definitions 10 - - `src/graphql/*.gleam` - Generated type-safe Gleam code 11 - 12 - ## HTTP Adapter 13 - 14 - This example uses the **Erlang adapter** which uses `gleam_httpc` for HTTP requests: 15 - 16 - ```gleam 17 - import squall 18 - 19 - let client = squall.new_erlang_client("https://rickandmortyapi.com/graphql", []) 20 - ``` 21 - 22 - ## Running 23 - 24 - ```bash 25 - # Build the project 26 - gleam build 27 - 28 - # Run the main example 29 - gleam run -m erlang_example 30 - 31 - # Run the variables example 32 - gleam run -m example_with_vars 33 - ``` 34 - 35 - ## Regenerating GraphQL Code 36 - 37 - If you modify the `.gql` files, regenerate the code: 38 - 39 - ```bash 40 - # From the squall root directory 41 - cd ../.. 42 - gleam run -m squall generate https://rickandmortyapi.com/graphql 43 - ``` 44 - 45 - This will update the `.gleam` files in `src/graphql/`. 46 - 47 - ## Features Demonstrated 48 - 49 - ✅ **Erlang/OTP Runtime** - Uses `gleam_httpc` HTTP client 50 - ✅ **Type Safety** - Fully typed based on GraphQL schema 51 - ✅ **Nested Objects** - Automatic decoder generation 52 - ✅ **Optional Fields** - Proper `Option` type handling 53 - ✅ **Variables** - Type-safe GraphQL variables 54 - ✅ **JSON Serialization** - Convert responses back to JSON
-9
examples/01-erlang/gleam.toml
··· 1 - name = "erlang_example" 2 - version = "0.1.0" 3 - description = "Squall Erlang example - Rick and Morty GraphQL API" 4 - 5 - [dependencies] 6 - gleam_stdlib = ">= 0.65.0 and < 0.66.0" 7 - gleam_json = ">= 3.0.0 and < 4.0.0" 8 - gleam_http = ">= 4.3.0 and < 5.0.0" 9 - squall = { path = "../.." }
+8 -6
examples/01-erlang/manifest.toml example/manifest.toml
··· 12 12 { name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" }, 13 13 { name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" }, 14 14 { name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" }, 15 - { name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" }, 16 - { name = "squall", version = "0.1.0", build_tools = ["gleam"], requirements = ["argv", "filepath", "glam", "gleam_fetch", "gleam_http", "gleam_httpc", "gleam_json", "gleam_stdlib", "simplifile"], source = "local", path = "../.." }, 15 + { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, 16 + { name = "simplifile", version = "2.3.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "957E0E5B75927659F1D2A1B7B75D7B9BA96FAA8D0C53EA71C4AD9CD0C6B848F6" }, 17 + { name = "squall", version = "0.1.0", build_tools = ["gleam"], requirements = ["argv", "filepath", "glam", "gleam_fetch", "gleam_http", "gleam_httpc", "gleam_javascript", "gleam_json", "gleam_stdlib", "simplifile", "swell"], source = "local", path = ".." }, 18 + { name = "swell", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "swell", source = "hex", outer_checksum = "7CCA8C61349396C5B59B3C0627185F5B30917044E0D61CB7E0E5CC75C1B4A8E9" }, 17 19 ] 18 20 19 21 [requirements] 20 - gleam_fetch = { version = ">= 1.0.0 and < 2.0.0" } 21 22 gleam_http = { version = ">= 4.3.0 and < 5.0.0" } 22 - gleam_javascript = { version = ">= 0.3.0 and < 2.0.0" } 23 + gleam_httpc = { version = ">= 5.0.0 and < 6.0.0" } 23 24 gleam_json = { version = ">= 3.0.0 and < 4.0.0" } 24 - gleam_stdlib = { version = ">= 0.65.0 and < 0.66.0" } 25 - squall = { path = "../.." } 25 + gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } 26 + gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 27 + squall = { path = "../" }
-38
examples/01-erlang/src/erlang_example.gleam
··· 1 - import gleam/io 2 - import gleam/json 3 - import gleam/string 4 - import graphql/multi_query 5 - import squall 6 - 7 - pub fn main() { 8 - io.println("Squall Multi-Field Query Example (Erlang)") 9 - io.println("==========================================\n") 10 - 11 - // Create an Erlang client (uses gleam_httpc for HTTP requests) 12 - let client = squall.new_erlang_client("https://rickandmortyapi.com/graphql", []) 13 - 14 - let result = multi_query.multi_query(client) 15 - 16 - case result { 17 - Ok(response) -> { 18 - io.println("✓ Success! Received response from API\n") 19 - 20 - // Print the Gleam data structure 21 - io.println("Gleam Response Structure:") 22 - io.println("-------------------------") 23 - io.println(string.inspect(response)) 24 - 25 - // Convert to JSON and print 26 - io.println("\nJSON Response:") 27 - io.println("--------------") 28 - let json_response = multi_query.multi_query_response_to_json(response) 29 - io.println(json.to_string(json_response)) 30 - 31 - Nil 32 - } 33 - Error(err) -> { 34 - io.println("✗ Error: " <> err) 35 - Nil 36 - } 37 - } 38 - }
-46
examples/01-erlang/src/example_with_vars.gleam
··· 1 - import gleam/io 2 - import gleam/json 3 - import gleam/string 4 - import graphql/multi_query_with_vars 5 - import squall 6 - 7 - pub fn main() { 8 - io.println("Squall Multi-Field Query Example (with Variables - Erlang)") 9 - io.println("===========================================================\n") 10 - 11 - io.println("Calling multi_query_with_vars with:") 12 - io.println(" page: 2") 13 - io.println(" name: \"rick\"") 14 - io.println(" locationId: \"1\"") 15 - io.println(" episodeIds: [1, 2]\n") 16 - 17 - // Create an Erlang client (uses gleam_httpc for HTTP requests) 18 - let client = squall.new_erlang_client("https://rickandmortyapi.com/graphql", []) 19 - 20 - let result = 21 - multi_query_with_vars.multi_query_with_vars(client, 2, "rick", "1", [1, 2]) 22 - 23 - case result { 24 - Ok(response) -> { 25 - io.println("✓ Success! Received response from API\n") 26 - 27 - // Print the Gleam data structure 28 - io.println("Gleam Response Structure:") 29 - io.println("-------------------------") 30 - io.println(string.inspect(response)) 31 - 32 - // Convert to JSON and print 33 - io.println("\nJSON Response:") 34 - io.println("--------------") 35 - let json_response = 36 - multi_query_with_vars.multi_query_with_vars_response_to_json(response) 37 - io.println(json.to_string(json_response)) 38 - 39 - Nil 40 - } 41 - Error(err) -> { 42 - io.println("✗ Error: " <> err) 43 - Nil 44 - } 45 - } 46 - }
-71
examples/01-erlang/src/graphql/get_character.gleam
··· 1 - import gleam/dynamic/decode 2 - import gleam/json 3 - import squall 4 - import gleam/option.{type Option} 5 - 6 - pub type Character { 7 - Character( 8 - id: Option(String), 9 - name: Option(String), 10 - status: Option(String), 11 - species: Option(String), 12 - type_: Option(String), 13 - gender: Option(String), 14 - ) 15 - } 16 - 17 - pub fn character_decoder() -> decode.Decoder(Character) { 18 - use id <- decode.field("id", decode.optional(decode.string)) 19 - use name <- decode.field("name", decode.optional(decode.string)) 20 - use status <- decode.field("status", decode.optional(decode.string)) 21 - use species <- decode.field("species", decode.optional(decode.string)) 22 - use type_ <- decode.field("type", decode.optional(decode.string)) 23 - use gender <- decode.field("gender", decode.optional(decode.string)) 24 - decode.success(Character( 25 - id: id, 26 - name: name, 27 - status: status, 28 - species: species, 29 - type_: type_, 30 - gender: gender, 31 - )) 32 - } 33 - 34 - pub fn character_to_json(input: Character) -> json.Json { 35 - json.object( 36 - [ 37 - #("id", json.nullable(input.id, json.string)), 38 - #("name", json.nullable(input.name, json.string)), 39 - #("status", json.nullable(input.status, json.string)), 40 - #("species", json.nullable(input.species, json.string)), 41 - #("type", json.nullable(input.type_, json.string)), 42 - #("gender", json.nullable(input.gender, json.string)), 43 - ], 44 - ) 45 - } 46 - 47 - pub type GetCharacterResponse { 48 - GetCharacterResponse(character: Option(Character)) 49 - } 50 - 51 - pub fn get_character_response_decoder() -> decode.Decoder(GetCharacterResponse) { 52 - use character <- decode.field("character", decode.optional(character_decoder())) 53 - decode.success(GetCharacterResponse(character: character)) 54 - } 55 - 56 - pub fn get_character_response_to_json(input: GetCharacterResponse) -> json.Json { 57 - json.object( 58 - [ 59 - #("character", json.nullable(input.character, character_to_json)), 60 - ], 61 - ) 62 - } 63 - 64 - pub fn get_character(client: squall.Client, id: String) { 65 - squall.execute_query( 66 - client, 67 - "query GetCharacter($id: ID!) { character(id: $id) { id name status species type gender } }", 68 - json.object([#("id", json.string(id))]), 69 - get_character_response_decoder(), 70 - ) 71 - }
examples/01-erlang/src/graphql/get_character.gleam.new

This is a binary file and will not be displayed.

-2
examples/01-erlang/src/graphql/get_character.gql example/src/graphql/get_character.gql
··· 4 4 name 5 5 status 6 6 species 7 - type 8 - gender 9 7 } 10 8 }
-78
examples/01-erlang/src/graphql/get_characters.gleam
··· 1 - import gleam/dynamic/decode 2 - import gleam/json 3 - import squall 4 - import gleam/option.{type Option} 5 - 6 - pub type Characters { 7 - Characters(results: Option(List(Character))) 8 - } 9 - 10 - pub fn characters_decoder() -> decode.Decoder(Characters) { 11 - use results <- decode.field("results", decode.optional(decode.list(character_decoder()))) 12 - decode.success(Characters(results: results)) 13 - } 14 - 15 - pub type Character { 16 - Character( 17 - id: Option(String), 18 - name: Option(String), 19 - status: Option(String), 20 - species: Option(String), 21 - ) 22 - } 23 - 24 - pub fn character_decoder() -> decode.Decoder(Character) { 25 - use id <- decode.field("id", decode.optional(decode.string)) 26 - use name <- decode.field("name", decode.optional(decode.string)) 27 - use status <- decode.field("status", decode.optional(decode.string)) 28 - use species <- decode.field("species", decode.optional(decode.string)) 29 - decode.success(Character(id: id, name: name, status: status, species: species)) 30 - } 31 - 32 - pub fn characters_to_json(input: Characters) -> json.Json { 33 - json.object( 34 - [ 35 - #("results", json.nullable( 36 - input.results, 37 - fn(list) { json.array(from: list, of: character_to_json) }, 38 - )), 39 - ], 40 - ) 41 - } 42 - 43 - pub fn character_to_json(input: Character) -> json.Json { 44 - json.object( 45 - [ 46 - #("id", json.nullable(input.id, json.string)), 47 - #("name", json.nullable(input.name, json.string)), 48 - #("status", json.nullable(input.status, json.string)), 49 - #("species", json.nullable(input.species, json.string)), 50 - ], 51 - ) 52 - } 53 - 54 - pub type GetCharactersResponse { 55 - GetCharactersResponse(characters: Option(Characters)) 56 - } 57 - 58 - pub fn get_characters_response_decoder() -> decode.Decoder(GetCharactersResponse) { 59 - use characters <- decode.field("characters", decode.optional(characters_decoder())) 60 - decode.success(GetCharactersResponse(characters: characters)) 61 - } 62 - 63 - pub fn get_characters_response_to_json(input: GetCharactersResponse) -> json.Json { 64 - json.object( 65 - [ 66 - #("characters", json.nullable(input.characters, characters_to_json)), 67 - ], 68 - ) 69 - } 70 - 71 - pub fn get_characters(client: squall.Client) { 72 - squall.execute_query( 73 - client, 74 - "query GetCharacters { characters { results { id name status species } } }", 75 - json.object([]), 76 - get_characters_response_decoder(), 77 - ) 78 - }
examples/01-erlang/src/graphql/get_characters.gleam.new

This is a binary file and will not be displayed.

-10
examples/01-erlang/src/graphql/get_characters.gql
··· 1 - query GetCharacters { 2 - characters { 3 - results { 4 - id 5 - name 6 - status 7 - species 8 - } 9 - } 10 - }
-119
examples/01-erlang/src/graphql/multi_query.gleam
··· 1 - import gleam/dynamic/decode 2 - import gleam/json 3 - import squall 4 - import gleam/option.{type Option} 5 - 6 - pub type Characters { 7 - Characters(info: Option(Info), results: Option(List(Character))) 8 - } 9 - 10 - pub fn characters_decoder() -> decode.Decoder(Characters) { 11 - use info <- decode.field("info", decode.optional(info_decoder())) 12 - use results <- decode.field("results", decode.optional(decode.list(character_decoder()))) 13 - decode.success(Characters(info: info, results: results)) 14 - } 15 - 16 - pub type Info { 17 - Info(count: Option(Int)) 18 - } 19 - 20 - pub fn info_decoder() -> decode.Decoder(Info) { 21 - use count <- decode.field("count", decode.optional(decode.int)) 22 - decode.success(Info(count: count)) 23 - } 24 - 25 - pub type Character { 26 - Character(name: Option(String)) 27 - } 28 - 29 - pub fn character_decoder() -> decode.Decoder(Character) { 30 - use name <- decode.field("name", decode.optional(decode.string)) 31 - decode.success(Character(name: name)) 32 - } 33 - 34 - pub type Location { 35 - Location(id: Option(String)) 36 - } 37 - 38 - pub fn location_decoder() -> decode.Decoder(Location) { 39 - use id <- decode.field("id", decode.optional(decode.string)) 40 - decode.success(Location(id: id)) 41 - } 42 - 43 - pub type Episode { 44 - Episode(id: Option(String)) 45 - } 46 - 47 - pub fn episode_decoder() -> decode.Decoder(Episode) { 48 - use id <- decode.field("id", decode.optional(decode.string)) 49 - decode.success(Episode(id: id)) 50 - } 51 - 52 - pub fn characters_to_json(input: Characters) -> json.Json { 53 - json.object( 54 - [ 55 - #("info", json.nullable(input.info, info_to_json)), 56 - #("results", json.nullable( 57 - input.results, 58 - fn(list) { json.array(from: list, of: character_to_json) }, 59 - )), 60 - ], 61 - ) 62 - } 63 - 64 - pub fn info_to_json(input: Info) -> json.Json { 65 - json.object([#("count", json.nullable(input.count, json.int))]) 66 - } 67 - 68 - pub fn character_to_json(input: Character) -> json.Json { 69 - json.object([#("name", json.nullable(input.name, json.string))]) 70 - } 71 - 72 - pub fn location_to_json(input: Location) -> json.Json { 73 - json.object([#("id", json.nullable(input.id, json.string))]) 74 - } 75 - 76 - pub fn episode_to_json(input: Episode) -> json.Json { 77 - json.object([#("id", json.nullable(input.id, json.string))]) 78 - } 79 - 80 - pub type MultiQueryResponse { 81 - MultiQueryResponse( 82 - characters: Option(Characters), 83 - location: Option(Location), 84 - episodes_by_ids: Option(List(Episode)), 85 - ) 86 - } 87 - 88 - pub fn multi_query_response_decoder() -> decode.Decoder(MultiQueryResponse) { 89 - use characters <- decode.field("characters", decode.optional(characters_decoder())) 90 - use location <- decode.field("location", decode.optional(location_decoder())) 91 - use episodes_by_ids <- decode.field("episodesByIds", decode.optional(decode.list(episode_decoder()))) 92 - decode.success(MultiQueryResponse( 93 - characters: characters, 94 - location: location, 95 - episodes_by_ids: episodes_by_ids, 96 - )) 97 - } 98 - 99 - pub fn multi_query_response_to_json(input: MultiQueryResponse) -> json.Json { 100 - json.object( 101 - [ 102 - #("characters", json.nullable(input.characters, characters_to_json)), 103 - #("location", json.nullable(input.location, location_to_json)), 104 - #("episodesByIds", json.nullable( 105 - input.episodes_by_ids, 106 - fn(list) { json.array(from: list, of: episode_to_json) }, 107 - )), 108 - ], 109 - ) 110 - } 111 - 112 - pub fn multi_query(client: squall.Client) { 113 - squall.execute_query( 114 - client, 115 - "query MultiQuery { characters(page: 2, filter: { name: \"rick\" }) { info { count } results { name } } location(id: 1) { id } episodesByIds(ids: [1, 2]) { id } }", 116 - json.object([]), 117 - multi_query_response_decoder(), 118 - ) 119 - }
examples/01-erlang/src/graphql/multi_query.gleam.new

This is a binary file and will not be displayed.

-16
examples/01-erlang/src/graphql/multi_query.gql
··· 1 - query MultiQuery { 2 - characters(page: 2, filter: { name: "rick" }) { 3 - info { 4 - count 5 - } 6 - results { 7 - name 8 - } 9 - } 10 - location(id: 1) { 11 - id 12 - } 13 - episodesByIds(ids: [1, 2]) { 14 - id 15 - } 16 - }
-138
examples/01-erlang/src/graphql/multi_query_with_vars.gleam
··· 1 - import gleam/dynamic/decode 2 - import gleam/json 3 - import squall 4 - import gleam/option.{type Option} 5 - 6 - pub type Characters { 7 - Characters(info: Option(Info), results: Option(List(Character))) 8 - } 9 - 10 - pub fn characters_decoder() -> decode.Decoder(Characters) { 11 - use info <- decode.field("info", decode.optional(info_decoder())) 12 - use results <- decode.field("results", decode.optional(decode.list(character_decoder()))) 13 - decode.success(Characters(info: info, results: results)) 14 - } 15 - 16 - pub type Info { 17 - Info(count: Option(Int)) 18 - } 19 - 20 - pub fn info_decoder() -> decode.Decoder(Info) { 21 - use count <- decode.field("count", decode.optional(decode.int)) 22 - decode.success(Info(count: count)) 23 - } 24 - 25 - pub type Character { 26 - Character(name: Option(String)) 27 - } 28 - 29 - pub fn character_decoder() -> decode.Decoder(Character) { 30 - use name <- decode.field("name", decode.optional(decode.string)) 31 - decode.success(Character(name: name)) 32 - } 33 - 34 - pub type Location { 35 - Location(id: Option(String), name: Option(String)) 36 - } 37 - 38 - pub fn location_decoder() -> decode.Decoder(Location) { 39 - use id <- decode.field("id", decode.optional(decode.string)) 40 - use name <- decode.field("name", decode.optional(decode.string)) 41 - decode.success(Location(id: id, name: name)) 42 - } 43 - 44 - pub type Episode { 45 - Episode(id: Option(String), name: Option(String)) 46 - } 47 - 48 - pub fn episode_decoder() -> decode.Decoder(Episode) { 49 - use id <- decode.field("id", decode.optional(decode.string)) 50 - use name <- decode.field("name", decode.optional(decode.string)) 51 - decode.success(Episode(id: id, name: name)) 52 - } 53 - 54 - pub fn characters_to_json(input: Characters) -> json.Json { 55 - json.object( 56 - [ 57 - #("info", json.nullable(input.info, info_to_json)), 58 - #("results", json.nullable( 59 - input.results, 60 - fn(list) { json.array(from: list, of: character_to_json) }, 61 - )), 62 - ], 63 - ) 64 - } 65 - 66 - pub fn info_to_json(input: Info) -> json.Json { 67 - json.object([#("count", json.nullable(input.count, json.int))]) 68 - } 69 - 70 - pub fn character_to_json(input: Character) -> json.Json { 71 - json.object([#("name", json.nullable(input.name, json.string))]) 72 - } 73 - 74 - pub fn location_to_json(input: Location) -> json.Json { 75 - json.object( 76 - [ 77 - #("id", json.nullable(input.id, json.string)), 78 - #("name", json.nullable(input.name, json.string)), 79 - ], 80 - ) 81 - } 82 - 83 - pub fn episode_to_json(input: Episode) -> json.Json { 84 - json.object( 85 - [ 86 - #("id", json.nullable(input.id, json.string)), 87 - #("name", json.nullable(input.name, json.string)), 88 - ], 89 - ) 90 - } 91 - 92 - pub type MultiQueryWithVarsResponse { 93 - MultiQueryWithVarsResponse( 94 - characters: Option(Characters), 95 - location: Option(Location), 96 - episodes_by_ids: Option(List(Episode)), 97 - ) 98 - } 99 - 100 - pub fn multi_query_with_vars_response_decoder() -> decode.Decoder(MultiQueryWithVarsResponse) { 101 - use characters <- decode.field("characters", decode.optional(characters_decoder())) 102 - use location <- decode.field("location", decode.optional(location_decoder())) 103 - use episodes_by_ids <- decode.field("episodesByIds", decode.optional(decode.list(episode_decoder()))) 104 - decode.success(MultiQueryWithVarsResponse( 105 - characters: characters, 106 - location: location, 107 - episodes_by_ids: episodes_by_ids, 108 - )) 109 - } 110 - 111 - pub fn multi_query_with_vars_response_to_json(input: MultiQueryWithVarsResponse) -> json.Json { 112 - json.object( 113 - [ 114 - #("characters", json.nullable(input.characters, characters_to_json)), 115 - #("location", json.nullable(input.location, location_to_json)), 116 - #("episodesByIds", json.nullable( 117 - input.episodes_by_ids, 118 - fn(list) { json.array(from: list, of: episode_to_json) }, 119 - )), 120 - ], 121 - ) 122 - } 123 - 124 - pub fn multi_query_with_vars(client: squall.Client, page: Int, name: String, location_id: String, episode_ids: List(Int)) { 125 - squall.execute_query( 126 - client, 127 - "query MultiQueryWithVars($page: Int, $name: String, $locationId: ID!, $episodeIds: [Int!]!) { characters(page: $page, filter: { name: $name }) { info { count } results { name } } location(id: $locationId) { id name } episodesByIds(ids: $episodeIds) { id name } }", 128 - json.object( 129 - [ 130 - #("page", json.int(page)), 131 - #("name", json.string(name)), 132 - #("locationId", json.string(location_id)), 133 - #("episodeIds", json.array(from: episode_ids, of: json.int)), 134 - ], 135 - ), 136 - multi_query_with_vars_response_decoder(), 137 - ) 138 - }
examples/01-erlang/src/graphql/multi_query_with_vars.gleam.new

This is a binary file and will not be displayed.

-18
examples/01-erlang/src/graphql/multi_query_with_vars.gql
··· 1 - query MultiQueryWithVars($page: Int, $name: String, $locationId: ID!, $episodeIds: [Int!]!) { 2 - characters(page: $page, filter: { name: $name }) { 3 - info { 4 - count 5 - } 6 - results { 7 - name 8 - } 9 - } 10 - location(id: $locationId) { 11 - id 12 - name 13 - } 14 - episodesByIds(ids: $episodeIds) { 15 - id 16 - name 17 - } 18 - }
-87
examples/02-javascript/README.md
··· 1 - # Squall JavaScript Example 2 - 3 - This example demonstrates using Squall-generated GraphQL clients on the **JavaScript** target (Node.js or browser). 4 - 5 - ## What's Inside 6 - 7 - - `javascript_example.gleam` - Multi-field query example using Fetch API 8 - - `src/graphql/*.gql` - GraphQL query definitions 9 - - `src/graphql/*.gleam` - Generated type-safe Gleam code (same as Erlang!) 10 - 11 - ## HTTP Adapter 12 - 13 - This example uses the **JavaScript adapter** which uses the Fetch API for HTTP requests: 14 - 15 - ```gleam 16 - import squall 17 - 18 - let client = squall.new_javascript_client("https://rickandmortyapi.com/graphql", []) 19 - ``` 20 - 21 - The Fetch API works in: 22 - - **Node.js** v18+ (built-in) 23 - - **Browsers** (all modern browsers) 24 - - **Deno** (built-in) 25 - 26 - ## Running 27 - 28 - ### Node.js (Recommended) 29 - 30 - ```bash 31 - # Build for JavaScript target 32 - gleam build 33 - 34 - # Run the example 35 - gleam run -m javascript_example 36 - ``` 37 - 38 - ### Browser 39 - 40 - 1. Build the JavaScript bundle: 41 - ```bash 42 - gleam build --target javascript 43 - ``` 44 - 45 - 2. The compiled JavaScript will be in `build/dev/javascript/` 46 - 47 - 3. Include it in an HTML file or use with a bundler (Vite, Webpack, etc.) 48 - 49 - ## Requirements 50 - 51 - - **Node.js** v18.0.0 or later (for native Fetch API support) 52 - - Or any modern browser 53 - 54 - ## Regenerating GraphQL Code 55 - 56 - If you modify the `.gql` files, regenerate the code: 57 - 58 - ```bash 59 - # From the squall root directory 60 - cd ../.. 61 - gleam run -m squall generate https://rickandmortyapi.com/graphql 62 - ``` 63 - 64 - This will update the `.gleam` files in `src/graphql/`. 65 - 66 - ## Key Differences from Erlang Example 67 - 68 - The **only difference** between this and the Erlang example is the client creation: 69 - 70 - ```gleam 71 - // JavaScript 72 - squall.new_javascript_client(endpoint, headers) 73 - 74 - // vs Erlang 75 - squall.new_erlang_client(endpoint, headers) 76 - ``` 77 - 78 - **Everything else is identical!** The same generated GraphQL code works on both targets. 79 - 80 - ## Features Demonstrated 81 - 82 - ✅ **JavaScript Runtime** - Uses Fetch API (Node.js/Browser) 83 - ✅ **Type Safety** - Fully typed based on GraphQL schema 84 - ✅ **Isomorphic Code** - Same generated code as Erlang example 85 - ✅ **Nested Objects** - Automatic decoder generation 86 - ✅ **Optional Fields** - Proper `Option` type handling 87 - ✅ **JSON Serialization** - Convert responses back to JSON
-14
examples/02-javascript/gleam.toml
··· 1 - name = "javascript_example" 2 - version = "0.1.0" 3 - description = "Squall JavaScript example - Rick and Morty GraphQL API" 4 - target = "javascript" 5 - 6 - [dependencies] 7 - gleam_stdlib = ">= 0.65.0 and < 0.66.0" 8 - gleam_json = ">= 3.0.0 and < 4.0.0" 9 - gleam_http = ">= 4.3.0 and < 5.0.0" 10 - gleam_javascript = ">= 0.3.0 and < 2.0.0" 11 - squall = { path = "../.." } 12 - 13 - [javascript] 14 - runtime = "node"
-24
examples/02-javascript/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 = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, 7 - { name = "glam", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "237C2CE218A2A0A5D46D625F8EF5B78F964BC91018B78D692B17E1AB84295229" }, 8 - { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, 9 - { name = "gleam_fetch", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "2CBF9F2E1C71AEBBFB13A9D5720CD8DB4263EB02FE60C5A7A1C6E17B0151C20C" }, 10 - { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, 11 - { name = "gleam_httpc", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C545172618D07811494E97AAA4A0FB34DA6F6D0061FDC8041C2F8E3BE2B2E48F" }, 12 - { name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" }, 13 - { name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" }, 14 - { name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" }, 15 - { name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" }, 16 - { name = "squall", version = "0.1.0", build_tools = ["gleam"], requirements = ["argv", "filepath", "glam", "gleam_fetch", "gleam_http", "gleam_httpc", "gleam_json", "gleam_stdlib", "simplifile"], source = "local", path = "../.." }, 17 - ] 18 - 19 - [requirements] 20 - gleam_http = { version = ">= 4.3.0 and < 5.0.0" } 21 - gleam_javascript = { version = ">= 0.3.0 and < 2.0.0" } 22 - gleam_json = { version = ">= 3.0.0 and < 4.0.0" } 23 - gleam_stdlib = { version = ">= 0.65.0 and < 0.66.0" } 24 - squall = { path = "../.." }
-71
examples/02-javascript/src/graphql/get_character.gleam
··· 1 - import gleam/dynamic/decode 2 - import gleam/json 3 - import squall 4 - import gleam/option.{type Option} 5 - 6 - pub type Character { 7 - Character( 8 - id: Option(String), 9 - name: Option(String), 10 - status: Option(String), 11 - species: Option(String), 12 - type_: Option(String), 13 - gender: Option(String), 14 - ) 15 - } 16 - 17 - pub fn character_decoder() -> decode.Decoder(Character) { 18 - use id <- decode.field("id", decode.optional(decode.string)) 19 - use name <- decode.field("name", decode.optional(decode.string)) 20 - use status <- decode.field("status", decode.optional(decode.string)) 21 - use species <- decode.field("species", decode.optional(decode.string)) 22 - use type_ <- decode.field("type", decode.optional(decode.string)) 23 - use gender <- decode.field("gender", decode.optional(decode.string)) 24 - decode.success(Character( 25 - id: id, 26 - name: name, 27 - status: status, 28 - species: species, 29 - type_: type_, 30 - gender: gender, 31 - )) 32 - } 33 - 34 - pub fn character_to_json(input: Character) -> json.Json { 35 - json.object( 36 - [ 37 - #("id", json.nullable(input.id, json.string)), 38 - #("name", json.nullable(input.name, json.string)), 39 - #("status", json.nullable(input.status, json.string)), 40 - #("species", json.nullable(input.species, json.string)), 41 - #("type", json.nullable(input.type_, json.string)), 42 - #("gender", json.nullable(input.gender, json.string)), 43 - ], 44 - ) 45 - } 46 - 47 - pub type GetCharacterResponse { 48 - GetCharacterResponse(character: Option(Character)) 49 - } 50 - 51 - pub fn get_character_response_decoder() -> decode.Decoder(GetCharacterResponse) { 52 - use character <- decode.field("character", decode.optional(character_decoder())) 53 - decode.success(GetCharacterResponse(character: character)) 54 - } 55 - 56 - pub fn get_character_response_to_json(input: GetCharacterResponse) -> json.Json { 57 - json.object( 58 - [ 59 - #("character", json.nullable(input.character, character_to_json)), 60 - ], 61 - ) 62 - } 63 - 64 - pub fn get_character(client: squall.Client, id: String) { 65 - squall.execute_query( 66 - client, 67 - "query GetCharacter($id: ID!) { character(id: $id) { id name status species type gender } }", 68 - json.object([#("id", json.string(id))]), 69 - get_character_response_decoder(), 70 - ) 71 - }
-10
examples/02-javascript/src/graphql/get_character.gql
··· 1 - query GetCharacter($id: ID!) { 2 - character(id: $id) { 3 - id 4 - name 5 - status 6 - species 7 - type 8 - gender 9 - } 10 - }
-78
examples/02-javascript/src/graphql/get_characters.gleam
··· 1 - import gleam/dynamic/decode 2 - import gleam/json 3 - import squall 4 - import gleam/option.{type Option} 5 - 6 - pub type Characters { 7 - Characters(results: Option(List(Character))) 8 - } 9 - 10 - pub fn characters_decoder() -> decode.Decoder(Characters) { 11 - use results <- decode.field("results", decode.optional(decode.list(character_decoder()))) 12 - decode.success(Characters(results: results)) 13 - } 14 - 15 - pub type Character { 16 - Character( 17 - id: Option(String), 18 - name: Option(String), 19 - status: Option(String), 20 - species: Option(String), 21 - ) 22 - } 23 - 24 - pub fn character_decoder() -> decode.Decoder(Character) { 25 - use id <- decode.field("id", decode.optional(decode.string)) 26 - use name <- decode.field("name", decode.optional(decode.string)) 27 - use status <- decode.field("status", decode.optional(decode.string)) 28 - use species <- decode.field("species", decode.optional(decode.string)) 29 - decode.success(Character(id: id, name: name, status: status, species: species)) 30 - } 31 - 32 - pub fn characters_to_json(input: Characters) -> json.Json { 33 - json.object( 34 - [ 35 - #("results", json.nullable( 36 - input.results, 37 - fn(list) { json.array(from: list, of: character_to_json) }, 38 - )), 39 - ], 40 - ) 41 - } 42 - 43 - pub fn character_to_json(input: Character) -> json.Json { 44 - json.object( 45 - [ 46 - #("id", json.nullable(input.id, json.string)), 47 - #("name", json.nullable(input.name, json.string)), 48 - #("status", json.nullable(input.status, json.string)), 49 - #("species", json.nullable(input.species, json.string)), 50 - ], 51 - ) 52 - } 53 - 54 - pub type GetCharactersResponse { 55 - GetCharactersResponse(characters: Option(Characters)) 56 - } 57 - 58 - pub fn get_characters_response_decoder() -> decode.Decoder(GetCharactersResponse) { 59 - use characters <- decode.field("characters", decode.optional(characters_decoder())) 60 - decode.success(GetCharactersResponse(characters: characters)) 61 - } 62 - 63 - pub fn get_characters_response_to_json(input: GetCharactersResponse) -> json.Json { 64 - json.object( 65 - [ 66 - #("characters", json.nullable(input.characters, characters_to_json)), 67 - ], 68 - ) 69 - } 70 - 71 - pub fn get_characters(client: squall.Client) { 72 - squall.execute_query( 73 - client, 74 - "query GetCharacters { characters { results { id name status species } } }", 75 - json.object([]), 76 - get_characters_response_decoder(), 77 - ) 78 - }
-10
examples/02-javascript/src/graphql/get_characters.gql
··· 1 - query GetCharacters { 2 - characters { 3 - results { 4 - id 5 - name 6 - status 7 - species 8 - } 9 - } 10 - }
-119
examples/02-javascript/src/graphql/multi_query.gleam
··· 1 - import gleam/dynamic/decode 2 - import gleam/json 3 - import squall 4 - import gleam/option.{type Option} 5 - 6 - pub type Characters { 7 - Characters(info: Option(Info), results: Option(List(Character))) 8 - } 9 - 10 - pub fn characters_decoder() -> decode.Decoder(Characters) { 11 - use info <- decode.field("info", decode.optional(info_decoder())) 12 - use results <- decode.field("results", decode.optional(decode.list(character_decoder()))) 13 - decode.success(Characters(info: info, results: results)) 14 - } 15 - 16 - pub type Info { 17 - Info(count: Option(Int)) 18 - } 19 - 20 - pub fn info_decoder() -> decode.Decoder(Info) { 21 - use count <- decode.field("count", decode.optional(decode.int)) 22 - decode.success(Info(count: count)) 23 - } 24 - 25 - pub type Character { 26 - Character(name: Option(String)) 27 - } 28 - 29 - pub fn character_decoder() -> decode.Decoder(Character) { 30 - use name <- decode.field("name", decode.optional(decode.string)) 31 - decode.success(Character(name: name)) 32 - } 33 - 34 - pub type Location { 35 - Location(id: Option(String)) 36 - } 37 - 38 - pub fn location_decoder() -> decode.Decoder(Location) { 39 - use id <- decode.field("id", decode.optional(decode.string)) 40 - decode.success(Location(id: id)) 41 - } 42 - 43 - pub type Episode { 44 - Episode(id: Option(String)) 45 - } 46 - 47 - pub fn episode_decoder() -> decode.Decoder(Episode) { 48 - use id <- decode.field("id", decode.optional(decode.string)) 49 - decode.success(Episode(id: id)) 50 - } 51 - 52 - pub fn characters_to_json(input: Characters) -> json.Json { 53 - json.object( 54 - [ 55 - #("info", json.nullable(input.info, info_to_json)), 56 - #("results", json.nullable( 57 - input.results, 58 - fn(list) { json.array(from: list, of: character_to_json) }, 59 - )), 60 - ], 61 - ) 62 - } 63 - 64 - pub fn info_to_json(input: Info) -> json.Json { 65 - json.object([#("count", json.nullable(input.count, json.int))]) 66 - } 67 - 68 - pub fn character_to_json(input: Character) -> json.Json { 69 - json.object([#("name", json.nullable(input.name, json.string))]) 70 - } 71 - 72 - pub fn location_to_json(input: Location) -> json.Json { 73 - json.object([#("id", json.nullable(input.id, json.string))]) 74 - } 75 - 76 - pub fn episode_to_json(input: Episode) -> json.Json { 77 - json.object([#("id", json.nullable(input.id, json.string))]) 78 - } 79 - 80 - pub type MultiQueryResponse { 81 - MultiQueryResponse( 82 - characters: Option(Characters), 83 - location: Option(Location), 84 - episodes_by_ids: Option(List(Episode)), 85 - ) 86 - } 87 - 88 - pub fn multi_query_response_decoder() -> decode.Decoder(MultiQueryResponse) { 89 - use characters <- decode.field("characters", decode.optional(characters_decoder())) 90 - use location <- decode.field("location", decode.optional(location_decoder())) 91 - use episodes_by_ids <- decode.field("episodesByIds", decode.optional(decode.list(episode_decoder()))) 92 - decode.success(MultiQueryResponse( 93 - characters: characters, 94 - location: location, 95 - episodes_by_ids: episodes_by_ids, 96 - )) 97 - } 98 - 99 - pub fn multi_query_response_to_json(input: MultiQueryResponse) -> json.Json { 100 - json.object( 101 - [ 102 - #("characters", json.nullable(input.characters, characters_to_json)), 103 - #("location", json.nullable(input.location, location_to_json)), 104 - #("episodesByIds", json.nullable( 105 - input.episodes_by_ids, 106 - fn(list) { json.array(from: list, of: episode_to_json) }, 107 - )), 108 - ], 109 - ) 110 - } 111 - 112 - pub fn multi_query(client: squall.Client) { 113 - squall.execute_query( 114 - client, 115 - "query MultiQuery { characters(page: 2, filter: { name: \"rick\" }) { info { count } results { name } } location(id: 1) { id } episodesByIds(ids: [1, 2]) { id } }", 116 - json.object([]), 117 - multi_query_response_decoder(), 118 - ) 119 - }
-16
examples/02-javascript/src/graphql/multi_query.gql
··· 1 - query MultiQuery { 2 - characters(page: 2, filter: { name: "rick" }) { 3 - info { 4 - count 5 - } 6 - results { 7 - name 8 - } 9 - } 10 - location(id: 1) { 11 - id 12 - } 13 - episodesByIds(ids: [1, 2]) { 14 - id 15 - } 16 - }
-138
examples/02-javascript/src/graphql/multi_query_with_vars.gleam
··· 1 - import gleam/dynamic/decode 2 - import gleam/json 3 - import squall 4 - import gleam/option.{type Option} 5 - 6 - pub type Characters { 7 - Characters(info: Option(Info), results: Option(List(Character))) 8 - } 9 - 10 - pub fn characters_decoder() -> decode.Decoder(Characters) { 11 - use info <- decode.field("info", decode.optional(info_decoder())) 12 - use results <- decode.field("results", decode.optional(decode.list(character_decoder()))) 13 - decode.success(Characters(info: info, results: results)) 14 - } 15 - 16 - pub type Info { 17 - Info(count: Option(Int)) 18 - } 19 - 20 - pub fn info_decoder() -> decode.Decoder(Info) { 21 - use count <- decode.field("count", decode.optional(decode.int)) 22 - decode.success(Info(count: count)) 23 - } 24 - 25 - pub type Character { 26 - Character(name: Option(String)) 27 - } 28 - 29 - pub fn character_decoder() -> decode.Decoder(Character) { 30 - use name <- decode.field("name", decode.optional(decode.string)) 31 - decode.success(Character(name: name)) 32 - } 33 - 34 - pub type Location { 35 - Location(id: Option(String), name: Option(String)) 36 - } 37 - 38 - pub fn location_decoder() -> decode.Decoder(Location) { 39 - use id <- decode.field("id", decode.optional(decode.string)) 40 - use name <- decode.field("name", decode.optional(decode.string)) 41 - decode.success(Location(id: id, name: name)) 42 - } 43 - 44 - pub type Episode { 45 - Episode(id: Option(String), name: Option(String)) 46 - } 47 - 48 - pub fn episode_decoder() -> decode.Decoder(Episode) { 49 - use id <- decode.field("id", decode.optional(decode.string)) 50 - use name <- decode.field("name", decode.optional(decode.string)) 51 - decode.success(Episode(id: id, name: name)) 52 - } 53 - 54 - pub fn characters_to_json(input: Characters) -> json.Json { 55 - json.object( 56 - [ 57 - #("info", json.nullable(input.info, info_to_json)), 58 - #("results", json.nullable( 59 - input.results, 60 - fn(list) { json.array(from: list, of: character_to_json) }, 61 - )), 62 - ], 63 - ) 64 - } 65 - 66 - pub fn info_to_json(input: Info) -> json.Json { 67 - json.object([#("count", json.nullable(input.count, json.int))]) 68 - } 69 - 70 - pub fn character_to_json(input: Character) -> json.Json { 71 - json.object([#("name", json.nullable(input.name, json.string))]) 72 - } 73 - 74 - pub fn location_to_json(input: Location) -> json.Json { 75 - json.object( 76 - [ 77 - #("id", json.nullable(input.id, json.string)), 78 - #("name", json.nullable(input.name, json.string)), 79 - ], 80 - ) 81 - } 82 - 83 - pub fn episode_to_json(input: Episode) -> json.Json { 84 - json.object( 85 - [ 86 - #("id", json.nullable(input.id, json.string)), 87 - #("name", json.nullable(input.name, json.string)), 88 - ], 89 - ) 90 - } 91 - 92 - pub type MultiQueryWithVarsResponse { 93 - MultiQueryWithVarsResponse( 94 - characters: Option(Characters), 95 - location: Option(Location), 96 - episodes_by_ids: Option(List(Episode)), 97 - ) 98 - } 99 - 100 - pub fn multi_query_with_vars_response_decoder() -> decode.Decoder(MultiQueryWithVarsResponse) { 101 - use characters <- decode.field("characters", decode.optional(characters_decoder())) 102 - use location <- decode.field("location", decode.optional(location_decoder())) 103 - use episodes_by_ids <- decode.field("episodesByIds", decode.optional(decode.list(episode_decoder()))) 104 - decode.success(MultiQueryWithVarsResponse( 105 - characters: characters, 106 - location: location, 107 - episodes_by_ids: episodes_by_ids, 108 - )) 109 - } 110 - 111 - pub fn multi_query_with_vars_response_to_json(input: MultiQueryWithVarsResponse) -> json.Json { 112 - json.object( 113 - [ 114 - #("characters", json.nullable(input.characters, characters_to_json)), 115 - #("location", json.nullable(input.location, location_to_json)), 116 - #("episodesByIds", json.nullable( 117 - input.episodes_by_ids, 118 - fn(list) { json.array(from: list, of: episode_to_json) }, 119 - )), 120 - ], 121 - ) 122 - } 123 - 124 - pub fn multi_query_with_vars(client: squall.Client, page: Int, name: String, location_id: String, episode_ids: List(Int)) { 125 - squall.execute_query( 126 - client, 127 - "query MultiQueryWithVars($page: Int, $name: String, $locationId: ID!, $episodeIds: [Int!]!) { characters(page: $page, filter: { name: $name }) { info { count } results { name } } location(id: $locationId) { id name } episodesByIds(ids: $episodeIds) { id name } }", 128 - json.object( 129 - [ 130 - #("page", json.int(page)), 131 - #("name", json.string(name)), 132 - #("locationId", json.string(location_id)), 133 - #("episodeIds", json.array(from: episode_ids, of: json.int)), 134 - ], 135 - ), 136 - multi_query_with_vars_response_decoder(), 137 - ) 138 - }
-18
examples/02-javascript/src/graphql/multi_query_with_vars.gql
··· 1 - query MultiQueryWithVars($page: Int, $name: String, $locationId: ID!, $episodeIds: [Int!]!) { 2 - characters(page: $page, filter: { name: $name }) { 3 - info { 4 - count 5 - } 6 - results { 7 - name 8 - } 9 - } 10 - location(id: $locationId) { 11 - id 12 - name 13 - } 14 - episodesByIds(ids: $episodeIds) { 15 - id 16 - name 17 - } 18 - }
-43
examples/02-javascript/src/javascript_example.gleam
··· 1 - import gleam/io 2 - import gleam/javascript/promise 3 - import gleam/json 4 - import gleam/string 5 - import graphql/multi_query 6 - import squall 7 - 8 - pub fn main() { 9 - io.println("Squall Multi-Field Query Example (JavaScript/Node.js)") 10 - io.println("======================================================\n") 11 - 12 - // Create a JavaScript client (uses Fetch API for HTTP requests) 13 - let client = 14 - squall.new_javascript_client("https://rickandmortyapi.com/graphql", []) 15 - 16 - // On JavaScript, multi_query returns a Promise that resolves to a Result 17 - // We use promise.await to wait for the result 18 - multi_query.multi_query(client) 19 - |> promise.await(fn(result) { 20 - case result { 21 - Ok(response) -> { 22 - io.println("✓ Success! Received response from API\n") 23 - 24 - // Print the Gleam data structure 25 - io.println("Gleam Response Structure:") 26 - io.println("-------------------------") 27 - io.println(string.inspect(response)) 28 - 29 - // Convert to JSON and print 30 - io.println("\nJSON Response:") 31 - io.println("--------------") 32 - let json_response = multi_query.multi_query_response_to_json(response) 33 - io.println(json.to_string(json_response)) 34 - 35 - promise.resolve(Nil) 36 - } 37 - Error(err) -> { 38 - io.println("✗ Error: " <> err) 39 - promise.resolve(Nil) 40 - } 41 - } 42 - }) 43 - }
-135
examples/03-isomorphic/README.md
··· 1 - # Squall Isomorphic Example 2 - 3 - This example demonstrates writing **cross-platform** Gleam code that works on **both Erlang and JavaScript** targets using Squall. 4 - 5 - ## What's Inside 6 - 7 - - `isomorphic_example.gleam` - Code that runs on both targets 8 - - `src/graphql/*.gql` - GraphQL query definitions 9 - - `src/graphql/*.gleam` - Generated type-safe Gleam code 10 - 11 - ## The Magic: Target-Conditional Code 12 - 13 - The same codebase works on both Erlang and JavaScript using `@target()` attributes: 14 - 15 - ```gleam 16 - import squall 17 - import graphql/multi_query 18 - 19 - @target(javascript) 20 - import gleam/javascript/promise 21 - 22 - // Shared business logic - works on both targets! 23 - fn handle_response(response: multi_query.MultiQueryResponse) -> Nil { 24 - io.println("✓ Success!") 25 - io.println(string.inspect(response)) 26 - } 27 - 28 - fn handle_error(err: String) -> Nil { 29 - io.println("✗ Error: " <> err) 30 - } 31 - 32 - // Erlang implementation - synchronous 33 - @target(erlang) 34 - fn run() -> Nil { 35 - let client = squall.new_erlang_client("https://rickandmortyapi.com/graphql", []) 36 - let result = multi_query.multi_query(client) 37 - 38 - case result { 39 - Ok(response) -> handle_response(response) 40 - Error(err) -> handle_error(err) 41 - } 42 - } 43 - 44 - // JavaScript implementation - asynchronous with Promises 45 - @target(javascript) 46 - fn run() -> promise.Promise(Nil) { 47 - let client = squall.new_javascript_client("https://rickandmortyapi.com/graphql", []) 48 - 49 - multi_query.multi_query(client) 50 - |> promise.await(fn(result) { 51 - case result { 52 - Ok(response) -> { 53 - handle_response(response) 54 - promise.resolve(Nil) 55 - } 56 - Error(err) -> { 57 - handle_error(err) 58 - promise.resolve(Nil) 59 - } 60 - } 61 - }) 62 - } 63 - 64 - pub fn main() { 65 - run() // Works on both targets! 66 - } 67 - ``` 68 - 69 - ## Running on Erlang 70 - 71 - ```bash 72 - # Build and run on Erlang 73 - gleam run -m isomorphic_example 74 - ``` 75 - 76 - Output: `"Running on Erlang target (using gleam_httpc)"` 77 - 78 - ## Running on JavaScript 79 - 80 - ```bash 81 - # Build and run on JavaScript (Node.js) 82 - gleam run --target javascript -m isomorphic_example 83 - ``` 84 - 85 - Output: `"Running on JavaScript target (using Fetch API)"` 86 - 87 - ## How It Works 88 - 89 - 1. **Compile time**: Gleam includes only the code for the target platform 90 - - On Erlang: Only the `@target(erlang)` functions are compiled 91 - - On JavaScript: Only the `@target(javascript)` functions are compiled 92 - 93 - 2. **Runtime**: The appropriate HTTP adapter is used automatically 94 - - Erlang → `gleam_httpc` (synchronous, returns `Result`) 95 - - JavaScript → Fetch API (asynchronous, returns `Promise(Result)`) 96 - 97 - 3. **Shared business logic**: Response/error handlers work on both targets 98 - - `handle_response()` and `handle_error()` are platform-agnostic 99 - - Only the HTTP execution layer differs between platforms 100 - 101 - 4. **Generated code**: Completely platform-agnostic! 102 - - The same GraphQL code works everywhere 103 - - No platform-specific imports in generated files 104 - - Automatically adapts to sync (Erlang) or async (JavaScript) execution 105 - 106 - ## Build Artifacts 107 - 108 - ### Erlang Target 109 - ```bash 110 - gleam build 111 - # Outputs to: build/dev/erlang/ 112 - ``` 113 - 114 - ### JavaScript Target 115 - ```bash 116 - gleam build --target javascript 117 - # Outputs to: build/dev/javascript/ 118 - ``` 119 - 120 - ## Use Cases 121 - 122 - This pattern is perfect for: 123 - 124 - ✅ **Full-stack Gleam apps** - Share GraphQL clients between backend (Erlang) and frontend (JavaScript) 125 - ✅ **Multi-platform libraries** - Write once, run everywhere 126 - ✅ **Migration scenarios** - Gradually move from one target to another 127 - ✅ **Testing** - Test the same business logic on different runtimes 128 - 129 - ## Features Demonstrated 130 - 131 - ✅ **Cross-Platform Code** - Single codebase for both targets 132 - ✅ **Conditional Compilation** - `@target()` attributes 133 - ✅ **HTTP Adapter Pattern** - Platform-specific implementations 134 - ✅ **Type Safety** - Fully typed across all targets 135 - ✅ **Zero Duplication** - Generated code shared between targets
-10
examples/03-isomorphic/gleam.toml
··· 1 - name = "isomorphic_example" 2 - version = "0.1.0" 3 - description = "Squall isomorphic example - Works on Erlang AND JavaScript" 4 - 5 - [dependencies] 6 - gleam_stdlib = ">= 0.65.0 and < 0.66.0" 7 - gleam_json = ">= 3.0.0 and < 4.0.0" 8 - gleam_http = ">= 4.3.0 and < 5.0.0" 9 - gleam_javascript = ">= 0.3.0 and < 2.0.0" 10 - squall = { path = "../.." }
-24
examples/03-isomorphic/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 = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, 7 - { name = "glam", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "237C2CE218A2A0A5D46D625F8EF5B78F964BC91018B78D692B17E1AB84295229" }, 8 - { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, 9 - { name = "gleam_fetch", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "2CBF9F2E1C71AEBBFB13A9D5720CD8DB4263EB02FE60C5A7A1C6E17B0151C20C" }, 10 - { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, 11 - { name = "gleam_httpc", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C545172618D07811494E97AAA4A0FB34DA6F6D0061FDC8041C2F8E3BE2B2E48F" }, 12 - { name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" }, 13 - { name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" }, 14 - { name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" }, 15 - { name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" }, 16 - { name = "squall", version = "0.1.0", build_tools = ["gleam"], requirements = ["argv", "filepath", "glam", "gleam_fetch", "gleam_http", "gleam_httpc", "gleam_json", "gleam_stdlib", "simplifile"], source = "local", path = "../.." }, 17 - ] 18 - 19 - [requirements] 20 - gleam_http = { version = ">= 4.3.0 and < 5.0.0" } 21 - gleam_javascript = { version = ">= 0.3.0 and < 2.0.0" } 22 - gleam_json = { version = ">= 3.0.0 and < 4.0.0" } 23 - gleam_stdlib = { version = ">= 0.65.0 and < 0.66.0" } 24 - squall = { path = "../.." }
-71
examples/03-isomorphic/src/graphql/get_character.gleam
··· 1 - import gleam/dynamic/decode 2 - import gleam/json 3 - import squall 4 - import gleam/option.{type Option} 5 - 6 - pub type Character { 7 - Character( 8 - id: Option(String), 9 - name: Option(String), 10 - status: Option(String), 11 - species: Option(String), 12 - type_: Option(String), 13 - gender: Option(String), 14 - ) 15 - } 16 - 17 - pub fn character_decoder() -> decode.Decoder(Character) { 18 - use id <- decode.field("id", decode.optional(decode.string)) 19 - use name <- decode.field("name", decode.optional(decode.string)) 20 - use status <- decode.field("status", decode.optional(decode.string)) 21 - use species <- decode.field("species", decode.optional(decode.string)) 22 - use type_ <- decode.field("type", decode.optional(decode.string)) 23 - use gender <- decode.field("gender", decode.optional(decode.string)) 24 - decode.success(Character( 25 - id: id, 26 - name: name, 27 - status: status, 28 - species: species, 29 - type_: type_, 30 - gender: gender, 31 - )) 32 - } 33 - 34 - pub fn character_to_json(input: Character) -> json.Json { 35 - json.object( 36 - [ 37 - #("id", json.nullable(input.id, json.string)), 38 - #("name", json.nullable(input.name, json.string)), 39 - #("status", json.nullable(input.status, json.string)), 40 - #("species", json.nullable(input.species, json.string)), 41 - #("type", json.nullable(input.type_, json.string)), 42 - #("gender", json.nullable(input.gender, json.string)), 43 - ], 44 - ) 45 - } 46 - 47 - pub type GetCharacterResponse { 48 - GetCharacterResponse(character: Option(Character)) 49 - } 50 - 51 - pub fn get_character_response_decoder() -> decode.Decoder(GetCharacterResponse) { 52 - use character <- decode.field("character", decode.optional(character_decoder())) 53 - decode.success(GetCharacterResponse(character: character)) 54 - } 55 - 56 - pub fn get_character_response_to_json(input: GetCharacterResponse) -> json.Json { 57 - json.object( 58 - [ 59 - #("character", json.nullable(input.character, character_to_json)), 60 - ], 61 - ) 62 - } 63 - 64 - pub fn get_character(client: squall.Client, id: String) { 65 - squall.execute_query( 66 - client, 67 - "query GetCharacter($id: ID!) { character(id: $id) { id name status species type gender } }", 68 - json.object([#("id", json.string(id))]), 69 - get_character_response_decoder(), 70 - ) 71 - }
-10
examples/03-isomorphic/src/graphql/get_character.gql
··· 1 - query GetCharacter($id: ID!) { 2 - character(id: $id) { 3 - id 4 - name 5 - status 6 - species 7 - type 8 - gender 9 - } 10 - }
-78
examples/03-isomorphic/src/graphql/get_characters.gleam
··· 1 - import gleam/dynamic/decode 2 - import gleam/json 3 - import squall 4 - import gleam/option.{type Option} 5 - 6 - pub type Characters { 7 - Characters(results: Option(List(Character))) 8 - } 9 - 10 - pub fn characters_decoder() -> decode.Decoder(Characters) { 11 - use results <- decode.field("results", decode.optional(decode.list(character_decoder()))) 12 - decode.success(Characters(results: results)) 13 - } 14 - 15 - pub type Character { 16 - Character( 17 - id: Option(String), 18 - name: Option(String), 19 - status: Option(String), 20 - species: Option(String), 21 - ) 22 - } 23 - 24 - pub fn character_decoder() -> decode.Decoder(Character) { 25 - use id <- decode.field("id", decode.optional(decode.string)) 26 - use name <- decode.field("name", decode.optional(decode.string)) 27 - use status <- decode.field("status", decode.optional(decode.string)) 28 - use species <- decode.field("species", decode.optional(decode.string)) 29 - decode.success(Character(id: id, name: name, status: status, species: species)) 30 - } 31 - 32 - pub fn characters_to_json(input: Characters) -> json.Json { 33 - json.object( 34 - [ 35 - #("results", json.nullable( 36 - input.results, 37 - fn(list) { json.array(from: list, of: character_to_json) }, 38 - )), 39 - ], 40 - ) 41 - } 42 - 43 - pub fn character_to_json(input: Character) -> json.Json { 44 - json.object( 45 - [ 46 - #("id", json.nullable(input.id, json.string)), 47 - #("name", json.nullable(input.name, json.string)), 48 - #("status", json.nullable(input.status, json.string)), 49 - #("species", json.nullable(input.species, json.string)), 50 - ], 51 - ) 52 - } 53 - 54 - pub type GetCharactersResponse { 55 - GetCharactersResponse(characters: Option(Characters)) 56 - } 57 - 58 - pub fn get_characters_response_decoder() -> decode.Decoder(GetCharactersResponse) { 59 - use characters <- decode.field("characters", decode.optional(characters_decoder())) 60 - decode.success(GetCharactersResponse(characters: characters)) 61 - } 62 - 63 - pub fn get_characters_response_to_json(input: GetCharactersResponse) -> json.Json { 64 - json.object( 65 - [ 66 - #("characters", json.nullable(input.characters, characters_to_json)), 67 - ], 68 - ) 69 - } 70 - 71 - pub fn get_characters(client: squall.Client) { 72 - squall.execute_query( 73 - client, 74 - "query GetCharacters { characters { results { id name status species } } }", 75 - json.object([]), 76 - get_characters_response_decoder(), 77 - ) 78 - }
-10
examples/03-isomorphic/src/graphql/get_characters.gql
··· 1 - query GetCharacters { 2 - characters { 3 - results { 4 - id 5 - name 6 - status 7 - species 8 - } 9 - } 10 - }
-119
examples/03-isomorphic/src/graphql/multi_query.gleam
··· 1 - import gleam/dynamic/decode 2 - import gleam/json 3 - import squall 4 - import gleam/option.{type Option} 5 - 6 - pub type Characters { 7 - Characters(info: Option(Info), results: Option(List(Character))) 8 - } 9 - 10 - pub fn characters_decoder() -> decode.Decoder(Characters) { 11 - use info <- decode.field("info", decode.optional(info_decoder())) 12 - use results <- decode.field("results", decode.optional(decode.list(character_decoder()))) 13 - decode.success(Characters(info: info, results: results)) 14 - } 15 - 16 - pub type Info { 17 - Info(count: Option(Int)) 18 - } 19 - 20 - pub fn info_decoder() -> decode.Decoder(Info) { 21 - use count <- decode.field("count", decode.optional(decode.int)) 22 - decode.success(Info(count: count)) 23 - } 24 - 25 - pub type Character { 26 - Character(name: Option(String)) 27 - } 28 - 29 - pub fn character_decoder() -> decode.Decoder(Character) { 30 - use name <- decode.field("name", decode.optional(decode.string)) 31 - decode.success(Character(name: name)) 32 - } 33 - 34 - pub type Location { 35 - Location(id: Option(String)) 36 - } 37 - 38 - pub fn location_decoder() -> decode.Decoder(Location) { 39 - use id <- decode.field("id", decode.optional(decode.string)) 40 - decode.success(Location(id: id)) 41 - } 42 - 43 - pub type Episode { 44 - Episode(id: Option(String)) 45 - } 46 - 47 - pub fn episode_decoder() -> decode.Decoder(Episode) { 48 - use id <- decode.field("id", decode.optional(decode.string)) 49 - decode.success(Episode(id: id)) 50 - } 51 - 52 - pub fn characters_to_json(input: Characters) -> json.Json { 53 - json.object( 54 - [ 55 - #("info", json.nullable(input.info, info_to_json)), 56 - #("results", json.nullable( 57 - input.results, 58 - fn(list) { json.array(from: list, of: character_to_json) }, 59 - )), 60 - ], 61 - ) 62 - } 63 - 64 - pub fn info_to_json(input: Info) -> json.Json { 65 - json.object([#("count", json.nullable(input.count, json.int))]) 66 - } 67 - 68 - pub fn character_to_json(input: Character) -> json.Json { 69 - json.object([#("name", json.nullable(input.name, json.string))]) 70 - } 71 - 72 - pub fn location_to_json(input: Location) -> json.Json { 73 - json.object([#("id", json.nullable(input.id, json.string))]) 74 - } 75 - 76 - pub fn episode_to_json(input: Episode) -> json.Json { 77 - json.object([#("id", json.nullable(input.id, json.string))]) 78 - } 79 - 80 - pub type MultiQueryResponse { 81 - MultiQueryResponse( 82 - characters: Option(Characters), 83 - location: Option(Location), 84 - episodes_by_ids: Option(List(Episode)), 85 - ) 86 - } 87 - 88 - pub fn multi_query_response_decoder() -> decode.Decoder(MultiQueryResponse) { 89 - use characters <- decode.field("characters", decode.optional(characters_decoder())) 90 - use location <- decode.field("location", decode.optional(location_decoder())) 91 - use episodes_by_ids <- decode.field("episodesByIds", decode.optional(decode.list(episode_decoder()))) 92 - decode.success(MultiQueryResponse( 93 - characters: characters, 94 - location: location, 95 - episodes_by_ids: episodes_by_ids, 96 - )) 97 - } 98 - 99 - pub fn multi_query_response_to_json(input: MultiQueryResponse) -> json.Json { 100 - json.object( 101 - [ 102 - #("characters", json.nullable(input.characters, characters_to_json)), 103 - #("location", json.nullable(input.location, location_to_json)), 104 - #("episodesByIds", json.nullable( 105 - input.episodes_by_ids, 106 - fn(list) { json.array(from: list, of: episode_to_json) }, 107 - )), 108 - ], 109 - ) 110 - } 111 - 112 - pub fn multi_query(client: squall.Client) { 113 - squall.execute_query( 114 - client, 115 - "query MultiQuery { characters(page: 2, filter: { name: \"rick\" }) { info { count } results { name } } location(id: 1) { id } episodesByIds(ids: [1, 2]) { id } }", 116 - json.object([]), 117 - multi_query_response_decoder(), 118 - ) 119 - }
-16
examples/03-isomorphic/src/graphql/multi_query.gql
··· 1 - query MultiQuery { 2 - characters(page: 2, filter: { name: "rick" }) { 3 - info { 4 - count 5 - } 6 - results { 7 - name 8 - } 9 - } 10 - location(id: 1) { 11 - id 12 - } 13 - episodesByIds(ids: [1, 2]) { 14 - id 15 - } 16 - }
-138
examples/03-isomorphic/src/graphql/multi_query_with_vars.gleam
··· 1 - import gleam/dynamic/decode 2 - import gleam/json 3 - import squall 4 - import gleam/option.{type Option} 5 - 6 - pub type Characters { 7 - Characters(info: Option(Info), results: Option(List(Character))) 8 - } 9 - 10 - pub fn characters_decoder() -> decode.Decoder(Characters) { 11 - use info <- decode.field("info", decode.optional(info_decoder())) 12 - use results <- decode.field("results", decode.optional(decode.list(character_decoder()))) 13 - decode.success(Characters(info: info, results: results)) 14 - } 15 - 16 - pub type Info { 17 - Info(count: Option(Int)) 18 - } 19 - 20 - pub fn info_decoder() -> decode.Decoder(Info) { 21 - use count <- decode.field("count", decode.optional(decode.int)) 22 - decode.success(Info(count: count)) 23 - } 24 - 25 - pub type Character { 26 - Character(name: Option(String)) 27 - } 28 - 29 - pub fn character_decoder() -> decode.Decoder(Character) { 30 - use name <- decode.field("name", decode.optional(decode.string)) 31 - decode.success(Character(name: name)) 32 - } 33 - 34 - pub type Location { 35 - Location(id: Option(String), name: Option(String)) 36 - } 37 - 38 - pub fn location_decoder() -> decode.Decoder(Location) { 39 - use id <- decode.field("id", decode.optional(decode.string)) 40 - use name <- decode.field("name", decode.optional(decode.string)) 41 - decode.success(Location(id: id, name: name)) 42 - } 43 - 44 - pub type Episode { 45 - Episode(id: Option(String), name: Option(String)) 46 - } 47 - 48 - pub fn episode_decoder() -> decode.Decoder(Episode) { 49 - use id <- decode.field("id", decode.optional(decode.string)) 50 - use name <- decode.field("name", decode.optional(decode.string)) 51 - decode.success(Episode(id: id, name: name)) 52 - } 53 - 54 - pub fn characters_to_json(input: Characters) -> json.Json { 55 - json.object( 56 - [ 57 - #("info", json.nullable(input.info, info_to_json)), 58 - #("results", json.nullable( 59 - input.results, 60 - fn(list) { json.array(from: list, of: character_to_json) }, 61 - )), 62 - ], 63 - ) 64 - } 65 - 66 - pub fn info_to_json(input: Info) -> json.Json { 67 - json.object([#("count", json.nullable(input.count, json.int))]) 68 - } 69 - 70 - pub fn character_to_json(input: Character) -> json.Json { 71 - json.object([#("name", json.nullable(input.name, json.string))]) 72 - } 73 - 74 - pub fn location_to_json(input: Location) -> json.Json { 75 - json.object( 76 - [ 77 - #("id", json.nullable(input.id, json.string)), 78 - #("name", json.nullable(input.name, json.string)), 79 - ], 80 - ) 81 - } 82 - 83 - pub fn episode_to_json(input: Episode) -> json.Json { 84 - json.object( 85 - [ 86 - #("id", json.nullable(input.id, json.string)), 87 - #("name", json.nullable(input.name, json.string)), 88 - ], 89 - ) 90 - } 91 - 92 - pub type MultiQueryWithVarsResponse { 93 - MultiQueryWithVarsResponse( 94 - characters: Option(Characters), 95 - location: Option(Location), 96 - episodes_by_ids: Option(List(Episode)), 97 - ) 98 - } 99 - 100 - pub fn multi_query_with_vars_response_decoder() -> decode.Decoder(MultiQueryWithVarsResponse) { 101 - use characters <- decode.field("characters", decode.optional(characters_decoder())) 102 - use location <- decode.field("location", decode.optional(location_decoder())) 103 - use episodes_by_ids <- decode.field("episodesByIds", decode.optional(decode.list(episode_decoder()))) 104 - decode.success(MultiQueryWithVarsResponse( 105 - characters: characters, 106 - location: location, 107 - episodes_by_ids: episodes_by_ids, 108 - )) 109 - } 110 - 111 - pub fn multi_query_with_vars_response_to_json(input: MultiQueryWithVarsResponse) -> json.Json { 112 - json.object( 113 - [ 114 - #("characters", json.nullable(input.characters, characters_to_json)), 115 - #("location", json.nullable(input.location, location_to_json)), 116 - #("episodesByIds", json.nullable( 117 - input.episodes_by_ids, 118 - fn(list) { json.array(from: list, of: episode_to_json) }, 119 - )), 120 - ], 121 - ) 122 - } 123 - 124 - pub fn multi_query_with_vars(client: squall.Client, page: Int, name: String, location_id: String, episode_ids: List(Int)) { 125 - squall.execute_query( 126 - client, 127 - "query MultiQueryWithVars($page: Int, $name: String, $locationId: ID!, $episodeIds: [Int!]!) { characters(page: $page, filter: { name: $name }) { info { count } results { name } } location(id: $locationId) { id name } episodesByIds(ids: $episodeIds) { id name } }", 128 - json.object( 129 - [ 130 - #("page", json.int(page)), 131 - #("name", json.string(name)), 132 - #("locationId", json.string(location_id)), 133 - #("episodeIds", json.array(from: episode_ids, of: json.int)), 134 - ], 135 - ), 136 - multi_query_with_vars_response_decoder(), 137 - ) 138 - }
-18
examples/03-isomorphic/src/graphql/multi_query_with_vars.gql
··· 1 - query MultiQueryWithVars($page: Int, $name: String, $locationId: ID!, $episodeIds: [Int!]!) { 2 - characters(page: $page, filter: { name: $name }) { 3 - info { 4 - count 5 - } 6 - results { 7 - name 8 - } 9 - } 10 - location(id: $locationId) { 11 - id 12 - name 13 - } 14 - episodesByIds(ids: $episodeIds) { 15 - id 16 - name 17 - } 18 - }
-76
examples/03-isomorphic/src/isomorphic_example.gleam
··· 1 - import gleam/io 2 - import gleam/json 3 - import gleam/string 4 - import graphql/multi_query 5 - import squall 6 - 7 - @target(javascript) 8 - import gleam/javascript/promise 9 - 10 - // This example demonstrates how to write isomorphic code 11 - // that works on both Erlang and JavaScript targets 12 - 13 - pub fn main() { 14 - io.println("Squall Isomorphic Example") 15 - io.println("=========================\n") 16 - 17 - // Use target-specific main implementations 18 - run() 19 - } 20 - 21 - // Shared function to handle successful responses 22 - fn handle_response(response: multi_query.MultiQueryResponse) -> Nil { 23 - io.println("✓ Success! Received response from API\n") 24 - 25 - // Print the Gleam data structure 26 - io.println("Gleam Response Structure:") 27 - io.println("-------------------------") 28 - io.println(string.inspect(response)) 29 - 30 - // Convert to JSON and print 31 - io.println("\nJSON Response:") 32 - io.println("--------------") 33 - let json_response = multi_query.multi_query_response_to_json(response) 34 - io.println(json.to_string(json_response)) 35 - } 36 - 37 - // Shared function to handle errors 38 - fn handle_error(err: String) -> Nil { 39 - io.println("✗ Error: " <> err) 40 - } 41 - 42 - // Erlang implementation - synchronous 43 - @target(erlang) 44 - fn run() -> Nil { 45 - io.println("Running on Erlang target (using gleam_httpc)\n") 46 - 47 - let client = squall.new_erlang_client("https://rickandmortyapi.com/graphql", []) 48 - let result = multi_query.multi_query(client) 49 - 50 - case result { 51 - Ok(response) -> handle_response(response) 52 - Error(err) -> handle_error(err) 53 - } 54 - } 55 - 56 - // JavaScript implementation - asynchronous 57 - @target(javascript) 58 - fn run() -> promise.Promise(Nil) { 59 - io.println("Running on JavaScript target (using Fetch API)\n") 60 - 61 - let client = squall.new_javascript_client("https://rickandmortyapi.com/graphql", []) 62 - 63 - multi_query.multi_query(client) 64 - |> promise.await(fn(result) { 65 - case result { 66 - Ok(response) -> { 67 - handle_response(response) 68 - promise.resolve(Nil) 69 - } 70 - Error(err) -> { 71 - handle_error(err) 72 - promise.resolve(Nil) 73 - } 74 - } 75 - }) 76 - }
-177
examples/README.md
··· 1 - # Squall Examples 2 - 3 - Welcome to the Squall examples! These demonstrate how to use Squall's **isomorphic GraphQL client generator** with the Rick and Morty API. 4 - 5 - ## 🌊 What is Squall? 6 - 7 - Squall generates **type-safe GraphQL clients** for Gleam that work on **any target** - Erlang, JavaScript (Browser), and JavaScript (Node.js). Write `.gql` files, and Squall generates fully-typed Gleam code with proper types, decoders, and serializers. 8 - 9 - ## Examples Overview 10 - 11 - ### [01-erlang](./01-erlang/) - Erlang/OTP Target 12 - 13 - Demonstrates Squall on the Erlang/OTP runtime using `gleam_httpc`. 14 - 15 - ```bash 16 - cd 01-erlang 17 - gleam run -m erlang_example 18 - ``` 19 - 20 - **Perfect for:** Backend services, servers, distributed systems 21 - 22 - ### [02-javascript](./02-javascript/) - JavaScript Target 23 - 24 - Demonstrates Squall on JavaScript (Node.js/Browser) using the Fetch API. 25 - 26 - ```bash 27 - cd 02-javascript 28 - gleam run -m javascript_example 29 - ``` 30 - 31 - **Perfect for:** Frontend apps, serverless functions, Deno 32 - 33 - ### [03-isomorphic](./03-isomorphic/) - Cross-Platform 34 - 35 - Demonstrates writing code that works on **both** Erlang and JavaScript! 36 - 37 - ```bash 38 - cd 03-isomorphic 39 - 40 - # Run on Erlang 41 - gleam run -m isomorphic_example 42 - 43 - # Run on JavaScript 44 - gleam run --target javascript -m isomorphic_example 45 - ``` 46 - 47 - **Perfect for:** Full-stack apps, shared libraries, universal code 48 - 49 - ## Quick Start 50 - 51 - ### 1. Generate GraphQL Code 52 - 53 - From the Squall root directory: 54 - 55 - ```bash 56 - gleam run -m squall generate https://rickandmortyapi.com/graphql 57 - ``` 58 - 59 - This discovers all `.gql` files in the examples and generates type-safe Gleam code. 60 - 61 - ### 2. Choose Your Target 62 - 63 - Pick the example that matches your use case: 64 - 65 - | Target | Example | Use Case | 66 - |--------|---------|----------| 67 - | **Erlang** | `01-erlang` | Backend, servers | 68 - | **JavaScript** | `02-javascript` | Frontend, Node.js | 69 - | **Both** | `03-isomorphic` | Full-stack apps | 70 - 71 - ### 3. Run It! 72 - 73 - ```bash 74 - cd <example-folder> 75 - gleam run 76 - ``` 77 - 78 - ## The Key Difference 79 - 80 - The **only difference** between Erlang and JavaScript code is the client creation: 81 - 82 - ```gleam 83 - // Erlang 84 - let client = squall.new_erlang_client(endpoint, headers) 85 - 86 - // JavaScript 87 - let client = squall.new_javascript_client(endpoint, headers) 88 - 89 - // Cross-platform (uses @target conditionals) 90 - @target(erlang) 91 - fn create_client() -> squall.Client { 92 - squall.new_erlang_client(endpoint, headers) 93 - } 94 - 95 - @target(javascript) 96 - fn create_client() -> squall.Client { 97 - squall.new_javascript_client(endpoint, headers) 98 - } 99 - ``` 100 - 101 - **Everything else is identical!** The same generated GraphQL code works everywhere. 102 - 103 - ## API Endpoint 104 - 105 - All examples use the public Rick and Morty GraphQL API: 106 - 107 - ``` 108 - https://rickandmortyapi.com/graphql 109 - ``` 110 - 111 - No API key required! 112 - 113 - ## GraphQL Queries 114 - 115 - Each example includes the same `.gql` files: 116 - 117 - - **`get_character.gql`** - Fetch a single character by ID 118 - - **`get_characters.gql`** - Fetch a list of characters 119 - - **`multi_query.gql`** - Multiple fields in one query 120 - - **`multi_query_with_vars.gql`** - Query with GraphQL variables 121 - 122 - ## Features Demonstrated 123 - 124 - ✅ **Type Safety** - All queries fully typed from GraphQL schema 125 - ✅ **Isomorphic** - Same code works on Erlang and JavaScript 126 - ✅ **Variables** - Type-safe GraphQL variable handling 127 - ✅ **Nested Objects** - Automatic decoder generation for complex types 128 - ✅ **Optional Fields** - Proper `Option` type handling 129 - ✅ **JSON Serialization** - Convert responses to/from JSON 130 - ✅ **HTTP Adapters** - Platform-specific HTTP implementations 131 - 132 - ## Project Structure 133 - 134 - ``` 135 - examples/ 136 - ├── README.md # This file 137 - ├── 01-erlang/ # Erlang example 138 - │ ├── gleam.toml 139 - │ ├── README.md 140 - │ └── src/ 141 - │ ├── erlang_example.gleam 142 - │ ├── example_with_vars.gleam 143 - │ └── graphql/ 144 - │ ├── *.gql # GraphQL queries 145 - │ └── *.gleam # Generated code 146 - ├── 02-javascript/ # JavaScript example 147 - │ ├── gleam.toml 148 - │ ├── README.md 149 - │ └── src/ 150 - │ ├── javascript_example.gleam 151 - │ └── graphql/ 152 - │ ├── *.gql 153 - │ └── *.gleam 154 - └── 03-isomorphic/ # Cross-platform example 155 - ├── gleam.toml 156 - ├── README.md 157 - └── src/ 158 - ├── isomorphic_example.gleam 159 - └── graphql/ 160 - ├── *.gql 161 - └── *.gleam 162 - ``` 163 - 164 - ## Next Steps 165 - 166 - 1. **Explore the examples** - Start with the one matching your target 167 - 2. **Read the individual READMEs** - Each has detailed instructions 168 - 3. **Modify the queries** - Edit `.gql` files and regenerate 169 - 4. **Try your own API** - Point Squall at your GraphQL endpoint! 170 - 171 - ## Learn More 172 - 173 - - [Squall Documentation](../../CLAUDE.md) 174 - - [Rick and Morty GraphQL API](https://rickandmortyapi.com/documentation/#graphql) 175 - - [Gleam Language](https://gleam.run) 176 - 177 - Happy querying! 🌊
+65 -154
src/squall.gleam
··· 1 1 import argv 2 2 import gleam/dynamic/decode 3 3 import gleam/http 4 - import gleam/http/request 4 + import gleam/http/request.{type Request} 5 5 import gleam/httpc 6 6 import gleam/io 7 7 import gleam/json 8 8 import gleam/list 9 9 import gleam/result 10 10 import gleam/string 11 - import squall/adapter.{type HttpAdapter} 12 11 13 12 @target(erlang) 14 13 import simplifile ··· 28 27 @target(erlang) 29 28 import squall/internal/schema 30 29 31 - @target(erlang) 32 - import squall/adapter/erlang 33 - 34 - @target(javascript) 35 - import squall/adapter/javascript 36 - 37 - @target(javascript) 38 - import gleam/javascript/promise.{type Promise} 39 - 40 - /// A GraphQL client with endpoint, headers, and HTTP adapter configuration 30 + /// A GraphQL client with endpoint and headers configuration. 31 + /// This client follows the sans-io pattern: it builds HTTP requests but doesn't send them. 32 + /// You must use your own HTTP client to send the requests. 41 33 pub type Client { 42 - Client( 43 - endpoint: String, 44 - headers: List(#(String, String)), 45 - send_request: HttpAdapter, 46 - ) 34 + Client(endpoint: String, headers: List(#(String, String))) 47 35 } 48 36 49 - /// Create a new GraphQL client with custom headers and HTTP adapter. 50 - /// For most cases, use target-specific constructors like `new_erlang_client` or `new_javascript_client`. 51 - pub fn new_client( 52 - endpoint: String, 53 - headers: List(#(String, String)), 54 - send_request: HttpAdapter, 55 - ) -> Client { 56 - Client(endpoint: endpoint, headers: headers, send_request: send_request) 57 - } 58 - 59 - /// Create a new GraphQL client with bearer token authentication and HTTP adapter. 60 - pub fn new_client_with_auth( 61 - endpoint: String, 62 - token: String, 63 - send_request: HttpAdapter, 64 - ) -> Client { 65 - Client( 66 - endpoint: endpoint, 67 - headers: [#("Authorization", "Bearer " <> token)], 68 - send_request: send_request, 69 - ) 70 - } 71 - 72 - @target(erlang) 73 - /// Create a new Erlang GraphQL client with custom headers. 74 - /// This uses the Erlang HTTP adapter (gleam_httpc). 75 - pub fn new_erlang_client( 76 - endpoint: String, 77 - headers: List(#(String, String)), 78 - ) -> Client { 79 - Client(endpoint: endpoint, headers: headers, send_request: erlang.adapter()) 37 + /// Create a new GraphQL client with custom headers. 38 + /// 39 + /// ## Example 40 + /// 41 + /// ```gleam 42 + /// let client = squall.new("https://api.example.com/graphql", []) 43 + /// ``` 44 + pub fn new(endpoint: String, headers: List(#(String, String))) -> Client { 45 + Client(endpoint: endpoint, headers: headers) 80 46 } 81 47 82 - @target(erlang) 83 - /// Create a new Erlang GraphQL client with bearer token authentication. 84 - pub fn new_erlang_client_with_auth(endpoint: String, token: String) -> Client { 85 - Client( 86 - endpoint: endpoint, 87 - headers: [#("Authorization", "Bearer " <> token)], 88 - send_request: erlang.adapter(), 89 - ) 48 + /// Create a new GraphQL client with bearer token authentication. 49 + /// 50 + /// ## Example 51 + /// 52 + /// ```gleam 53 + /// let client = squall.new_with_auth("https://api.example.com/graphql", "my-token") 54 + /// ``` 55 + pub fn new_with_auth(endpoint: String, token: String) -> Client { 56 + Client(endpoint: endpoint, headers: [#("Authorization", "Bearer " <> token)]) 90 57 } 91 58 92 - @target(javascript) 93 - /// Create a new JavaScript GraphQL client with custom headers. 94 - /// This uses the JavaScript HTTP adapter (Fetch API). 95 - pub fn new_javascript_client( 96 - endpoint: String, 97 - headers: List(#(String, String)), 98 - ) -> Client { 99 - Client( 100 - endpoint: endpoint, 101 - headers: headers, 102 - send_request: javascript.adapter(), 103 - ) 104 - } 105 - 106 - @target(javascript) 107 - /// Create a new JavaScript GraphQL client with bearer token authentication. 108 - pub fn new_javascript_client_with_auth( 109 - endpoint: String, 110 - token: String, 111 - ) -> Client { 112 - Client( 113 - endpoint: endpoint, 114 - headers: [#("Authorization", "Bearer " <> token)], 115 - send_request: javascript.adapter(), 116 - ) 117 - } 118 - 119 - @target(javascript) 120 - /// Execute a GraphQL query on JavaScript targets. 121 - /// Returns a Promise that resolves to a Result containing the decoded response. 122 - pub fn execute_query( 59 + /// Prepare an HTTP request for a GraphQL query. 60 + /// This function builds the request but does not send it. 61 + /// You must send the request using your own HTTP client. 62 + /// 63 + /// ## Example 64 + /// 65 + /// ```gleam 66 + /// let client = squall.new("https://api.example.com/graphql", []) 67 + /// let request = squall.prepare_request( 68 + /// client, 69 + /// "query { users { id name } }", 70 + /// json.object([]), 71 + /// ) 72 + /// 73 + /// // Send with your HTTP client (Erlang example) 74 + /// let assert Ok(response) = httpc.send(request) 75 + /// 76 + /// // Parse the response 77 + /// let assert Ok(data) = squall.parse_response(response.body, your_decoder) 78 + /// ``` 79 + pub fn prepare_request( 123 80 client: Client, 124 81 query: String, 125 82 variables: json.Json, 126 - decoder: decode.Decoder(a), 127 - ) -> Promise(Result(a, String)) { 128 - let body = 129 - json.object([#("query", json.string(query)), #("variables", variables)]) 130 - 131 - // Build the request 132 - let req_result = 133 - request.to(client.endpoint) 134 - |> result.map_error(fn(_) { "Invalid endpoint URL" }) 135 - |> result.map(fn(req) { 136 - req 137 - |> request.set_method(http.Post) 138 - |> request.set_body(json.to_string(body)) 139 - |> request.set_header("content-type", "application/json") 140 - }) 141 - |> result.map(fn(req) { 142 - list.fold(client.headers, req, fn(r, header) { 143 - request.set_header(r, header.0, header.1) 144 - }) 145 - }) 146 - 147 - // Convert Result to Promise 148 - case req_result { 149 - Error(e) -> promise.resolve(Error(e)) 150 - Ok(req) -> { 151 - // Send request and process response 152 - client.send_request(req) 153 - |> promise.map(result.map_error(_, fn(_) { "HTTP request failed" })) 154 - |> promise.try_await(fn(resp) { 155 - // Parse JSON 156 - let json_result = 157 - json.parse(from: resp.body, using: decode.dynamic) 158 - |> result.map_error(fn(_) { "Failed to decode JSON response" }) 159 - 160 - case json_result { 161 - Error(e) -> promise.resolve(Error(e)) 162 - Ok(json_value) -> { 163 - // Decode response 164 - let data_decoder = { 165 - use data <- decode.field("data", decoder) 166 - decode.success(data) 167 - } 168 - 169 - decode.run(json_value, data_decoder) 170 - |> result.map_error(fn(_) { "Failed to decode response data" }) 171 - |> promise.resolve 172 - } 173 - } 174 - }) 175 - } 176 - } 177 - } 178 - 179 - @target(erlang) 180 - /// Execute a GraphQL query on Erlang targets. 181 - /// Returns a Result containing the decoded response. 182 - pub fn execute_query( 183 - client: Client, 184 - query: String, 185 - variables: json.Json, 186 - decoder: decode.Decoder(a), 187 - ) -> Result(a, String) { 83 + ) -> Result(Request(String), String) { 188 84 let body = 189 85 json.object([#("query", json.string(query)), #("variables", variables)]) 190 86 ··· 204 100 request.set_header(r, header.0, header.1) 205 101 }) 206 102 207 - use resp <- result.try( 208 - client.send_request(req) 209 - |> result.map_error(fn(_) { "HTTP request failed" }), 210 - ) 103 + Ok(req) 104 + } 211 105 106 + /// Parse a GraphQL response body using the provided decoder. 107 + /// This function decodes the JSON response and extracts the data field. 108 + /// 109 + /// ## Example 110 + /// 111 + /// ```gleam 112 + /// let decoder = decode.field("users", decode.list(user_decoder)) 113 + /// 114 + /// case squall.parse_response(response_body, decoder) { 115 + /// Ok(users) -> io.println("Got users!") 116 + /// Error(err) -> io.println("Parse error: " <> err) 117 + /// } 118 + /// ``` 119 + pub fn parse_response( 120 + body: String, 121 + decoder: decode.Decoder(a), 122 + ) -> Result(a, String) { 212 123 use json_value <- result.try( 213 - json.parse(from: resp.body, using: decode.dynamic) 124 + json.parse(from: body, using: decode.dynamic) 214 125 |> result.map_error(fn(_) { "Failed to decode JSON response" }), 215 126 ) 216 127
-17
src/squall/adapter.gleam
··· 1 - import gleam/http/request.{type Request} 2 - import gleam/http/response.{type Response} 3 - 4 - @target(javascript) 5 - import gleam/javascript/promise.{type Promise} 6 - 7 - @target(erlang) 8 - /// HTTP adapter function type for Erlang target. 9 - /// Synchronously returns a Result with Response or error string. 10 - pub type HttpAdapter = 11 - fn(Request(String)) -> Result(Response(String), String) 12 - 13 - @target(javascript) 14 - /// HTTP adapter function type for JavaScript target. 15 - /// Asynchronously returns a Promise containing a Result with Response or error string. 16 - pub type HttpAdapter = 17 - fn(Request(String)) -> Promise(Result(Response(String), String))
-20
src/squall/adapter/erlang.gleam
··· 1 - @target(erlang) 2 - import gleam/http/request.{type Request} 3 - @target(erlang) 4 - import gleam/http/response.{type Response} 5 - @target(erlang) 6 - import gleam/httpc 7 - @target(erlang) 8 - import gleam/result 9 - @target(erlang) 10 - import squall/adapter.{type HttpAdapter} 11 - 12 - @target(erlang) 13 - /// Erlang-specific HTTP adapter using gleam_httpc. 14 - /// This adapter works on the Erlang/OTP target. 15 - pub fn adapter() -> HttpAdapter { 16 - fn(req: Request(String)) -> Result(Response(String), String) { 17 - httpc.send(req) 18 - |> result.map_error(fn(_) { "HTTP request failed" }) 19 - } 20 - }
-26
src/squall/adapter/javascript.gleam
··· 1 - @target(javascript) 2 - import gleam/fetch 3 - @target(javascript) 4 - import gleam/http/request 5 - @target(javascript) 6 - import gleam/http/response 7 - @target(javascript) 8 - import gleam/javascript/promise.{type Promise} 9 - @target(javascript) 10 - import gleam/result 11 - @target(javascript) 12 - import squall/adapter 13 - 14 - @target(javascript) 15 - /// JavaScript-specific HTTP adapter using the Fetch API via gleam_fetch. 16 - /// This adapter works on both browser and Node.js JavaScript targets. 17 - /// Returns a Promise that resolves to a Result. 18 - pub fn adapter() -> adapter.HttpAdapter { 19 - fn(req: request.Request(String)) -> Promise( 20 - Result(response.Response(String), String), 21 - ) { 22 - fetch.send(req) 23 - |> promise.try_await(fetch.read_text_body) 24 - |> promise.map(result.map_error(_, fn(_) { "HTTP request failed" })) 25 - } 26 - }
+51 -7
src/squall/internal/codegen.gleam
··· 211 211 // Minimal imports - just what we need for decoders and the squall client 212 212 let core_imports = [ 213 213 "import gleam/dynamic/decode", 214 + "import gleam/http/request.{type Request}", 214 215 "import gleam/json", 215 216 "import squall", 216 217 ] ··· 1195 1196 } 1196 1197 } 1197 1198 1198 - // Generate function 1199 + // Generate two functions: one to prepare the request, one to parse the response 1199 1200 fn generate_function( 1200 1201 operation_name: String, 1201 1202 response_type_name: String, 1203 + variables: List(graphql_ast.Variable), 1204 + query_string: String, 1205 + schema_types: dict.Dict(String, schema.Type), 1206 + ) -> Document { 1207 + // Generate the prepare request function 1208 + let prepare_function = generate_prepare_function( 1209 + operation_name, 1210 + variables, 1211 + query_string, 1212 + schema_types, 1213 + ) 1214 + 1215 + // Generate the parse response function 1216 + let parse_function = generate_parse_function(operation_name, response_type_name) 1217 + 1218 + // Combine both functions with a line break 1219 + doc.concat([prepare_function, doc.lines(2), parse_function]) 1220 + } 1221 + 1222 + // Generate the function that prepares the HTTP request 1223 + fn generate_prepare_function( 1224 + operation_name: String, 1202 1225 variables: List(graphql_ast.Variable), 1203 1226 query_string: String, 1204 1227 schema_types: dict.Dict(String, schema.Type), ··· 1270 1293 } 1271 1294 } 1272 1295 1273 - // Build simplified function body that just calls squall.execute_query 1296 + // Build function body that calls squall.prepare_request 1274 1297 let body_doc = 1275 - call_doc("squall.execute_query", [ 1298 + call_doc("squall.prepare_request", [ 1276 1299 doc.from_string("client"), 1277 1300 string_doc(query_string), 1278 1301 variables_code, 1279 - doc.from_string(snake_case(response_type_name) <> "_decoder()"), 1280 1302 ]) 1281 1303 1282 - // Build function signature 1283 - // The return type is handled by squall.execute_query (Promise on JS, Result on Erlang) 1304 + // Build function signature with explicit return type 1284 1305 doc.concat([ 1285 1306 doc.from_string("pub fn " <> function_name), 1286 1307 comma_list("(", param_docs, ")"), 1287 - doc.from_string(" "), 1308 + doc.from_string(" -> Result(Request(String), String) "), 1309 + block([body_doc]), 1310 + ]) 1311 + } 1312 + 1313 + // Generate the function that parses the response body 1314 + fn generate_parse_function( 1315 + operation_name: String, 1316 + response_type_name: String, 1317 + ) -> Document { 1318 + let function_name = "parse_" <> operation_name <> "_response" 1319 + let decoder_name = snake_case(response_type_name) <> "_decoder" 1320 + 1321 + let body_doc = 1322 + call_doc("squall.parse_response", [ 1323 + doc.from_string("body"), 1324 + doc.from_string(decoder_name <> "()"), 1325 + ]) 1326 + 1327 + doc.concat([ 1328 + doc.from_string("pub fn " <> function_name), 1329 + doc.from_string("(body: String) -> Result("), 1330 + doc.from_string(response_type_name), 1331 + doc.from_string(", String) "), 1288 1332 block([body_doc]), 1289 1333 ]) 1290 1334 }