-331
CLAUDE.md
-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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+
+17
example/gleam.toml
+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
+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
+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
-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
-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
+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
-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
-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
-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
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
-2
examples/01-erlang/src/graphql/get_character.gql
example/src/graphql/get_character.gql
-78
examples/01-erlang/src/graphql/get_characters.gleam
-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
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
-10
examples/01-erlang/src/graphql/get_characters.gql
-119
examples/01-erlang/src/graphql/multi_query.gleam
-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
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
-16
examples/01-erlang/src/graphql/multi_query.gql
-138
examples/01-erlang/src/graphql/multi_query_with_vars.gleam
-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
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
-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
-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
-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
-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
-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
-10
examples/02-javascript/src/graphql/get_character.gql
-78
examples/02-javascript/src/graphql/get_characters.gleam
-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
-10
examples/02-javascript/src/graphql/get_characters.gql
-119
examples/02-javascript/src/graphql/multi_query.gleam
-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
-16
examples/02-javascript/src/graphql/multi_query.gql
-138
examples/02-javascript/src/graphql/multi_query_with_vars.gleam
-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
-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
-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
-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
-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
-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
-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
-10
examples/03-isomorphic/src/graphql/get_character.gql
-78
examples/03-isomorphic/src/graphql/get_characters.gleam
-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
-10
examples/03-isomorphic/src/graphql/get_characters.gql
-119
examples/03-isomorphic/src/graphql/multi_query.gleam
-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
-16
examples/03-isomorphic/src/graphql/multi_query.gql
-138
examples/03-isomorphic/src/graphql/multi_query_with_vars.gleam
-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
-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
-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
-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
+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
-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
-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
-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
+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
}