diff --git a/CLAUDE.md b/CLAUDE.md index 69d530f..973200d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,19 +10,26 @@ 1. **Convention over Configuration**: No config files, just drop `.gql` files in the right place 2. **Type Safety First**: All generated code is fully typed based on GraphQL schema -3. **Test-Driven Development**: Every feature has tests written first -4. **Modular Design**: Clear separation of concerns across modules +3. **Isomorphic by Design**: Generated client code works on any Gleam target (Erlang, JavaScript/Browser, JavaScript/Node) +4. **Test-Driven Development**: Every feature has tests written first +5. **Modular Design**: Clear separation of concerns across modules ### Module Breakdown ``` -src/squall/internal/ -├── error.gleam # Comprehensive error types -├── discovery.gleam # File discovery and validation -├── parser.gleam # GraphQL lexer + recursive descent parser -├── schema.gleam # GraphQL schema introspection -├── type_mapping.gleam # GraphQL to Gleam type conversion -└── codegen.gleam # Code generation with glam +src/squall/ +├── squall.gleam # Public API & Client type +├── adapter.gleam # HTTP adapter interface +├── adapter/ +│ ├── erlang.gleam # Erlang HTTP adapter (uses gleam_httpc) +│ └── javascript.gleam # JavaScript HTTP adapter (uses Fetch API) +└── internal/ + ├── error.gleam # Comprehensive error types + ├── discovery.gleam # File discovery and validation + ├── parser.gleam # GraphQL lexer + recursive descent parser + ├── schema.gleam # GraphQL schema introspection + ├── type_mapping.gleam # GraphQL to Gleam type conversion + └── codegen.gleam # Code generation with glam ``` ### Data Flow @@ -51,7 +58,35 @@ GraphQL types map to Gleam types: - `[Type]` → `List(Type)` - Custom objects → Custom Gleam types -### 3. Testing Strategy +### 3. HTTP Adapter Pattern + +**Why Adapters?** +To make generated code isomorphic (work across Gleam targets), Squall uses an adapter pattern for HTTP requests: + +- **`squall.Client`** contains a `send_request: HttpAdapter` field +- **`HttpAdapter`** is a function type: `fn(Request(String)) -> Result(Response(String), String)` +- **Platform-specific adapters**: + - `squall/adapter/erlang.gleam` - Uses `gleam_httpc` for Erlang target + - `squall/adapter/javascript.gleam` - Uses Fetch API for JavaScript targets (browser/Node) +- **Generated code** calls `client.send_request(req)` instead of directly using `httpc` + +**Usage Example:** +```gleam +// Erlang target +import squall +let client = squall.new_erlang_client("https://api.example.com/graphql", []) + +// JavaScript target +import squall +let client = squall.new_javascript_client("https://api.example.com/graphql", []) + +// Generic (with explicit adapter) +import squall +import squall/adapter/erlang +let client = squall.new_client("https://api.example.com/graphql", [], erlang.adapter()) +``` + +### 4. Testing Strategy **TDD Approach:** 1. Write test first (in `test/` directory) @@ -65,12 +100,15 @@ GraphQL types map to Gleam types: - **Snapshot tests**: Using `birdie` for code generation validation - **Integration tests**: Test file embedded in `test/fixtures/graphql/` -### 4. Code Generation +### 5. Code Generation Uses the `glam` library for pretty-printing. Generated code includes: -- Type definitions (custom types for responses) +- Type definitions (custom types for responses and inputs) - JSON decoders (using `gleam/dynamic`) -- HTTP client functions (using `gleam_http` + `gleam_httpc`) +- JSON serializers (for converting types back to JSON) +- HTTP client functions (using `squall.Client` with adapter pattern) + +**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. ## File Conventions @@ -192,13 +230,26 @@ Example: Adding fragment support 3. **Review the diff** to ensure it's correct 4. **Accept snapshots**: `BIRDIE_ACCEPT=1 gleam test` +## Isomorphic Architecture Benefits + +✅ **Achieved:** +- Generated client code works on Erlang, JavaScript/Browser, and JavaScript/Node +- No platform-specific imports in generated code +- Users can easily create custom adapters for other HTTP libraries +- Generator (CLI) remains Erlang-only (appropriate for build tools) + +🎯 **How It Works:** +1. **Generation Time**: CLI runs on Erlang, uses `gleam_httpc` for schema introspection +2. **Runtime**: Generated code uses `squall.Client` with platform-specific adapter +3. **User Code**: Chooses adapter based on target (Erlang vs JavaScript) + ## Known Limitations -1. **HTTP Client**: Currently returns an error - needs full implementation -2. **Fragments**: Not yet implemented -3. **Directives**: Not yet handled (@include, @skip, etc.) -4. **Custom Scalars**: All map to String by default -5. **Introspection**: Doesn't handle deeply nested types beyond 5 levels +1. **Fragments**: Not yet implemented +2. **Directives**: Not yet handled (@include, @skip, etc.) +3. **Custom Scalars**: Most map to String by default (except JSON which maps to `json.Json`) +4. **Introspection**: Doesn't handle deeply nested types beyond 5 levels +5. **JavaScript FFI**: Fetch adapter requires testing on actual JavaScript runtime ## Code Style @@ -241,10 +292,10 @@ Example: Adding fragment support ### High Priority -- [ ] Implement full HTTP client (replace stub in `make_graphql_request`) +- [ ] Test JavaScript adapter on actual JavaScript runtime (browser & Node.js) - [ ] Add fragment support - [ ] Handle GraphQL directives -- [ ] Custom scalar type mapping +- [ ] Custom scalar type mapping with user configuration ### Medium Priority @@ -252,6 +303,7 @@ Example: Adding fragment support - [ ] Watch mode for development - [ ] Better error messages with source code snippets - [ ] Cache schema introspection results +- [ ] WebSocket adapter for subscriptions ### Low Priority diff --git a/README.md b/README.md index 25c6ff8..174bd5c 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,20 @@ # 🌊 Squall -A type-safe GraphQL client generator for Gleam, inspired by [squirrel](https://github.com/giacomocavalieri/squirrel). +A type-safe **isomorphic** GraphQL client generator for Gleam. -Squall parses `.gql` files in your project, introspects your GraphQL endpoint, and generates fully type-safe Gleam code with decoders, and keeps your GraphQL queries in sync with your schema. +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! > **⚠️ Warning**: This project is in early development and may contain bugs. Also hasn't been published yet. ## Features +✨ **Isomorphic** - Works on Erlang (backend) and JavaScript (browser/Node.js) - Type-safe code generation from GraphQL schema - Convention over configuration - `.gql` files in `src/**/graphql/` directories - Schema introspection from GraphQL endpoints - Supports queries, mutations, and subscriptions +- Target-specific HTTP adapters (gleam_httpc for Erlang, Fetch API for JavaScript) - Client abstraction with authentication and custom headers -- Multiple endpoint support ## Installation @@ -22,8 +23,13 @@ Add squall to your `gleam.toml`: ```toml [dependencies] squall = "0.1.0" + +# If using JavaScript target, also add: +gleam_javascript = ">= 0.3.0 and < 2.0.0" ``` +The `gleam_javascript` dependency provides Promise support needed for async operations on the JavaScript target. + ## Quick Start ### 1. Create a GraphQL query file @@ -54,14 +60,21 @@ import squall import my_app/graphql/get_character pub fn main() { - // Create a client - let client = squall.new_client( - endpoint: "https://rickandmortyapi.com/graphql", - headers: [] + // Create a client for your target + // On Erlang: + let client = squall.new_erlang_client( + "https://rickandmortyapi.com/graphql", + [] ) - // Use the generated function - case get_character.get_character(client: client, id: "1") { + // On JavaScript: + // let client = squall.new_javascript_client( + // "https://rickandmortyapi.com/graphql", + // [] + // ) + + // Use the generated function - works on both targets! + case get_character.get_character(client, "1") { Ok(response) -> { io.println("Character: " <> response.character.name) } @@ -72,35 +85,98 @@ pub fn main() { } ``` +**Note**: On JavaScript, the generated function returns a `Promise(Result(...))` instead of `Result(...)`. See the [examples](./examples/) for how to handle both targets. + ## Creating a Client -Squall uses a client abstraction to manage your GraphQL endpoint and headers (including authentication): +Squall provides target-specific client constructors: + +### Erlang Target ```gleam import squall -// Create a client with custom headers -let client = squall.new_client( - endpoint: "https://api.example.com/graphql", - headers: [ - #("Authorization", "Bearer your-api-token-here"), - #("X-Custom-Header", "value") - ] +// Basic client +let client = squall.new_erlang_client( + "https://api.example.com/graphql", + [#("X-Custom-Header", "value")] ) -// Or use the convenience function for bearer token auth -let client = squall.new_client_with_auth( - endpoint: "https://api.example.com/graphql", - token: "your-api-token-here" +// With bearer token authentication +let client = squall.new_erlang_client_with_auth( + "https://api.example.com/graphql", + "your-api-token-here" ) +``` + +### JavaScript Target -// For public APIs with no authentication -let client = squall.new_client( - endpoint: "https://rickandmortyapi.com/graphql", - headers: [] +```gleam +import squall + +// Basic client +let client = squall.new_javascript_client( + "https://api.example.com/graphql", + [#("X-Custom-Header", "value")] ) + +// With bearer token authentication +let client = squall.new_javascript_client_with_auth( + "https://api.example.com/graphql", + "your-api-token-here" +) +``` + +### Isomorphic Code + +For code that works on both targets, use `@target()` conditionals: + +```gleam +import squall + +@target(erlang) +fn create_client() -> squall.Client { + squall.new_erlang_client("https://api.example.com/graphql", []) +} + +@target(javascript) +fn create_client() -> squall.Client { + squall.new_javascript_client("https://api.example.com/graphql", []) +} + +pub fn main() { + let client = create_client() // Works on both targets! + // ... use the client +} ``` +## Isomorphic Architecture + +Squall's isomorphic design means you write your GraphQL queries once, and they work on both Erlang and JavaScript: + +### What's Different Between Targets? + +| Aspect | Erlang | JavaScript | +|--------|--------|------------| +| **HTTP Client** | `gleam_httpc` | Fetch API | +| **Return Type** | `Result(T, String)` | `Promise(Result(T, String))` | +| **Execution** | Synchronous | Asynchronous | +| **Client Constructor** | `new_erlang_client()` | `new_javascript_client()` | + +### What's the Same? + +✅ **GraphQL queries** - Same `.gql` files +✅ **Generated types** - Identical response types +✅ **Generated decoders** - Same JSON parsing logic +✅ **Business logic** - Share response handling code +✅ **Generated functions** - Same function signatures (except return type) + +The only code you need to write differently is: +1. Client creation (use `@target()` conditionals) +2. Promise handling on JavaScript (use `promise.await()`) + +Everything else is truly isomorphic! + ## How It Works Squall follows a simple workflow: @@ -126,6 +202,14 @@ your-project/ └── gleam.toml ``` +## Examples + +Check out the [examples](./examples/) directory for complete working examples: + +- **[01-erlang](./examples/01-erlang/)** - Erlang/OTP backend example +- **[02-javascript](./examples/02-javascript/)** - JavaScript (Node.js/Browser) example +- **[03-isomorphic](./examples/03-isomorphic/)** - Cross-platform example that runs on both! + ## Generated Code For a query like: @@ -140,22 +224,46 @@ query GetUser($id: ID!) { } ``` -Squall generates: +Squall generates type-safe Gleam code with: +**Response Types:** ```gleam pub type GetUserResponse { - GetUserResponse( + GetUserResponse(user: Option(User)) +} + +pub type User { + User( id: String, name: Option(String), email: Option(String) ) } +``` + +**Decoders:** +```gleam +fn get_user_response_decoder() -> decode.Decoder(GetUserResponse) { + // Auto-generated JSON decoder +} +``` +**Query Function:** +```gleam +// On Erlang - returns Result pub fn get_user( client: squall.Client, id: String, ) -> Result(GetUserResponse, String) { - // HTTP request + JSON decoding implementation + squall.execute_query(client, query, variables, decoder) +} + +// On JavaScript - returns Promise(Result) +pub fn get_user( + client: squall.Client, + id: String, +) -> Promise(Result(GetUserResponse, String)) { + squall.execute_query(client, query, variables, decoder) } ``` @@ -188,15 +296,23 @@ gleam run -m squall generate ## Architecture -Squall is built with a modular architecture: +Squall is built with a modular, isomorphic architecture: +### Core Modules - **`discovery`**: Finds and reads `.gql` files - **`parser`**: Lexes and parses GraphQL operations - **`schema`**: Introspects and parses GraphQL schemas - **`type_mapping`**: Maps GraphQL types to Gleam types -- **`codegen`**: Generates Gleam code +- **`codegen`**: Generates platform-agnostic Gleam code - **`error`**: Comprehensive error handling +### HTTP Adapter Pattern +- **`adapter`**: Defines the HTTP adapter interface +- **`adapter/erlang`**: Erlang implementation using `gleam_httpc` +- **`adapter/javascript`**: JavaScript implementation using Fetch API + +The generated code calls `squall.execute_query()`, which automatically uses the correct HTTP adapter for your target platform. + ## Development ### Running Tests @@ -210,8 +326,12 @@ gleam test ``` squall/ ├── src/ -│ ├── squall.gleam # CLI entry point +│ ├── squall.gleam # CLI entry point & execute_query │ └── squall/ +│ ├── adapter.gleam # HTTP adapter interface +│ ├── adapter/ +│ │ ├── erlang.gleam # Erlang HTTP adapter +│ │ └── javascript.gleam # JavaScript HTTP adapter │ └── internal/ │ ├── discovery.gleam # File discovery │ ├── parser.gleam # GraphQL parser @@ -225,21 +345,31 @@ squall/ │ ├── schema_test.gleam │ ├── type_mapping_test.gleam │ └── codegen_test.gleam -└── birdie_snapshots/ # Snapshot tests +├── birdie_snapshots/ # Snapshot tests +└── examples/ # Working examples + ├── 01-erlang/ # Erlang example + ├── 02-javascript/ # JavaScript example + └── 03-isomorphic/ # Cross-platform example ``` ## Roadmap +### Completed ✅ - [x] Query support - [x] Mutation support - [x] Subscription support - [x] Type-safe code generation - [x] Schema introspection - [x] Client abstraction with headers/authentication -- [x] Multiple endpoint support +- [x] **Isomorphic support** (Erlang + JavaScript targets) +- [x] HTTP adapter pattern +- [x] Target-conditional compilation + +### In Progress 🚧 - [ ] Fragment support -- [ ] Directive handling -- [ ] Custom scalar mapping +- [ ] Directive handling (`@include`, `@skip`, etc.) +- [ ] Custom scalar type mapping +- [ ] Schema caching for faster regeneration ## Contributing diff --git a/birdie_snapshots/mutation_function_generation.accepted b/birdie_snapshots/mutation_function_generation.accepted index 21d7616..2e8025d 100644 --- a/birdie_snapshots/mutation_function_generation.accepted +++ b/birdie_snapshots/mutation_function_generation.accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Mutation function generation +file: ./test/codegen_test.gleam +test_name: generate_mutation_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} @@ -44,39 +41,12 @@ pub fn create_user_response_to_json(input: CreateUserResponse) -> json.Json { json.object([#("createUser", json.nullable(input.create_user, user_to_json))]) } -pub fn create_user(client: squall.Client, name: String) -> Result(CreateUserResponse, String) { - let query = - "mutation CreateUser($name: String!) { createUser(name: $name) { id name } }" - let variables = - json.object([#("name", json.string(name))]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn create_user(client: squall.Client, name: String) { + squall.execute_query( + client, + "mutation CreateUser($name: String!) { createUser(name: $name) { id name } }", + json.object([#("name", json.string(name))]), + create_user_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", create_user_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/mutation_with_input_object_variable.accepted b/birdie_snapshots/mutation_with_input_object_variable.accepted index 9e63cc7..813fbd0 100644 --- a/birdie_snapshots/mutation_with_input_object_variable.accepted +++ b/birdie_snapshots/mutation_with_input_object_variable.accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Mutation with InputObject variable +file: ./test/codegen_test.gleam +test_name: generate_mutation_with_input_object_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option, Some, None} @@ -73,39 +70,12 @@ pub fn update_profile_response_to_json(input: UpdateProfileResponse) -> json.Jso ) } -pub fn update_profile(client: squall.Client, input: ProfileInput) -> Result(UpdateProfileResponse, String) { - let query = - "mutation UpdateProfile($input: ProfileInput!) { updateProfile(input: $input) { id displayName } }" - let variables = - json.object([#("input", profile_input_to_json(input))]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn update_profile(client: squall.Client, input: ProfileInput) { + squall.execute_query( + client, + "mutation UpdateProfile($input: ProfileInput!) { updateProfile(input: $input) { id displayName } }", + json.object([#("input", profile_input_to_json(input))]), + update_profile_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", update_profile_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/mutation_with_json_scalar_in_input_object.accepted b/birdie_snapshots/mutation_with_json_scalar_in_input_object.accepted index 0a4dff3..12a364c 100644 --- a/birdie_snapshots/mutation_with_json_scalar_in_input_object.accepted +++ b/birdie_snapshots/mutation_with_json_scalar_in_input_object.accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Mutation with JSON scalar in InputObject +file: ./test/codegen_test.gleam +test_name: generate_mutation_with_json_input_field_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option, Some, None} import gleam/dynamic.{type Dynamic} @@ -74,39 +71,12 @@ pub fn update_settings_response_to_json(input: UpdateSettingsResponse) -> json.J ) } -pub fn update_settings(client: squall.Client, input: SettingsInput) -> Result(UpdateSettingsResponse, String) { - let query = - "mutation UpdateSettings($input: SettingsInput!) { updateSettings(input: $input) { id metadata } }" - let variables = - json.object([#("input", settings_input_to_json(input))]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn update_settings(client: squall.Client, input: SettingsInput) { + squall.execute_query( + client, + "mutation UpdateSettings($input: SettingsInput!) { updateSettings(input: $input) { id metadata } }", + json.object([#("input", settings_input_to_json(input))]), + update_settings_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", update_settings_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/mutation_with_nested_input_object_types.accepted b/birdie_snapshots/mutation_with_nested_input_object_types.accepted index 9ebb233..5df3d45 100644 --- a/birdie_snapshots/mutation_with_nested_input_object_types.accepted +++ b/birdie_snapshots/mutation_with_nested_input_object_types.accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Mutation with nested InputObject types +file: ./test/codegen_test.gleam +test_name: generate_mutation_with_nested_input_object_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option, Some, None} @@ -99,39 +96,12 @@ pub fn update_profile_response_to_json(input: UpdateProfileResponse) -> json.Jso ) } -pub fn update_profile(client: squall.Client, input: ProfileInput) -> Result(UpdateProfileResponse, String) { - let query = - "mutation UpdateProfile($input: ProfileInput!) { updateProfile(input: $input) { id displayName } }" - let variables = - json.object([#("input", profile_input_to_json(input))]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn update_profile(client: squall.Client, input: ProfileInput) { + squall.execute_query( + client, + "mutation UpdateProfile($input: ProfileInput!) { updateProfile(input: $input) { id displayName } }", + json.object([#("input", profile_input_to_json(input))]), + update_profile_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", update_profile_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/mutation_with_optional_input_object_fields_(imports_some,_none).accepted b/birdie_snapshots/mutation_with_optional_input_object_fields_(imports_some,_none).accepted index d77c7cc..f9e658c 100644 --- a/birdie_snapshots/mutation_with_optional_input_object_fields_(imports_some,_none).accepted +++ b/birdie_snapshots/mutation_with_optional_input_object_fields_(imports_some,_none).accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Mutation with optional InputObject fields (imports Some, None) +file: ./test/codegen_test.gleam +test_name: generate_mutation_with_optional_input_fields_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option, Some, None} @@ -88,39 +85,12 @@ pub fn create_profile_response_to_json(input: CreateProfileResponse) -> json.Jso ) } -pub fn create_profile(client: squall.Client, input: ProfileInput) -> Result(CreateProfileResponse, String) { - let query = - "mutation CreateProfile($input: ProfileInput!) { createProfile(input: $input) { id displayName description } }" - let variables = - json.object([#("input", profile_input_to_json(input))]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn create_profile(client: squall.Client, input: ProfileInput) { + squall.execute_query( + client, + "mutation CreateProfile($input: ProfileInput!) { createProfile(input: $input) { id displayName description } }", + json.object([#("input", profile_input_to_json(input))]), + create_profile_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", create_profile_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/query_with_all_non_nullable_fields_(no_option_import).accepted b/birdie_snapshots/query_with_all_non_nullable_fields_(no_option_import).accepted index fe6794c..d7efc57 100644 --- a/birdie_snapshots/query_with_all_non_nullable_fields_(no_option_import).accepted +++ b/birdie_snapshots/query_with_all_non_nullable_fields_(no_option_import).accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Query with all non-nullable fields (no Option import) +file: ./test/codegen_test.gleam +test_name: generate_query_with_all_non_nullable_fields_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall pub type Product { @@ -45,39 +42,12 @@ pub fn get_product_response_to_json(input: GetProductResponse) -> json.Json { json.object([#("product", product_to_json(input.product))]) } -pub fn get_product(client: squall.Client) -> Result(GetProductResponse, String) { - let query = - "query GetProduct { product { id name price } }" - let variables = - json.object([]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn get_product(client: squall.Client) { + squall.execute_query( + client, + "query GetProduct { product { id name price } }", + json.object([]), + get_product_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", get_product_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/query_with_inline_array_arguments.accepted b/birdie_snapshots/query_with_inline_array_arguments.accepted index bc4115d..430c1b9 100644 --- a/birdie_snapshots/query_with_inline_array_arguments.accepted +++ b/birdie_snapshots/query_with_inline_array_arguments.accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Query with inline array arguments +file: ./test/codegen_test.gleam +test_name: generate_inline_array_arguments_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} @@ -51,39 +48,12 @@ pub fn get_episodes_response_to_json(input: GetEpisodesResponse) -> json.Json { ) } -pub fn get_episodes(client: squall.Client) -> Result(GetEpisodesResponse, String) { - let query = - "query GetEpisodes { episodesByIds(ids: [1, 2, 3]) { id name } }" - let variables = - json.object([]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn get_episodes(client: squall.Client) { + squall.execute_query( + client, + "query GetEpisodes { episodesByIds(ids: [1, 2, 3]) { id name } }", + json.object([]), + get_episodes_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", get_episodes_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/query_with_inline_object_arguments.accepted b/birdie_snapshots/query_with_inline_object_arguments.accepted index fee2e1b..a66f67d 100644 --- a/birdie_snapshots/query_with_inline_object_arguments.accepted +++ b/birdie_snapshots/query_with_inline_object_arguments.accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Query with inline object arguments +file: ./test/codegen_test.gleam +test_name: generate_inline_object_arguments_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} @@ -68,39 +65,12 @@ pub fn get_characters_response_to_json(input: GetCharactersResponse) -> json.Jso ) } -pub fn get_characters(client: squall.Client) -> Result(GetCharactersResponse, String) { - let query = - "query GetCharacters { characters(filter: { name: \"rick\", status: \"alive\" }) { results { id name } } }" - let variables = - json.object([]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn get_characters(client: squall.Client) { + squall.execute_query( + client, + "query GetCharacters { characters(filter: { name: \"rick\", status: \"alive\" }) { results { id name } } }", + json.object([]), + get_characters_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", get_characters_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/query_with_inline_scalar_arguments.accepted b/birdie_snapshots/query_with_inline_scalar_arguments.accepted index 02a2951..0b88b18 100644 --- a/birdie_snapshots/query_with_inline_scalar_arguments.accepted +++ b/birdie_snapshots/query_with_inline_scalar_arguments.accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Query with inline scalar arguments +file: ./test/codegen_test.gleam +test_name: generate_inline_scalar_arguments_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} @@ -48,39 +45,12 @@ pub fn get_character_response_to_json(input: GetCharacterResponse) -> json.Json ) } -pub fn get_character(client: squall.Client) -> Result(GetCharacterResponse, String) { - let query = - "query GetCharacter { character(id: 1) { id name } }" - let variables = - json.object([]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn get_character(client: squall.Client) { + squall.execute_query( + client, + "query GetCharacter { character(id: 1) { id name } }", + json.object([]), + get_character_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", get_character_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/query_with_json_scalar_field.accepted b/birdie_snapshots/query_with_json_scalar_field.accepted index 1344c1a..75678fc 100644 --- a/birdie_snapshots/query_with_json_scalar_field.accepted +++ b/birdie_snapshots/query_with_json_scalar_field.accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Query with JSON scalar field +file: ./test/codegen_test.gleam +test_name: generate_query_with_json_scalar_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} import gleam/dynamic.{type Dynamic} @@ -47,39 +44,12 @@ pub fn get_profile_response_to_json(input: GetProfileResponse) -> json.Json { json.object([#("profile", json.nullable(input.profile, profile_to_json))]) } -pub fn get_profile(client: squall.Client) -> Result(GetProfileResponse, String) { - let query = - "query GetProfile { profile { id displayName metadata } }" - let variables = - json.object([]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn get_profile(client: squall.Client) { + squall.execute_query( + client, + "query GetProfile { profile { id displayName metadata } }", + json.object([]), + get_profile_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", get_profile_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/query_with_multiple_root_fields_and_mixed_arguments.accepted b/birdie_snapshots/query_with_multiple_root_fields_and_mixed_arguments.accepted index ed55ddd..17c01a2 100644 --- a/birdie_snapshots/query_with_multiple_root_fields_and_mixed_arguments.accepted +++ b/birdie_snapshots/query_with_multiple_root_fields_and_mixed_arguments.accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Query with multiple root fields and mixed arguments +file: ./test/codegen_test.gleam +test_name: generate_multiple_root_fields_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} @@ -118,39 +115,12 @@ pub fn multi_query_response_to_json(input: MultiQueryResponse) -> json.Json { ) } -pub fn multi_query(client: squall.Client) -> Result(MultiQueryResponse, String) { - let query = - "query MultiQuery { characters(page: 2, filter: { name: \"rick\" }) { info { count } results { name } } location(id: 1) { id } episodesByIds(ids: [1, 2]) { id } }" - let variables = - json.object([]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn multi_query(client: squall.Client) { + squall.execute_query( + client, + "query MultiQuery { characters(page: 2, filter: { name: \"rick\" }) { info { count } results { name } } location(id: 1) { id } episodesByIds(ids: [1, 2]) { id } }", + json.object([]), + multi_query_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", multi_query_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/query_with_nested_types_generation.accepted b/birdie_snapshots/query_with_nested_types_generation.accepted index 04f3e06..e54e963 100644 --- a/birdie_snapshots/query_with_nested_types_generation.accepted +++ b/birdie_snapshots/query_with_nested_types_generation.accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Query with nested types generation +file: ./test/codegen_test.gleam +test_name: generate_query_with_nested_types_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} @@ -50,39 +47,12 @@ pub fn get_character_response_to_json(input: GetCharacterResponse) -> json.Json ) } -pub fn get_character(client: squall.Client, id: String) -> Result(GetCharacterResponse, String) { - let query = - "query GetCharacter($id: ID!) { character(id: $id) { id name status } }" - let variables = - json.object([#("id", json.string(id))]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn get_character(client: squall.Client, id: String) { + squall.execute_query( + client, + "query GetCharacter($id: ID!) { character(id: $id) { id name status } }", + json.object([#("id", json.string(id))]), + get_character_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", get_character_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/query_with_optional_response_fields_(no_some,_none_imports).accepted b/birdie_snapshots/query_with_optional_response_fields_(no_some,_none_imports).accepted index 969204e..42689da 100644 --- a/birdie_snapshots/query_with_optional_response_fields_(no_some,_none_imports).accepted +++ b/birdie_snapshots/query_with_optional_response_fields_(no_some,_none_imports).accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Query with optional response fields (no Some, None imports) +file: ./test/codegen_test.gleam +test_name: generate_query_with_optional_response_fields_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} @@ -50,39 +47,12 @@ pub fn get_profile_response_to_json(input: GetProfileResponse) -> json.Json { json.object([#("profile", json.nullable(input.profile, profile_to_json))]) } -pub fn get_profile(client: squall.Client) -> Result(GetProfileResponse, String) { - let query = - "query GetProfile { profile { id displayName description } }" - let variables = - json.object([]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn get_profile(client: squall.Client) { + squall.execute_query( + client, + "query GetProfile { profile { id displayName description } }", + json.object([]), + get_profile_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", get_profile_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/query_with_variables_function_generation.accepted b/birdie_snapshots/query_with_variables_function_generation.accepted index 16915df..f15ec7d 100644 --- a/birdie_snapshots/query_with_variables_function_generation.accepted +++ b/birdie_snapshots/query_with_variables_function_generation.accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Query with variables function generation +file: ./test/codegen_test.gleam +test_name: generate_query_with_variables_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} @@ -44,39 +41,12 @@ pub fn get_user_response_to_json(input: GetUserResponse) -> json.Json { json.object([#("user", json.nullable(input.user, user_to_json))]) } -pub fn get_user(client: squall.Client, id: String) -> Result(GetUserResponse, String) { - let query = - "query GetUser($id: ID!) { user(id: $id) { id name } }" - let variables = - json.object([#("id", json.string(id))]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn get_user(client: squall.Client, id: String) { + squall.execute_query( + client, + "query GetUser($id: ID!) { user(id: $id) { id name } }", + json.object([#("id", json.string(id))]), + get_user_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", get_user_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/response_serializer_for_simple_type.accepted b/birdie_snapshots/response_serializer_for_simple_type.accepted index 853775d..7de82cf 100644 --- a/birdie_snapshots/response_serializer_for_simple_type.accepted +++ b/birdie_snapshots/response_serializer_for_simple_type.accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Response serializer for simple type +file: ./test/codegen_test.gleam +test_name: generate_response_serializer_simple_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} @@ -44,39 +41,12 @@ pub fn get_user_response_to_json(input: GetUserResponse) -> json.Json { json.object([#("user", json.nullable(input.user, user_to_json))]) } -pub fn get_user(client: squall.Client) -> Result(GetUserResponse, String) { - let query = - "query GetUser { user { id name } }" - let variables = - json.object([]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn get_user(client: squall.Client) { + squall.execute_query( + client, + "query GetUser { user { id name } }", + json.object([]), + get_user_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", get_user_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/response_serializer_with_all_scalar_types.accepted b/birdie_snapshots/response_serializer_with_all_scalar_types.accepted index c86ebed..14f522a 100644 --- a/birdie_snapshots/response_serializer_with_all_scalar_types.accepted +++ b/birdie_snapshots/response_serializer_with_all_scalar_types.accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Response serializer with all scalar types +file: ./test/codegen_test.gleam +test_name: generate_response_serializer_with_all_scalars_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} import gleam/dynamic.{type Dynamic} @@ -67,39 +64,12 @@ pub fn get_product_response_to_json(input: GetProductResponse) -> json.Json { json.object([#("product", json.nullable(input.product, product_to_json))]) } -pub fn get_product(client: squall.Client) -> Result(GetProductResponse, String) { - let query = - "query GetProduct { product { id name price inStock rating metadata } }" - let variables = - json.object([]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn get_product(client: squall.Client) { + squall.execute_query( + client, + "query GetProduct { product { id name price inStock rating metadata } }", + json.object([]), + get_product_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", get_product_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/response_serializer_with_lists.accepted b/birdie_snapshots/response_serializer_with_lists.accepted index 065b34d..39d77d4 100644 --- a/birdie_snapshots/response_serializer_with_lists.accepted +++ b/birdie_snapshots/response_serializer_with_lists.accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Response serializer with lists +file: ./test/codegen_test.gleam +test_name: generate_response_serializer_with_lists_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} @@ -51,39 +48,12 @@ pub fn get_users_response_to_json(input: GetUsersResponse) -> json.Json { ) } -pub fn get_users(client: squall.Client) -> Result(GetUsersResponse, String) { - let query = - "query GetUsers { users { id name } }" - let variables = - json.object([]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn get_users(client: squall.Client) { + squall.execute_query( + client, + "query GetUsers { users { id name } }", + json.object([]), + get_users_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", get_users_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/response_serializer_with_nested_types.accepted b/birdie_snapshots/response_serializer_with_nested_types.accepted index ecc3891..b3d7919 100644 --- a/birdie_snapshots/response_serializer_with_nested_types.accepted +++ b/birdie_snapshots/response_serializer_with_nested_types.accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Response serializer with nested types +file: ./test/codegen_test.gleam +test_name: generate_response_serializer_with_nested_types_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} @@ -65,39 +62,12 @@ pub fn get_user_response_to_json(input: GetUserResponse) -> json.Json { json.object([#("user", json.nullable(input.user, user_to_json))]) } -pub fn get_user(client: squall.Client) -> Result(GetUserResponse, String) { - let query = - "query GetUser { user { id name location { city country } } }" - let variables = - json.object([]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn get_user(client: squall.Client) { + squall.execute_query( + client, + "query GetUser { user { id name location { city country } } }", + json.object([]), + get_user_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", get_user_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/response_serializer_with_optional_fields.accepted b/birdie_snapshots/response_serializer_with_optional_fields.accepted index 1e65971..2975f31 100644 --- a/birdie_snapshots/response_serializer_with_optional_fields.accepted +++ b/birdie_snapshots/response_serializer_with_optional_fields.accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Response serializer with optional fields +file: ./test/codegen_test.gleam +test_name: generate_response_serializer_with_optional_fields_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} @@ -46,39 +43,12 @@ pub fn get_user_response_to_json(input: GetUserResponse) -> json.Json { json.object([#("user", json.nullable(input.user, user_to_json))]) } -pub fn get_user(client: squall.Client) -> Result(GetUserResponse, String) { - let query = - "query GetUser { user { id name email } }" - let variables = - json.object([]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn get_user(client: squall.Client) { + squall.execute_query( + client, + "query GetUser { user { id name email } }", + json.object([]), + get_user_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", get_user_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/simple_query_function_generation.accepted b/birdie_snapshots/simple_query_function_generation.accepted index dfc01d7..0a0c4a8 100644 --- a/birdie_snapshots/simple_query_function_generation.accepted +++ b/birdie_snapshots/simple_query_function_generation.accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Simple query function generation +file: ./test/codegen_test.gleam +test_name: generate_simple_query_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} @@ -44,39 +41,12 @@ pub fn get_user_response_to_json(input: GetUserResponse) -> json.Json { json.object([#("user", json.nullable(input.user, user_to_json))]) } -pub fn get_user(client: squall.Client) -> Result(GetUserResponse, String) { - let query = - "query GetUser { user { id name } }" - let variables = - json.object([]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn get_user(client: squall.Client) { + squall.execute_query( + client, + "query GetUser { user { id name } }", + json.object([]), + get_user_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", get_user_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/birdie_snapshots/type_with_reserved_keywords.accepted b/birdie_snapshots/type_with_reserved_keywords.accepted index 6b95efa..3088289 100644 --- a/birdie_snapshots/type_with_reserved_keywords.accepted +++ b/birdie_snapshots/type_with_reserved_keywords.accepted @@ -1,14 +1,11 @@ --- version: 1.4.1 title: Type with reserved keywords +file: ./test/codegen_test.gleam +test_name: generate_with_reserved_keywords_test --- import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} @@ -53,39 +50,12 @@ pub fn get_item_response_to_json(input: GetItemResponse) -> json.Json { json.object([#("item", json.nullable(input.item, item_to_json))]) } -pub fn get_item(client: squall.Client) -> Result(GetItemResponse, String) { - let query = - "query GetItem { item { id type case let } }" - let variables = - json.object([]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn get_item(client: squall.Client) { + squall.execute_query( + client, + "query GetItem { item { id type case let } }", + json.object([]), + get_item_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", get_item_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/example/README.md b/example/README.md deleted file mode 100644 index 03b8ba1..0000000 --- a/example/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# Example: Rick and Morty API - -This example demonstrates how to use Squall with the Rick and Morty GraphQL API. - -## API Endpoint - -``` -https://rickandmortyapi.com/graphql -``` - -## Queries - -### get_character.gql - -Fetches a single character by ID. - -**Variables:** -- `id` (ID!): The character's ID - -**Example:** -```gleam -import example/graphql/get_character - -pub fn main() { - let result = get_character.get_character( - endpoint: "https://rickandmortyapi.com/graphql", - id: "1" - ) - - case result { - Ok(response) -> { - // Handle response - io.println("Character: " <> response.character.name) - } - Error(err) -> { - io.println("Error: " <> err) - } - } -} -``` - -### get_characters.gql - -Fetches a list of characters. - -**Example:** -```gleam -import example/graphql/get_characters - -pub fn main() { - let result = get_characters.get_characters( - endpoint: "https://rickandmortyapi.com/graphql" - ) - - case result { - Ok(response) -> { - // Handle response with list of characters - } - Error(err) -> { - io.println("Error: " <> err) - } - } -} -``` - -## Generate Code - -Since this is a separate project, you need to run Squall from the parent directory: - -```bash -# From the example directory, go back to parent -cd .. - -# Run Squall to generate code -gleam run -m squall generate https://rickandmortyapi.com/graphql - -# Go back to example -cd example -``` - -This will discover the `.gql` files and generate: -- `src/example/graphql/get_character.gleam` -- `src/example/graphql/get_characters.gleam` - -Then you can run the example: - -```bash -gleam run -``` diff --git a/example/gleam.toml b/example/gleam.toml deleted file mode 100644 index bfb4ead..0000000 --- a/example/gleam.toml +++ /dev/null @@ -1,12 +0,0 @@ -name = "example" -version = "0.1.0" -description = "Example project using Squall with Rick and Morty API" - -[dependencies] -gleam_stdlib = ">= 0.65.0 and < 0.66.0" -gleam_json = ">= 3.0.0 and < 4.0.0" -gleam_http = ">= 4.3.0 and < 5.0.0" -gleam_httpc = ">= 5.0.0 and < 6.0.0" -squall = { path = "../" } - -[dev-dependencies] diff --git a/examples/01-erlang/README.md b/examples/01-erlang/README.md new file mode 100644 index 0000000..48ec6da --- /dev/null +++ b/examples/01-erlang/README.md @@ -0,0 +1,54 @@ +# Squall Erlang Example + +This example demonstrates using Squall-generated GraphQL clients on the **Erlang/OTP** target. + +## What's Inside + +- `erlang_example.gleam` - Multi-field query example +- `example_with_vars.gleam` - Query with GraphQL variables +- `src/graphql/*.gql` - GraphQL query definitions +- `src/graphql/*.gleam` - Generated type-safe Gleam code + +## HTTP Adapter + +This example uses the **Erlang adapter** which uses `gleam_httpc` for HTTP requests: + +```gleam +import squall + +let client = squall.new_erlang_client("https://rickandmortyapi.com/graphql", []) +``` + +## Running + +```bash +# Build the project +gleam build + +# Run the main example +gleam run -m erlang_example + +# Run the variables example +gleam run -m example_with_vars +``` + +## Regenerating GraphQL Code + +If you modify the `.gql` files, regenerate the code: + +```bash +# From the squall root directory +cd ../.. +gleam run -m squall generate https://rickandmortyapi.com/graphql +``` + +This will update the `.gleam` files in `src/graphql/`. + +## Features Demonstrated + +✅ **Erlang/OTP Runtime** - Uses `gleam_httpc` HTTP client +✅ **Type Safety** - Fully typed based on GraphQL schema +✅ **Nested Objects** - Automatic decoder generation +✅ **Optional Fields** - Proper `Option` type handling +✅ **Variables** - Type-safe GraphQL variables +✅ **JSON Serialization** - Convert responses back to JSON diff --git a/examples/01-erlang/gleam.toml b/examples/01-erlang/gleam.toml new file mode 100644 index 0000000..3a953aa --- /dev/null +++ b/examples/01-erlang/gleam.toml @@ -0,0 +1,9 @@ +name = "erlang_example" +version = "0.1.0" +description = "Squall Erlang example - Rick and Morty GraphQL API" + +[dependencies] +gleam_stdlib = ">= 0.65.0 and < 0.66.0" +gleam_json = ">= 3.0.0 and < 4.0.0" +gleam_http = ">= 4.3.0 and < 5.0.0" +squall = { path = "../.." } diff --git a/examples/01-erlang/manifest.toml b/examples/01-erlang/manifest.toml new file mode 100644 index 0000000..135270e --- /dev/null +++ b/examples/01-erlang/manifest.toml @@ -0,0 +1,25 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, + { name = "glam", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "237C2CE218A2A0A5D46D625F8EF5B78F964BC91018B78D692B17E1AB84295229" }, + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, + { 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" }, + { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, + { 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" }, + { name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" }, + { name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" }, + { name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" }, + { name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" }, + { 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 = "../.." }, +] + +[requirements] +gleam_fetch = { version = ">= 1.0.0 and < 2.0.0" } +gleam_http = { version = ">= 4.3.0 and < 5.0.0" } +gleam_javascript = { version = ">= 0.3.0 and < 2.0.0" } +gleam_json = { version = ">= 3.0.0 and < 4.0.0" } +gleam_stdlib = { version = ">= 0.65.0 and < 0.66.0" } +squall = { path = "../.." } diff --git a/example/src/example.gleam b/examples/01-erlang/src/erlang_example.gleam similarity index 74% rename from example/src/example.gleam rename to examples/01-erlang/src/erlang_example.gleam index 2f36498..0dfd6d9 100644 --- a/example/src/example.gleam +++ b/examples/01-erlang/src/erlang_example.gleam @@ -5,10 +5,11 @@ import graphql/multi_query import squall pub fn main() { - io.println("Squall Multi-Field Query Example") - io.println("=================================\n") + io.println("Squall Multi-Field Query Example (Erlang)") + io.println("==========================================\n") - let client = squall.new_client("https://rickandmortyapi.com/graphql", []) + // Create an Erlang client (uses gleam_httpc for HTTP requests) + let client = squall.new_erlang_client("https://rickandmortyapi.com/graphql", []) let result = multi_query.multi_query(client) diff --git a/example/src/example_with_vars.gleam b/examples/01-erlang/src/example_with_vars.gleam similarity index 82% rename from example/src/example_with_vars.gleam rename to examples/01-erlang/src/example_with_vars.gleam index e02f5ae..b22fce5 100644 --- a/example/src/example_with_vars.gleam +++ b/examples/01-erlang/src/example_with_vars.gleam @@ -5,8 +5,8 @@ import graphql/multi_query_with_vars import squall pub fn main() { - io.println("Squall Multi-Field Query Example (with Variables)") - io.println("==================================================\n") + io.println("Squall Multi-Field Query Example (with Variables - Erlang)") + io.println("===========================================================\n") io.println("Calling multi_query_with_vars with:") io.println(" page: 2") @@ -14,7 +14,8 @@ pub fn main() { io.println(" locationId: \"1\"") io.println(" episodeIds: [1, 2]\n") - let client = squall.new_client("https://rickandmortyapi.com/graphql", []) + // Create an Erlang client (uses gleam_httpc for HTTP requests) + let client = squall.new_erlang_client("https://rickandmortyapi.com/graphql", []) let result = multi_query_with_vars.multi_query_with_vars(client, 2, "rick", "1", [1, 2]) diff --git a/example/src/graphql/get_character.gleam b/examples/01-erlang/src/graphql/get_character.gleam similarity index 59% rename from example/src/graphql/get_character.gleam rename to examples/01-erlang/src/graphql/get_character.gleam index 2bc4658..28a00b7 100644 --- a/example/src/graphql/get_character.gleam +++ b/examples/01-erlang/src/graphql/get_character.gleam @@ -1,10 +1,5 @@ import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} @@ -66,38 +61,11 @@ pub fn get_character_response_to_json(input: GetCharacterResponse) -> json.Json ) } -pub fn get_character(client: squall.Client, id: String) -> Result(GetCharacterResponse, String) { - let query = - "query GetCharacter($id: ID!) { character(id: $id) { id name status species type gender } }" - let variables = - json.object([#("id", json.string(id))]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn get_character(client: squall.Client, id: String) { + squall.execute_query( + client, + "query GetCharacter($id: ID!) { character(id: $id) { id name status species type gender } }", + json.object([#("id", json.string(id))]), + get_character_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", get_character_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/examples/01-erlang/src/graphql/get_character.gleam.new b/examples/01-erlang/src/graphql/get_character.gleam.new new file mode 100644 index 0000000..e69de29 diff --git a/example/src/graphql/get_character.gql b/examples/01-erlang/src/graphql/get_character.gql similarity index 100% rename from example/src/graphql/get_character.gql rename to examples/01-erlang/src/graphql/get_character.gql diff --git a/example/src/graphql/get_characters.gleam b/examples/01-erlang/src/graphql/get_characters.gleam similarity index 62% rename from example/src/graphql/get_characters.gleam rename to examples/01-erlang/src/graphql/get_characters.gleam index 721d8ae..b3ca40b 100644 --- a/example/src/graphql/get_characters.gleam +++ b/examples/01-erlang/src/graphql/get_characters.gleam @@ -1,10 +1,5 @@ import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} @@ -73,38 +68,11 @@ pub fn get_characters_response_to_json(input: GetCharactersResponse) -> json.Jso ) } -pub fn get_characters(client: squall.Client) -> Result(GetCharactersResponse, String) { - let query = - "query GetCharacters { characters { results { id name status species } } }" - let variables = - json.object([]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn get_characters(client: squall.Client) { + squall.execute_query( + client, + "query GetCharacters { characters { results { id name status species } } }", + json.object([]), + get_characters_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", get_characters_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/examples/01-erlang/src/graphql/get_characters.gleam.new b/examples/01-erlang/src/graphql/get_characters.gleam.new new file mode 100644 index 0000000..e69de29 diff --git a/example/src/graphql/get_characters.gql b/examples/01-erlang/src/graphql/get_characters.gql similarity index 100% rename from example/src/graphql/get_characters.gql rename to examples/01-erlang/src/graphql/get_characters.gql diff --git a/example/src/graphql/multi_query.gleam b/examples/01-erlang/src/graphql/multi_query.gleam similarity index 71% rename from example/src/graphql/multi_query.gleam rename to examples/01-erlang/src/graphql/multi_query.gleam index 3a59475..dc55876 100644 --- a/example/src/graphql/multi_query.gleam +++ b/examples/01-erlang/src/graphql/multi_query.gleam @@ -1,10 +1,5 @@ import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} @@ -114,38 +109,11 @@ pub fn multi_query_response_to_json(input: MultiQueryResponse) -> json.Json { ) } -pub fn multi_query(client: squall.Client) -> Result(MultiQueryResponse, String) { - let query = - "query MultiQuery { characters(page: 2, filter: { name: \"rick\" }) { info { count } results { name } } location(id: 1) { id } episodesByIds(ids: [1, 2]) { id } }" - let variables = - json.object([]) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), +pub fn multi_query(client: squall.Client) { + squall.execute_query( + client, + "query MultiQuery { characters(page: 2, filter: { name: \"rick\" }) { info { count } results { name } } location(id: 1) { id } episodesByIds(ids: [1, 2]) { id } }", + json.object([]), + multi_query_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", multi_query_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/examples/01-erlang/src/graphql/multi_query.gleam.new b/examples/01-erlang/src/graphql/multi_query.gleam.new new file mode 100644 index 0000000..e69de29 diff --git a/example/src/graphql/multi_query.gql b/examples/01-erlang/src/graphql/multi_query.gql similarity index 100% rename from example/src/graphql/multi_query.gql rename to examples/01-erlang/src/graphql/multi_query.gql diff --git a/example/src/graphql/multi_query_with_vars.gleam b/examples/01-erlang/src/graphql/multi_query_with_vars.gleam similarity index 76% rename from example/src/graphql/multi_query_with_vars.gleam rename to examples/01-erlang/src/graphql/multi_query_with_vars.gleam index 8f0d05c..d08fc2c 100644 --- a/example/src/graphql/multi_query_with_vars.gleam +++ b/examples/01-erlang/src/graphql/multi_query_with_vars.gleam @@ -1,10 +1,5 @@ import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc import gleam/json -import gleam/list -import gleam/result import squall import gleam/option.{type Option} @@ -126,10 +121,10 @@ pub fn multi_query_with_vars_response_to_json(input: MultiQueryWithVarsResponse) ) } -pub fn multi_query_with_vars(client: squall.Client, page: Int, name: String, location_id: String, episode_ids: List(Int)) -> Result(MultiQueryWithVarsResponse, String) { - let query = - "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 } }" - let variables = +pub fn multi_query_with_vars(client: squall.Client, page: Int, name: String, location_id: String, episode_ids: List(Int)) { + squall.execute_query( + client, + "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 } }", json.object( [ #("page", json.int(page)), @@ -137,34 +132,7 @@ pub fn multi_query_with_vars(client: squall.Client, page: Int, name: String, loc #("locationId", json.string(location_id)), #("episodeIds", json.array(from: episode_ids, of: json.int)), ], - ) - let body = - json.object([#("query", json.string(query)), #("variables", variables)]) - use req <- result.try( - request.to(client.endpoint) - |> result.map_error(fn(_) { "Invalid endpoint URL" }), + ), + multi_query_with_vars_response_decoder(), ) - let req = - req - |> request.set_method(http.Post) - |> request.set_body(json.to_string(body)) - |> request.set_header("content-type", "application/json") - let req = - list.fold(client.headers, req, fn(r, header) { - request.set_header(r, header.0, header.1) - }) - use resp <- result.try( - httpc.send(req) - |> result.map_error(fn(_) { "HTTP request failed" }), - ) - use json_value <- result.try( - json.parse(from: resp.body, using: decode.dynamic) - |> result.map_error(fn(_) { "Failed to decode JSON response" }), - ) - let data_and_response_decoder = { - use data <- decode.field("data", multi_query_with_vars_response_decoder()) - decode.success(data) - } - decode.run(json_value, data_and_response_decoder) - |> result.map_error(fn(_) { "Failed to decode response data" }) } diff --git a/examples/01-erlang/src/graphql/multi_query_with_vars.gleam.new b/examples/01-erlang/src/graphql/multi_query_with_vars.gleam.new new file mode 100644 index 0000000..e69de29 diff --git a/example/src/graphql/multi_query_with_vars.gql b/examples/01-erlang/src/graphql/multi_query_with_vars.gql similarity index 100% rename from example/src/graphql/multi_query_with_vars.gql rename to examples/01-erlang/src/graphql/multi_query_with_vars.gql diff --git a/examples/02-javascript/README.md b/examples/02-javascript/README.md new file mode 100644 index 0000000..090d538 --- /dev/null +++ b/examples/02-javascript/README.md @@ -0,0 +1,87 @@ +# Squall JavaScript Example + +This example demonstrates using Squall-generated GraphQL clients on the **JavaScript** target (Node.js or browser). + +## What's Inside + +- `javascript_example.gleam` - Multi-field query example using Fetch API +- `src/graphql/*.gql` - GraphQL query definitions +- `src/graphql/*.gleam` - Generated type-safe Gleam code (same as Erlang!) + +## HTTP Adapter + +This example uses the **JavaScript adapter** which uses the Fetch API for HTTP requests: + +```gleam +import squall + +let client = squall.new_javascript_client("https://rickandmortyapi.com/graphql", []) +``` + +The Fetch API works in: +- **Node.js** v18+ (built-in) +- **Browsers** (all modern browsers) +- **Deno** (built-in) + +## Running + +### Node.js (Recommended) + +```bash +# Build for JavaScript target +gleam build + +# Run the example +gleam run -m javascript_example +``` + +### Browser + +1. Build the JavaScript bundle: + ```bash + gleam build --target javascript + ``` + +2. The compiled JavaScript will be in `build/dev/javascript/` + +3. Include it in an HTML file or use with a bundler (Vite, Webpack, etc.) + +## Requirements + +- **Node.js** v18.0.0 or later (for native Fetch API support) +- Or any modern browser + +## Regenerating GraphQL Code + +If you modify the `.gql` files, regenerate the code: + +```bash +# From the squall root directory +cd ../.. +gleam run -m squall generate https://rickandmortyapi.com/graphql +``` + +This will update the `.gleam` files in `src/graphql/`. + +## Key Differences from Erlang Example + +The **only difference** between this and the Erlang example is the client creation: + +```gleam +// JavaScript +squall.new_javascript_client(endpoint, headers) + +// vs Erlang +squall.new_erlang_client(endpoint, headers) +``` + +**Everything else is identical!** The same generated GraphQL code works on both targets. + +## Features Demonstrated + +✅ **JavaScript Runtime** - Uses Fetch API (Node.js/Browser) +✅ **Type Safety** - Fully typed based on GraphQL schema +✅ **Isomorphic Code** - Same generated code as Erlang example +✅ **Nested Objects** - Automatic decoder generation +✅ **Optional Fields** - Proper `Option` type handling +✅ **JSON Serialization** - Convert responses back to JSON diff --git a/examples/02-javascript/gleam.toml b/examples/02-javascript/gleam.toml new file mode 100644 index 0000000..2f90306 --- /dev/null +++ b/examples/02-javascript/gleam.toml @@ -0,0 +1,14 @@ +name = "javascript_example" +version = "0.1.0" +description = "Squall JavaScript example - Rick and Morty GraphQL API" +target = "javascript" + +[dependencies] +gleam_stdlib = ">= 0.65.0 and < 0.66.0" +gleam_json = ">= 3.0.0 and < 4.0.0" +gleam_http = ">= 4.3.0 and < 5.0.0" +gleam_javascript = ">= 0.3.0 and < 2.0.0" +squall = { path = "../.." } + +[javascript] +runtime = "node" diff --git a/example/manifest.toml b/examples/02-javascript/manifest.toml similarity index 75% rename from example/manifest.toml rename to examples/02-javascript/manifest.toml index 0944aa4..92a097c 100644 --- a/example/manifest.toml +++ b/examples/02-javascript/manifest.toml @@ -6,17 +6,19 @@ packages = [ { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, { name = "glam", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "237C2CE218A2A0A5D46D625F8EF5B78F964BC91018B78D692B17E1AB84295229" }, { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, + { 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" }, { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, { 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" }, + { name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" }, { name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" }, { name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" }, { name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" }, - { name = "squall", version = "0.1.0", build_tools = ["gleam"], requirements = ["argv", "filepath", "glam", "gleam_http", "gleam_httpc", "gleam_json", "gleam_stdlib", "simplifile"], source = "local", path = ".." }, + { 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 = "../.." }, ] [requirements] gleam_http = { version = ">= 4.3.0 and < 5.0.0" } -gleam_httpc = { version = ">= 5.0.0 and < 6.0.0" } +gleam_javascript = { version = ">= 0.3.0 and < 2.0.0" } gleam_json = { version = ">= 3.0.0 and < 4.0.0" } gleam_stdlib = { version = ">= 0.65.0 and < 0.66.0" } -squall = { path = "../" } +squall = { path = "../.." } diff --git a/examples/02-javascript/src/graphql/get_character.gleam b/examples/02-javascript/src/graphql/get_character.gleam new file mode 100644 index 0000000..28a00b7 --- /dev/null +++ b/examples/02-javascript/src/graphql/get_character.gleam @@ -0,0 +1,71 @@ +import gleam/dynamic/decode +import gleam/json +import squall +import gleam/option.{type Option} + +pub type Character { + Character( + id: Option(String), + name: Option(String), + status: Option(String), + species: Option(String), + type_: Option(String), + gender: Option(String), + ) +} + +pub fn character_decoder() -> decode.Decoder(Character) { + use id <- decode.field("id", decode.optional(decode.string)) + use name <- decode.field("name", decode.optional(decode.string)) + use status <- decode.field("status", decode.optional(decode.string)) + use species <- decode.field("species", decode.optional(decode.string)) + use type_ <- decode.field("type", decode.optional(decode.string)) + use gender <- decode.field("gender", decode.optional(decode.string)) + decode.success(Character( + id: id, + name: name, + status: status, + species: species, + type_: type_, + gender: gender, + )) +} + +pub fn character_to_json(input: Character) -> json.Json { + json.object( + [ + #("id", json.nullable(input.id, json.string)), + #("name", json.nullable(input.name, json.string)), + #("status", json.nullable(input.status, json.string)), + #("species", json.nullable(input.species, json.string)), + #("type", json.nullable(input.type_, json.string)), + #("gender", json.nullable(input.gender, json.string)), + ], + ) +} + +pub type GetCharacterResponse { + GetCharacterResponse(character: Option(Character)) +} + +pub fn get_character_response_decoder() -> decode.Decoder(GetCharacterResponse) { + use character <- decode.field("character", decode.optional(character_decoder())) + decode.success(GetCharacterResponse(character: character)) +} + +pub fn get_character_response_to_json(input: GetCharacterResponse) -> json.Json { + json.object( + [ + #("character", json.nullable(input.character, character_to_json)), + ], + ) +} + +pub fn get_character(client: squall.Client, id: String) { + squall.execute_query( + client, + "query GetCharacter($id: ID!) { character(id: $id) { id name status species type gender } }", + json.object([#("id", json.string(id))]), + get_character_response_decoder(), + ) +} diff --git a/examples/02-javascript/src/graphql/get_character.gql b/examples/02-javascript/src/graphql/get_character.gql new file mode 100644 index 0000000..56d6948 --- /dev/null +++ b/examples/02-javascript/src/graphql/get_character.gql @@ -0,0 +1,10 @@ +query GetCharacter($id: ID!) { + character(id: $id) { + id + name + status + species + type + gender + } +} diff --git a/examples/02-javascript/src/graphql/get_characters.gleam b/examples/02-javascript/src/graphql/get_characters.gleam new file mode 100644 index 0000000..b3ca40b --- /dev/null +++ b/examples/02-javascript/src/graphql/get_characters.gleam @@ -0,0 +1,78 @@ +import gleam/dynamic/decode +import gleam/json +import squall +import gleam/option.{type Option} + +pub type Characters { + Characters(results: Option(List(Character))) +} + +pub fn characters_decoder() -> decode.Decoder(Characters) { + use results <- decode.field("results", decode.optional(decode.list(character_decoder()))) + decode.success(Characters(results: results)) +} + +pub type Character { + Character( + id: Option(String), + name: Option(String), + status: Option(String), + species: Option(String), + ) +} + +pub fn character_decoder() -> decode.Decoder(Character) { + use id <- decode.field("id", decode.optional(decode.string)) + use name <- decode.field("name", decode.optional(decode.string)) + use status <- decode.field("status", decode.optional(decode.string)) + use species <- decode.field("species", decode.optional(decode.string)) + decode.success(Character(id: id, name: name, status: status, species: species)) +} + +pub fn characters_to_json(input: Characters) -> json.Json { + json.object( + [ + #("results", json.nullable( + input.results, + fn(list) { json.array(from: list, of: character_to_json) }, + )), + ], + ) +} + +pub fn character_to_json(input: Character) -> json.Json { + json.object( + [ + #("id", json.nullable(input.id, json.string)), + #("name", json.nullable(input.name, json.string)), + #("status", json.nullable(input.status, json.string)), + #("species", json.nullable(input.species, json.string)), + ], + ) +} + +pub type GetCharactersResponse { + GetCharactersResponse(characters: Option(Characters)) +} + +pub fn get_characters_response_decoder() -> decode.Decoder(GetCharactersResponse) { + use characters <- decode.field("characters", decode.optional(characters_decoder())) + decode.success(GetCharactersResponse(characters: characters)) +} + +pub fn get_characters_response_to_json(input: GetCharactersResponse) -> json.Json { + json.object( + [ + #("characters", json.nullable(input.characters, characters_to_json)), + ], + ) +} + +pub fn get_characters(client: squall.Client) { + squall.execute_query( + client, + "query GetCharacters { characters { results { id name status species } } }", + json.object([]), + get_characters_response_decoder(), + ) +} diff --git a/examples/02-javascript/src/graphql/get_characters.gql b/examples/02-javascript/src/graphql/get_characters.gql new file mode 100644 index 0000000..4269012 --- /dev/null +++ b/examples/02-javascript/src/graphql/get_characters.gql @@ -0,0 +1,10 @@ +query GetCharacters { + characters { + results { + id + name + status + species + } + } +} diff --git a/examples/02-javascript/src/graphql/multi_query.gleam b/examples/02-javascript/src/graphql/multi_query.gleam new file mode 100644 index 0000000..dc55876 --- /dev/null +++ b/examples/02-javascript/src/graphql/multi_query.gleam @@ -0,0 +1,119 @@ +import gleam/dynamic/decode +import gleam/json +import squall +import gleam/option.{type Option} + +pub type Characters { + Characters(info: Option(Info), results: Option(List(Character))) +} + +pub fn characters_decoder() -> decode.Decoder(Characters) { + use info <- decode.field("info", decode.optional(info_decoder())) + use results <- decode.field("results", decode.optional(decode.list(character_decoder()))) + decode.success(Characters(info: info, results: results)) +} + +pub type Info { + Info(count: Option(Int)) +} + +pub fn info_decoder() -> decode.Decoder(Info) { + use count <- decode.field("count", decode.optional(decode.int)) + decode.success(Info(count: count)) +} + +pub type Character { + Character(name: Option(String)) +} + +pub fn character_decoder() -> decode.Decoder(Character) { + use name <- decode.field("name", decode.optional(decode.string)) + decode.success(Character(name: name)) +} + +pub type Location { + Location(id: Option(String)) +} + +pub fn location_decoder() -> decode.Decoder(Location) { + use id <- decode.field("id", decode.optional(decode.string)) + decode.success(Location(id: id)) +} + +pub type Episode { + Episode(id: Option(String)) +} + +pub fn episode_decoder() -> decode.Decoder(Episode) { + use id <- decode.field("id", decode.optional(decode.string)) + decode.success(Episode(id: id)) +} + +pub fn characters_to_json(input: Characters) -> json.Json { + json.object( + [ + #("info", json.nullable(input.info, info_to_json)), + #("results", json.nullable( + input.results, + fn(list) { json.array(from: list, of: character_to_json) }, + )), + ], + ) +} + +pub fn info_to_json(input: Info) -> json.Json { + json.object([#("count", json.nullable(input.count, json.int))]) +} + +pub fn character_to_json(input: Character) -> json.Json { + json.object([#("name", json.nullable(input.name, json.string))]) +} + +pub fn location_to_json(input: Location) -> json.Json { + json.object([#("id", json.nullable(input.id, json.string))]) +} + +pub fn episode_to_json(input: Episode) -> json.Json { + json.object([#("id", json.nullable(input.id, json.string))]) +} + +pub type MultiQueryResponse { + MultiQueryResponse( + characters: Option(Characters), + location: Option(Location), + episodes_by_ids: Option(List(Episode)), + ) +} + +pub fn multi_query_response_decoder() -> decode.Decoder(MultiQueryResponse) { + use characters <- decode.field("characters", decode.optional(characters_decoder())) + use location <- decode.field("location", decode.optional(location_decoder())) + use episodes_by_ids <- decode.field("episodesByIds", decode.optional(decode.list(episode_decoder()))) + decode.success(MultiQueryResponse( + characters: characters, + location: location, + episodes_by_ids: episodes_by_ids, + )) +} + +pub fn multi_query_response_to_json(input: MultiQueryResponse) -> json.Json { + json.object( + [ + #("characters", json.nullable(input.characters, characters_to_json)), + #("location", json.nullable(input.location, location_to_json)), + #("episodesByIds", json.nullable( + input.episodes_by_ids, + fn(list) { json.array(from: list, of: episode_to_json) }, + )), + ], + ) +} + +pub fn multi_query(client: squall.Client) { + squall.execute_query( + client, + "query MultiQuery { characters(page: 2, filter: { name: \"rick\" }) { info { count } results { name } } location(id: 1) { id } episodesByIds(ids: [1, 2]) { id } }", + json.object([]), + multi_query_response_decoder(), + ) +} diff --git a/examples/02-javascript/src/graphql/multi_query.gql b/examples/02-javascript/src/graphql/multi_query.gql new file mode 100644 index 0000000..1661f49 --- /dev/null +++ b/examples/02-javascript/src/graphql/multi_query.gql @@ -0,0 +1,16 @@ +query MultiQuery { + characters(page: 2, filter: { name: "rick" }) { + info { + count + } + results { + name + } + } + location(id: 1) { + id + } + episodesByIds(ids: [1, 2]) { + id + } +} diff --git a/examples/02-javascript/src/graphql/multi_query_with_vars.gleam b/examples/02-javascript/src/graphql/multi_query_with_vars.gleam new file mode 100644 index 0000000..d08fc2c --- /dev/null +++ b/examples/02-javascript/src/graphql/multi_query_with_vars.gleam @@ -0,0 +1,138 @@ +import gleam/dynamic/decode +import gleam/json +import squall +import gleam/option.{type Option} + +pub type Characters { + Characters(info: Option(Info), results: Option(List(Character))) +} + +pub fn characters_decoder() -> decode.Decoder(Characters) { + use info <- decode.field("info", decode.optional(info_decoder())) + use results <- decode.field("results", decode.optional(decode.list(character_decoder()))) + decode.success(Characters(info: info, results: results)) +} + +pub type Info { + Info(count: Option(Int)) +} + +pub fn info_decoder() -> decode.Decoder(Info) { + use count <- decode.field("count", decode.optional(decode.int)) + decode.success(Info(count: count)) +} + +pub type Character { + Character(name: Option(String)) +} + +pub fn character_decoder() -> decode.Decoder(Character) { + use name <- decode.field("name", decode.optional(decode.string)) + decode.success(Character(name: name)) +} + +pub type Location { + Location(id: Option(String), name: Option(String)) +} + +pub fn location_decoder() -> decode.Decoder(Location) { + use id <- decode.field("id", decode.optional(decode.string)) + use name <- decode.field("name", decode.optional(decode.string)) + decode.success(Location(id: id, name: name)) +} + +pub type Episode { + Episode(id: Option(String), name: Option(String)) +} + +pub fn episode_decoder() -> decode.Decoder(Episode) { + use id <- decode.field("id", decode.optional(decode.string)) + use name <- decode.field("name", decode.optional(decode.string)) + decode.success(Episode(id: id, name: name)) +} + +pub fn characters_to_json(input: Characters) -> json.Json { + json.object( + [ + #("info", json.nullable(input.info, info_to_json)), + #("results", json.nullable( + input.results, + fn(list) { json.array(from: list, of: character_to_json) }, + )), + ], + ) +} + +pub fn info_to_json(input: Info) -> json.Json { + json.object([#("count", json.nullable(input.count, json.int))]) +} + +pub fn character_to_json(input: Character) -> json.Json { + json.object([#("name", json.nullable(input.name, json.string))]) +} + +pub fn location_to_json(input: Location) -> json.Json { + json.object( + [ + #("id", json.nullable(input.id, json.string)), + #("name", json.nullable(input.name, json.string)), + ], + ) +} + +pub fn episode_to_json(input: Episode) -> json.Json { + json.object( + [ + #("id", json.nullable(input.id, json.string)), + #("name", json.nullable(input.name, json.string)), + ], + ) +} + +pub type MultiQueryWithVarsResponse { + MultiQueryWithVarsResponse( + characters: Option(Characters), + location: Option(Location), + episodes_by_ids: Option(List(Episode)), + ) +} + +pub fn multi_query_with_vars_response_decoder() -> decode.Decoder(MultiQueryWithVarsResponse) { + use characters <- decode.field("characters", decode.optional(characters_decoder())) + use location <- decode.field("location", decode.optional(location_decoder())) + use episodes_by_ids <- decode.field("episodesByIds", decode.optional(decode.list(episode_decoder()))) + decode.success(MultiQueryWithVarsResponse( + characters: characters, + location: location, + episodes_by_ids: episodes_by_ids, + )) +} + +pub fn multi_query_with_vars_response_to_json(input: MultiQueryWithVarsResponse) -> json.Json { + json.object( + [ + #("characters", json.nullable(input.characters, characters_to_json)), + #("location", json.nullable(input.location, location_to_json)), + #("episodesByIds", json.nullable( + input.episodes_by_ids, + fn(list) { json.array(from: list, of: episode_to_json) }, + )), + ], + ) +} + +pub fn multi_query_with_vars(client: squall.Client, page: Int, name: String, location_id: String, episode_ids: List(Int)) { + squall.execute_query( + client, + "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 } }", + json.object( + [ + #("page", json.int(page)), + #("name", json.string(name)), + #("locationId", json.string(location_id)), + #("episodeIds", json.array(from: episode_ids, of: json.int)), + ], + ), + multi_query_with_vars_response_decoder(), + ) +} diff --git a/examples/02-javascript/src/graphql/multi_query_with_vars.gql b/examples/02-javascript/src/graphql/multi_query_with_vars.gql new file mode 100644 index 0000000..8ea41e7 --- /dev/null +++ b/examples/02-javascript/src/graphql/multi_query_with_vars.gql @@ -0,0 +1,18 @@ +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 + } +} diff --git a/examples/02-javascript/src/javascript_example.gleam b/examples/02-javascript/src/javascript_example.gleam new file mode 100644 index 0000000..da0cefe --- /dev/null +++ b/examples/02-javascript/src/javascript_example.gleam @@ -0,0 +1,43 @@ +import gleam/io +import gleam/javascript/promise +import gleam/json +import gleam/string +import graphql/multi_query +import squall + +pub fn main() { + io.println("Squall Multi-Field Query Example (JavaScript/Node.js)") + io.println("======================================================\n") + + // Create a JavaScript client (uses Fetch API for HTTP requests) + let client = + squall.new_javascript_client("https://rickandmortyapi.com/graphql", []) + + // On JavaScript, multi_query returns a Promise that resolves to a Result + // We use promise.await to wait for the result + multi_query.multi_query(client) + |> promise.await(fn(result) { + case result { + Ok(response) -> { + io.println("✓ Success! Received response from API\n") + + // Print the Gleam data structure + io.println("Gleam Response Structure:") + io.println("-------------------------") + io.println(string.inspect(response)) + + // Convert to JSON and print + io.println("\nJSON Response:") + io.println("--------------") + let json_response = multi_query.multi_query_response_to_json(response) + io.println(json.to_string(json_response)) + + promise.resolve(Nil) + } + Error(err) -> { + io.println("✗ Error: " <> err) + promise.resolve(Nil) + } + } + }) +} diff --git a/examples/03-isomorphic/README.md b/examples/03-isomorphic/README.md new file mode 100644 index 0000000..e932c9b --- /dev/null +++ b/examples/03-isomorphic/README.md @@ -0,0 +1,135 @@ +# Squall Isomorphic Example + +This example demonstrates writing **cross-platform** Gleam code that works on **both Erlang and JavaScript** targets using Squall. + +## What's Inside + +- `isomorphic_example.gleam` - Code that runs on both targets +- `src/graphql/*.gql` - GraphQL query definitions +- `src/graphql/*.gleam` - Generated type-safe Gleam code + +## The Magic: Target-Conditional Code + +The same codebase works on both Erlang and JavaScript using `@target()` attributes: + +```gleam +import squall +import graphql/multi_query + +@target(javascript) +import gleam/javascript/promise + +// Shared business logic - works on both targets! +fn handle_response(response: multi_query.MultiQueryResponse) -> Nil { + io.println("✓ Success!") + io.println(string.inspect(response)) +} + +fn handle_error(err: String) -> Nil { + io.println("✗ Error: " <> err) +} + +// Erlang implementation - synchronous +@target(erlang) +fn run() -> Nil { + let client = squall.new_erlang_client("https://rickandmortyapi.com/graphql", []) + let result = multi_query.multi_query(client) + + case result { + Ok(response) -> handle_response(response) + Error(err) -> handle_error(err) + } +} + +// JavaScript implementation - asynchronous with Promises +@target(javascript) +fn run() -> promise.Promise(Nil) { + let client = squall.new_javascript_client("https://rickandmortyapi.com/graphql", []) + + multi_query.multi_query(client) + |> promise.await(fn(result) { + case result { + Ok(response) -> { + handle_response(response) + promise.resolve(Nil) + } + Error(err) -> { + handle_error(err) + promise.resolve(Nil) + } + } + }) +} + +pub fn main() { + run() // Works on both targets! +} +``` + +## Running on Erlang + +```bash +# Build and run on Erlang +gleam run -m isomorphic_example +``` + +Output: `"Running on Erlang target (using gleam_httpc)"` + +## Running on JavaScript + +```bash +# Build and run on JavaScript (Node.js) +gleam run --target javascript -m isomorphic_example +``` + +Output: `"Running on JavaScript target (using Fetch API)"` + +## How It Works + +1. **Compile time**: Gleam includes only the code for the target platform + - On Erlang: Only the `@target(erlang)` functions are compiled + - On JavaScript: Only the `@target(javascript)` functions are compiled + +2. **Runtime**: The appropriate HTTP adapter is used automatically + - Erlang → `gleam_httpc` (synchronous, returns `Result`) + - JavaScript → Fetch API (asynchronous, returns `Promise(Result)`) + +3. **Shared business logic**: Response/error handlers work on both targets + - `handle_response()` and `handle_error()` are platform-agnostic + - Only the HTTP execution layer differs between platforms + +4. **Generated code**: Completely platform-agnostic! + - The same GraphQL code works everywhere + - No platform-specific imports in generated files + - Automatically adapts to sync (Erlang) or async (JavaScript) execution + +## Build Artifacts + +### Erlang Target +```bash +gleam build +# Outputs to: build/dev/erlang/ +``` + +### JavaScript Target +```bash +gleam build --target javascript +# Outputs to: build/dev/javascript/ +``` + +## Use Cases + +This pattern is perfect for: + +✅ **Full-stack Gleam apps** - Share GraphQL clients between backend (Erlang) and frontend (JavaScript) +✅ **Multi-platform libraries** - Write once, run everywhere +✅ **Migration scenarios** - Gradually move from one target to another +✅ **Testing** - Test the same business logic on different runtimes + +## Features Demonstrated + +✅ **Cross-Platform Code** - Single codebase for both targets +✅ **Conditional Compilation** - `@target()` attributes +✅ **HTTP Adapter Pattern** - Platform-specific implementations +✅ **Type Safety** - Fully typed across all targets +✅ **Zero Duplication** - Generated code shared between targets diff --git a/examples/03-isomorphic/gleam.toml b/examples/03-isomorphic/gleam.toml new file mode 100644 index 0000000..8280df7 --- /dev/null +++ b/examples/03-isomorphic/gleam.toml @@ -0,0 +1,10 @@ +name = "isomorphic_example" +version = "0.1.0" +description = "Squall isomorphic example - Works on Erlang AND JavaScript" + +[dependencies] +gleam_stdlib = ">= 0.65.0 and < 0.66.0" +gleam_json = ">= 3.0.0 and < 4.0.0" +gleam_http = ">= 4.3.0 and < 5.0.0" +gleam_javascript = ">= 0.3.0 and < 2.0.0" +squall = { path = "../.." } diff --git a/examples/03-isomorphic/manifest.toml b/examples/03-isomorphic/manifest.toml new file mode 100644 index 0000000..92a097c --- /dev/null +++ b/examples/03-isomorphic/manifest.toml @@ -0,0 +1,24 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, + { name = "glam", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "237C2CE218A2A0A5D46D625F8EF5B78F964BC91018B78D692B17E1AB84295229" }, + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, + { 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" }, + { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, + { 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" }, + { name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" }, + { name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" }, + { name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" }, + { name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" }, + { 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 = "../.." }, +] + +[requirements] +gleam_http = { version = ">= 4.3.0 and < 5.0.0" } +gleam_javascript = { version = ">= 0.3.0 and < 2.0.0" } +gleam_json = { version = ">= 3.0.0 and < 4.0.0" } +gleam_stdlib = { version = ">= 0.65.0 and < 0.66.0" } +squall = { path = "../.." } diff --git a/examples/03-isomorphic/src/graphql/get_character.gleam b/examples/03-isomorphic/src/graphql/get_character.gleam new file mode 100644 index 0000000..28a00b7 --- /dev/null +++ b/examples/03-isomorphic/src/graphql/get_character.gleam @@ -0,0 +1,71 @@ +import gleam/dynamic/decode +import gleam/json +import squall +import gleam/option.{type Option} + +pub type Character { + Character( + id: Option(String), + name: Option(String), + status: Option(String), + species: Option(String), + type_: Option(String), + gender: Option(String), + ) +} + +pub fn character_decoder() -> decode.Decoder(Character) { + use id <- decode.field("id", decode.optional(decode.string)) + use name <- decode.field("name", decode.optional(decode.string)) + use status <- decode.field("status", decode.optional(decode.string)) + use species <- decode.field("species", decode.optional(decode.string)) + use type_ <- decode.field("type", decode.optional(decode.string)) + use gender <- decode.field("gender", decode.optional(decode.string)) + decode.success(Character( + id: id, + name: name, + status: status, + species: species, + type_: type_, + gender: gender, + )) +} + +pub fn character_to_json(input: Character) -> json.Json { + json.object( + [ + #("id", json.nullable(input.id, json.string)), + #("name", json.nullable(input.name, json.string)), + #("status", json.nullable(input.status, json.string)), + #("species", json.nullable(input.species, json.string)), + #("type", json.nullable(input.type_, json.string)), + #("gender", json.nullable(input.gender, json.string)), + ], + ) +} + +pub type GetCharacterResponse { + GetCharacterResponse(character: Option(Character)) +} + +pub fn get_character_response_decoder() -> decode.Decoder(GetCharacterResponse) { + use character <- decode.field("character", decode.optional(character_decoder())) + decode.success(GetCharacterResponse(character: character)) +} + +pub fn get_character_response_to_json(input: GetCharacterResponse) -> json.Json { + json.object( + [ + #("character", json.nullable(input.character, character_to_json)), + ], + ) +} + +pub fn get_character(client: squall.Client, id: String) { + squall.execute_query( + client, + "query GetCharacter($id: ID!) { character(id: $id) { id name status species type gender } }", + json.object([#("id", json.string(id))]), + get_character_response_decoder(), + ) +} diff --git a/examples/03-isomorphic/src/graphql/get_character.gql b/examples/03-isomorphic/src/graphql/get_character.gql new file mode 100644 index 0000000..56d6948 --- /dev/null +++ b/examples/03-isomorphic/src/graphql/get_character.gql @@ -0,0 +1,10 @@ +query GetCharacter($id: ID!) { + character(id: $id) { + id + name + status + species + type + gender + } +} diff --git a/examples/03-isomorphic/src/graphql/get_characters.gleam b/examples/03-isomorphic/src/graphql/get_characters.gleam new file mode 100644 index 0000000..b3ca40b --- /dev/null +++ b/examples/03-isomorphic/src/graphql/get_characters.gleam @@ -0,0 +1,78 @@ +import gleam/dynamic/decode +import gleam/json +import squall +import gleam/option.{type Option} + +pub type Characters { + Characters(results: Option(List(Character))) +} + +pub fn characters_decoder() -> decode.Decoder(Characters) { + use results <- decode.field("results", decode.optional(decode.list(character_decoder()))) + decode.success(Characters(results: results)) +} + +pub type Character { + Character( + id: Option(String), + name: Option(String), + status: Option(String), + species: Option(String), + ) +} + +pub fn character_decoder() -> decode.Decoder(Character) { + use id <- decode.field("id", decode.optional(decode.string)) + use name <- decode.field("name", decode.optional(decode.string)) + use status <- decode.field("status", decode.optional(decode.string)) + use species <- decode.field("species", decode.optional(decode.string)) + decode.success(Character(id: id, name: name, status: status, species: species)) +} + +pub fn characters_to_json(input: Characters) -> json.Json { + json.object( + [ + #("results", json.nullable( + input.results, + fn(list) { json.array(from: list, of: character_to_json) }, + )), + ], + ) +} + +pub fn character_to_json(input: Character) -> json.Json { + json.object( + [ + #("id", json.nullable(input.id, json.string)), + #("name", json.nullable(input.name, json.string)), + #("status", json.nullable(input.status, json.string)), + #("species", json.nullable(input.species, json.string)), + ], + ) +} + +pub type GetCharactersResponse { + GetCharactersResponse(characters: Option(Characters)) +} + +pub fn get_characters_response_decoder() -> decode.Decoder(GetCharactersResponse) { + use characters <- decode.field("characters", decode.optional(characters_decoder())) + decode.success(GetCharactersResponse(characters: characters)) +} + +pub fn get_characters_response_to_json(input: GetCharactersResponse) -> json.Json { + json.object( + [ + #("characters", json.nullable(input.characters, characters_to_json)), + ], + ) +} + +pub fn get_characters(client: squall.Client) { + squall.execute_query( + client, + "query GetCharacters { characters { results { id name status species } } }", + json.object([]), + get_characters_response_decoder(), + ) +} diff --git a/examples/03-isomorphic/src/graphql/get_characters.gql b/examples/03-isomorphic/src/graphql/get_characters.gql new file mode 100644 index 0000000..4269012 --- /dev/null +++ b/examples/03-isomorphic/src/graphql/get_characters.gql @@ -0,0 +1,10 @@ +query GetCharacters { + characters { + results { + id + name + status + species + } + } +} diff --git a/examples/03-isomorphic/src/graphql/multi_query.gleam b/examples/03-isomorphic/src/graphql/multi_query.gleam new file mode 100644 index 0000000..dc55876 --- /dev/null +++ b/examples/03-isomorphic/src/graphql/multi_query.gleam @@ -0,0 +1,119 @@ +import gleam/dynamic/decode +import gleam/json +import squall +import gleam/option.{type Option} + +pub type Characters { + Characters(info: Option(Info), results: Option(List(Character))) +} + +pub fn characters_decoder() -> decode.Decoder(Characters) { + use info <- decode.field("info", decode.optional(info_decoder())) + use results <- decode.field("results", decode.optional(decode.list(character_decoder()))) + decode.success(Characters(info: info, results: results)) +} + +pub type Info { + Info(count: Option(Int)) +} + +pub fn info_decoder() -> decode.Decoder(Info) { + use count <- decode.field("count", decode.optional(decode.int)) + decode.success(Info(count: count)) +} + +pub type Character { + Character(name: Option(String)) +} + +pub fn character_decoder() -> decode.Decoder(Character) { + use name <- decode.field("name", decode.optional(decode.string)) + decode.success(Character(name: name)) +} + +pub type Location { + Location(id: Option(String)) +} + +pub fn location_decoder() -> decode.Decoder(Location) { + use id <- decode.field("id", decode.optional(decode.string)) + decode.success(Location(id: id)) +} + +pub type Episode { + Episode(id: Option(String)) +} + +pub fn episode_decoder() -> decode.Decoder(Episode) { + use id <- decode.field("id", decode.optional(decode.string)) + decode.success(Episode(id: id)) +} + +pub fn characters_to_json(input: Characters) -> json.Json { + json.object( + [ + #("info", json.nullable(input.info, info_to_json)), + #("results", json.nullable( + input.results, + fn(list) { json.array(from: list, of: character_to_json) }, + )), + ], + ) +} + +pub fn info_to_json(input: Info) -> json.Json { + json.object([#("count", json.nullable(input.count, json.int))]) +} + +pub fn character_to_json(input: Character) -> json.Json { + json.object([#("name", json.nullable(input.name, json.string))]) +} + +pub fn location_to_json(input: Location) -> json.Json { + json.object([#("id", json.nullable(input.id, json.string))]) +} + +pub fn episode_to_json(input: Episode) -> json.Json { + json.object([#("id", json.nullable(input.id, json.string))]) +} + +pub type MultiQueryResponse { + MultiQueryResponse( + characters: Option(Characters), + location: Option(Location), + episodes_by_ids: Option(List(Episode)), + ) +} + +pub fn multi_query_response_decoder() -> decode.Decoder(MultiQueryResponse) { + use characters <- decode.field("characters", decode.optional(characters_decoder())) + use location <- decode.field("location", decode.optional(location_decoder())) + use episodes_by_ids <- decode.field("episodesByIds", decode.optional(decode.list(episode_decoder()))) + decode.success(MultiQueryResponse( + characters: characters, + location: location, + episodes_by_ids: episodes_by_ids, + )) +} + +pub fn multi_query_response_to_json(input: MultiQueryResponse) -> json.Json { + json.object( + [ + #("characters", json.nullable(input.characters, characters_to_json)), + #("location", json.nullable(input.location, location_to_json)), + #("episodesByIds", json.nullable( + input.episodes_by_ids, + fn(list) { json.array(from: list, of: episode_to_json) }, + )), + ], + ) +} + +pub fn multi_query(client: squall.Client) { + squall.execute_query( + client, + "query MultiQuery { characters(page: 2, filter: { name: \"rick\" }) { info { count } results { name } } location(id: 1) { id } episodesByIds(ids: [1, 2]) { id } }", + json.object([]), + multi_query_response_decoder(), + ) +} diff --git a/examples/03-isomorphic/src/graphql/multi_query.gql b/examples/03-isomorphic/src/graphql/multi_query.gql new file mode 100644 index 0000000..1661f49 --- /dev/null +++ b/examples/03-isomorphic/src/graphql/multi_query.gql @@ -0,0 +1,16 @@ +query MultiQuery { + characters(page: 2, filter: { name: "rick" }) { + info { + count + } + results { + name + } + } + location(id: 1) { + id + } + episodesByIds(ids: [1, 2]) { + id + } +} diff --git a/examples/03-isomorphic/src/graphql/multi_query_with_vars.gleam b/examples/03-isomorphic/src/graphql/multi_query_with_vars.gleam new file mode 100644 index 0000000..d08fc2c --- /dev/null +++ b/examples/03-isomorphic/src/graphql/multi_query_with_vars.gleam @@ -0,0 +1,138 @@ +import gleam/dynamic/decode +import gleam/json +import squall +import gleam/option.{type Option} + +pub type Characters { + Characters(info: Option(Info), results: Option(List(Character))) +} + +pub fn characters_decoder() -> decode.Decoder(Characters) { + use info <- decode.field("info", decode.optional(info_decoder())) + use results <- decode.field("results", decode.optional(decode.list(character_decoder()))) + decode.success(Characters(info: info, results: results)) +} + +pub type Info { + Info(count: Option(Int)) +} + +pub fn info_decoder() -> decode.Decoder(Info) { + use count <- decode.field("count", decode.optional(decode.int)) + decode.success(Info(count: count)) +} + +pub type Character { + Character(name: Option(String)) +} + +pub fn character_decoder() -> decode.Decoder(Character) { + use name <- decode.field("name", decode.optional(decode.string)) + decode.success(Character(name: name)) +} + +pub type Location { + Location(id: Option(String), name: Option(String)) +} + +pub fn location_decoder() -> decode.Decoder(Location) { + use id <- decode.field("id", decode.optional(decode.string)) + use name <- decode.field("name", decode.optional(decode.string)) + decode.success(Location(id: id, name: name)) +} + +pub type Episode { + Episode(id: Option(String), name: Option(String)) +} + +pub fn episode_decoder() -> decode.Decoder(Episode) { + use id <- decode.field("id", decode.optional(decode.string)) + use name <- decode.field("name", decode.optional(decode.string)) + decode.success(Episode(id: id, name: name)) +} + +pub fn characters_to_json(input: Characters) -> json.Json { + json.object( + [ + #("info", json.nullable(input.info, info_to_json)), + #("results", json.nullable( + input.results, + fn(list) { json.array(from: list, of: character_to_json) }, + )), + ], + ) +} + +pub fn info_to_json(input: Info) -> json.Json { + json.object([#("count", json.nullable(input.count, json.int))]) +} + +pub fn character_to_json(input: Character) -> json.Json { + json.object([#("name", json.nullable(input.name, json.string))]) +} + +pub fn location_to_json(input: Location) -> json.Json { + json.object( + [ + #("id", json.nullable(input.id, json.string)), + #("name", json.nullable(input.name, json.string)), + ], + ) +} + +pub fn episode_to_json(input: Episode) -> json.Json { + json.object( + [ + #("id", json.nullable(input.id, json.string)), + #("name", json.nullable(input.name, json.string)), + ], + ) +} + +pub type MultiQueryWithVarsResponse { + MultiQueryWithVarsResponse( + characters: Option(Characters), + location: Option(Location), + episodes_by_ids: Option(List(Episode)), + ) +} + +pub fn multi_query_with_vars_response_decoder() -> decode.Decoder(MultiQueryWithVarsResponse) { + use characters <- decode.field("characters", decode.optional(characters_decoder())) + use location <- decode.field("location", decode.optional(location_decoder())) + use episodes_by_ids <- decode.field("episodesByIds", decode.optional(decode.list(episode_decoder()))) + decode.success(MultiQueryWithVarsResponse( + characters: characters, + location: location, + episodes_by_ids: episodes_by_ids, + )) +} + +pub fn multi_query_with_vars_response_to_json(input: MultiQueryWithVarsResponse) -> json.Json { + json.object( + [ + #("characters", json.nullable(input.characters, characters_to_json)), + #("location", json.nullable(input.location, location_to_json)), + #("episodesByIds", json.nullable( + input.episodes_by_ids, + fn(list) { json.array(from: list, of: episode_to_json) }, + )), + ], + ) +} + +pub fn multi_query_with_vars(client: squall.Client, page: Int, name: String, location_id: String, episode_ids: List(Int)) { + squall.execute_query( + client, + "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 } }", + json.object( + [ + #("page", json.int(page)), + #("name", json.string(name)), + #("locationId", json.string(location_id)), + #("episodeIds", json.array(from: episode_ids, of: json.int)), + ], + ), + multi_query_with_vars_response_decoder(), + ) +} diff --git a/examples/03-isomorphic/src/graphql/multi_query_with_vars.gql b/examples/03-isomorphic/src/graphql/multi_query_with_vars.gql new file mode 100644 index 0000000..8ea41e7 --- /dev/null +++ b/examples/03-isomorphic/src/graphql/multi_query_with_vars.gql @@ -0,0 +1,18 @@ +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 + } +} diff --git a/examples/03-isomorphic/src/isomorphic_example.gleam b/examples/03-isomorphic/src/isomorphic_example.gleam new file mode 100644 index 0000000..1360d2c --- /dev/null +++ b/examples/03-isomorphic/src/isomorphic_example.gleam @@ -0,0 +1,76 @@ +import gleam/io +import gleam/json +import gleam/string +import graphql/multi_query +import squall + +@target(javascript) +import gleam/javascript/promise + +// This example demonstrates how to write isomorphic code +// that works on both Erlang and JavaScript targets + +pub fn main() { + io.println("Squall Isomorphic Example") + io.println("=========================\n") + + // Use target-specific main implementations + run() +} + +// Shared function to handle successful responses +fn handle_response(response: multi_query.MultiQueryResponse) -> Nil { + io.println("✓ Success! Received response from API\n") + + // Print the Gleam data structure + io.println("Gleam Response Structure:") + io.println("-------------------------") + io.println(string.inspect(response)) + + // Convert to JSON and print + io.println("\nJSON Response:") + io.println("--------------") + let json_response = multi_query.multi_query_response_to_json(response) + io.println(json.to_string(json_response)) +} + +// Shared function to handle errors +fn handle_error(err: String) -> Nil { + io.println("✗ Error: " <> err) +} + +// Erlang implementation - synchronous +@target(erlang) +fn run() -> Nil { + io.println("Running on Erlang target (using gleam_httpc)\n") + + let client = squall.new_erlang_client("https://rickandmortyapi.com/graphql", []) + let result = multi_query.multi_query(client) + + case result { + Ok(response) -> handle_response(response) + Error(err) -> handle_error(err) + } +} + +// JavaScript implementation - asynchronous +@target(javascript) +fn run() -> promise.Promise(Nil) { + io.println("Running on JavaScript target (using Fetch API)\n") + + let client = squall.new_javascript_client("https://rickandmortyapi.com/graphql", []) + + multi_query.multi_query(client) + |> promise.await(fn(result) { + case result { + Ok(response) -> { + handle_response(response) + promise.resolve(Nil) + } + Error(err) -> { + handle_error(err) + promise.resolve(Nil) + } + } + }) +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..fa6911c --- /dev/null +++ b/examples/README.md @@ -0,0 +1,177 @@ +# Squall Examples + +Welcome to the Squall examples! These demonstrate how to use Squall's **isomorphic GraphQL client generator** with the Rick and Morty API. + +## 🌊 What is Squall? + +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. + +## Examples Overview + +### [01-erlang](./01-erlang/) - Erlang/OTP Target + +Demonstrates Squall on the Erlang/OTP runtime using `gleam_httpc`. + +```bash +cd 01-erlang +gleam run -m erlang_example +``` + +**Perfect for:** Backend services, servers, distributed systems + +### [02-javascript](./02-javascript/) - JavaScript Target + +Demonstrates Squall on JavaScript (Node.js/Browser) using the Fetch API. + +```bash +cd 02-javascript +gleam run -m javascript_example +``` + +**Perfect for:** Frontend apps, serverless functions, Deno + +### [03-isomorphic](./03-isomorphic/) - Cross-Platform + +Demonstrates writing code that works on **both** Erlang and JavaScript! + +```bash +cd 03-isomorphic + +# Run on Erlang +gleam run -m isomorphic_example + +# Run on JavaScript +gleam run --target javascript -m isomorphic_example +``` + +**Perfect for:** Full-stack apps, shared libraries, universal code + +## Quick Start + +### 1. Generate GraphQL Code + +From the Squall root directory: + +```bash +gleam run -m squall generate https://rickandmortyapi.com/graphql +``` + +This discovers all `.gql` files in the examples and generates type-safe Gleam code. + +### 2. Choose Your Target + +Pick the example that matches your use case: + +| Target | Example | Use Case | +|--------|---------|----------| +| **Erlang** | `01-erlang` | Backend, servers | +| **JavaScript** | `02-javascript` | Frontend, Node.js | +| **Both** | `03-isomorphic` | Full-stack apps | + +### 3. Run It! + +```bash +cd +gleam run +``` + +## The Key Difference + +The **only difference** between Erlang and JavaScript code is the client creation: + +```gleam +// Erlang +let client = squall.new_erlang_client(endpoint, headers) + +// JavaScript +let client = squall.new_javascript_client(endpoint, headers) + +// Cross-platform (uses @target conditionals) +@target(erlang) +fn create_client() -> squall.Client { + squall.new_erlang_client(endpoint, headers) +} + +@target(javascript) +fn create_client() -> squall.Client { + squall.new_javascript_client(endpoint, headers) +} +``` + +**Everything else is identical!** The same generated GraphQL code works everywhere. + +## API Endpoint + +All examples use the public Rick and Morty GraphQL API: + +``` +https://rickandmortyapi.com/graphql +``` + +No API key required! + +## GraphQL Queries + +Each example includes the same `.gql` files: + +- **`get_character.gql`** - Fetch a single character by ID +- **`get_characters.gql`** - Fetch a list of characters +- **`multi_query.gql`** - Multiple fields in one query +- **`multi_query_with_vars.gql`** - Query with GraphQL variables + +## Features Demonstrated + +✅ **Type Safety** - All queries fully typed from GraphQL schema +✅ **Isomorphic** - Same code works on Erlang and JavaScript +✅ **Variables** - Type-safe GraphQL variable handling +✅ **Nested Objects** - Automatic decoder generation for complex types +✅ **Optional Fields** - Proper `Option` type handling +✅ **JSON Serialization** - Convert responses to/from JSON +✅ **HTTP Adapters** - Platform-specific HTTP implementations + +## Project Structure + +``` +examples/ +├── README.md # This file +├── 01-erlang/ # Erlang example +│ ├── gleam.toml +│ ├── README.md +│ └── src/ +│ ├── erlang_example.gleam +│ ├── example_with_vars.gleam +│ └── graphql/ +│ ├── *.gql # GraphQL queries +│ └── *.gleam # Generated code +├── 02-javascript/ # JavaScript example +│ ├── gleam.toml +│ ├── README.md +│ └── src/ +│ ├── javascript_example.gleam +│ └── graphql/ +│ ├── *.gql +│ └── *.gleam +└── 03-isomorphic/ # Cross-platform example + ├── gleam.toml + ├── README.md + └── src/ + ├── isomorphic_example.gleam + └── graphql/ + ├── *.gql + └── *.gleam +``` + +## Next Steps + +1. **Explore the examples** - Start with the one matching your target +2. **Read the individual READMEs** - Each has detailed instructions +3. **Modify the queries** - Edit `.gql` files and regenerate +4. **Try your own API** - Point Squall at your GraphQL endpoint! + +## Learn More + +- [Squall Documentation](../../CLAUDE.md) +- [Rick and Morty GraphQL API](https://rickandmortyapi.com/documentation/#graphql) +- [Gleam Language](https://gleam.run) + +Happy querying! 🌊 diff --git a/gleam.toml b/gleam.toml index 943385e..4c466dd 100644 --- a/gleam.toml +++ b/gleam.toml @@ -9,6 +9,8 @@ gleam_stdlib = ">= 0.65.0 and < 0.66.0" gleam_json = ">= 3.0.0 and < 4.0.0" gleam_http = ">= 4.3.0 and < 5.0.0" gleam_httpc = ">= 5.0.0 and < 6.0.0" +gleam_fetch = ">= 1.0.0 and < 2.0.0" +gleam_javascript = ">= 0.3.0 and < 2.0.0" simplifile = ">= 2.0.0 and < 3.0.0" filepath = ">= 1.0.0 and < 2.0.0" glam = ">= 2.0.0 and < 3.0.0" diff --git a/manifest.toml b/manifest.toml index 0f3fcd2..ef11a14 100644 --- a/manifest.toml +++ b/manifest.toml @@ -11,8 +11,10 @@ packages = [ { name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" }, { name = "gleam_community_colour", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "E34DD2C896AC3792151EDA939DA435FF3B69922F33415ED3C4406C932FBE9634" }, { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, + { 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" }, { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, { 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" }, + { name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" }, { name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" }, { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, { name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" }, @@ -31,6 +33,7 @@ argv = { version = ">= 1.0.0 and < 2.0.0" } birdie = { version = ">= 1.0.0 and < 2.0.0" } filepath = { version = ">= 1.0.0 and < 2.0.0" } glam = { version = ">= 2.0.0 and < 3.0.0" } +gleam_fetch = { version = ">= 1.0.0 and < 2.0.0" } gleam_http = { version = ">= 4.3.0 and < 5.0.0" } gleam_httpc = { version = ">= 5.0.0 and < 6.0.0" } gleam_json = { version = ">= 3.0.0 and < 4.0.0" } diff --git a/src/squall.gleam b/src/squall.gleam index 99ea5b8..23bd1f3 100644 --- a/src/squall.gleam +++ b/src/squall.gleam @@ -1,4 +1,5 @@ import argv +import gleam/dynamic/decode import gleam/http import gleam/http/request import gleam/httpc @@ -8,25 +9,199 @@ import gleam/list import gleam/result import gleam/string import simplifile +import squall/adapter.{type HttpAdapter} import squall/internal/codegen import squall/internal/discovery import squall/internal/error import squall/internal/parser import squall/internal/schema -/// A GraphQL client with endpoint and headers configuration +@target(erlang) +import squall/adapter/erlang + +@target(javascript) +import squall/adapter/javascript + +@target(javascript) +import gleam/javascript/promise.{type Promise} + +/// A GraphQL client with endpoint, headers, and HTTP adapter configuration pub type Client { - Client(endpoint: String, headers: List(#(String, String))) + Client( + endpoint: String, + headers: List(#(String, String)), + send_request: HttpAdapter, + ) +} + +/// Create a new GraphQL client with custom headers and HTTP adapter. +/// For most cases, use target-specific constructors like `new_erlang_client` or `new_javascript_client`. +pub fn new_client( + endpoint: String, + headers: List(#(String, String)), + send_request: HttpAdapter, +) -> Client { + Client(endpoint: endpoint, headers: headers, send_request: send_request) +} + +/// Create a new GraphQL client with bearer token authentication and HTTP adapter. +pub fn new_client_with_auth( + endpoint: String, + token: String, + send_request: HttpAdapter, +) -> Client { + Client( + endpoint: endpoint, + headers: [#("Authorization", "Bearer " <> token)], + send_request: send_request, + ) +} + +/// Create a new Erlang GraphQL client with custom headers. +/// This uses the Erlang HTTP adapter (gleam_httpc). +@target(erlang) +pub fn new_erlang_client( + endpoint: String, + headers: List(#(String, String)), +) -> Client { + Client(endpoint: endpoint, headers: headers, send_request: erlang.adapter()) } -/// Create a new GraphQL client with custom headers -pub fn new_client(endpoint: String, headers: List(#(String, String))) -> Client { - Client(endpoint: endpoint, headers: headers) +/// Create a new Erlang GraphQL client with bearer token authentication. +@target(erlang) +pub fn new_erlang_client_with_auth(endpoint: String, token: String) -> Client { + Client( + endpoint: endpoint, + headers: [#("Authorization", "Bearer " <> token)], + send_request: erlang.adapter(), + ) +} + +/// Create a new JavaScript GraphQL client with custom headers. +/// This uses the JavaScript HTTP adapter (Fetch API). +@target(javascript) +pub fn new_javascript_client( + endpoint: String, + headers: List(#(String, String)), +) -> Client { + Client(endpoint: endpoint, headers: headers, send_request: javascript.adapter()) +} + +/// Create a new JavaScript GraphQL client with bearer token authentication. +@target(javascript) +pub fn new_javascript_client_with_auth(endpoint: String, token: String) -> Client { + Client( + endpoint: endpoint, + headers: [#("Authorization", "Bearer " <> token)], + send_request: javascript.adapter(), + ) +} + +/// Execute a GraphQL query on JavaScript targets. +/// Returns a Promise that resolves to a Result containing the decoded response. +@target(javascript) +pub fn execute_query( + client: Client, + query: String, + variables: json.Json, + decoder: decode.Decoder(a), +) -> Promise(Result(a, String)) { + let body = + json.object([#("query", json.string(query)), #("variables", variables)]) + + // Build the request + let req_result = + request.to(client.endpoint) + |> result.map_error(fn(_) { "Invalid endpoint URL" }) + |> result.map(fn(req) { + req + |> request.set_method(http.Post) + |> request.set_body(json.to_string(body)) + |> request.set_header("content-type", "application/json") + }) + |> result.map(fn(req) { + list.fold(client.headers, req, fn(r, header) { + request.set_header(r, header.0, header.1) + }) + }) + + // Convert Result to Promise + case req_result { + Error(e) -> promise.resolve(Error(e)) + Ok(req) -> { + // Send request and process response + client.send_request(req) + |> promise.map(result.map_error(_, fn(_) { "HTTP request failed" })) + |> promise.try_await(fn(resp) { + // Parse JSON + let json_result = + json.parse(from: resp.body, using: decode.dynamic) + |> result.map_error(fn(_) { "Failed to decode JSON response" }) + + case json_result { + Error(e) -> promise.resolve(Error(e)) + Ok(json_value) -> { + // Decode response + let data_decoder = { + use data <- decode.field("data", decoder) + decode.success(data) + } + + decode.run(json_value, data_decoder) + |> result.map_error(fn(_) { "Failed to decode response data" }) + |> promise.resolve + } + } + }) + } + } } -/// Create a new GraphQL client with bearer token authentication -pub fn new_client_with_auth(endpoint: String, token: String) -> Client { - Client(endpoint: endpoint, headers: [#("Authorization", "Bearer " <> token)]) +/// Execute a GraphQL query on Erlang targets. +/// Returns a Result containing the decoded response. +@target(erlang) +pub fn execute_query( + client: Client, + query: String, + variables: json.Json, + decoder: decode.Decoder(a), +) -> Result(a, String) { + let body = + json.object([#("query", json.string(query)), #("variables", variables)]) + + use req <- result.try( + request.to(client.endpoint) + |> result.map_error(fn(_) { "Invalid endpoint URL" }), + ) + + let req = + req + |> request.set_method(http.Post) + |> request.set_body(json.to_string(body)) + |> request.set_header("content-type", "application/json") + + let req = + list.fold(client.headers, req, fn(r, header) { + request.set_header(r, header.0, header.1) + }) + + use resp <- result.try( + client.send_request(req) + |> result.map_error(fn(_) { "HTTP request failed" }), + ) + + use json_value <- result.try( + json.parse(from: resp.body, using: decode.dynamic) + |> result.map_error(fn(_) { "Failed to decode JSON response" }), + ) + + let data_decoder = { + use data <- decode.field("data", decoder) + decode.success(data) + } + + decode.run(json_value, data_decoder) + |> result.map_error(fn(_) { "Failed to decode response data" }) } pub fn main() { @@ -251,7 +426,7 @@ fn make_graphql_request( |> request.set_header("content-type", "application/json") |> request.set_header("accept", "application/json") - // Send request + // Send request using httpc (generator always runs on Erlang) use resp <- result.try( httpc.send(req) |> result.map_error(fn(_) { "Failed to send HTTP request to " <> endpoint }), diff --git a/src/squall/adapter.gleam b/src/squall/adapter.gleam new file mode 100644 index 0000000..3c03b4a --- /dev/null +++ b/src/squall/adapter.gleam @@ -0,0 +1,17 @@ +import gleam/http/request.{type Request} +import gleam/http/response.{type Response} + +@target(javascript) +import gleam/javascript/promise.{type Promise} + +/// HTTP adapter function type for Erlang target. +/// Synchronously returns a Result with Response or error string. +@target(erlang) +pub type HttpAdapter = + fn(Request(String)) -> Result(Response(String), String) + +/// HTTP adapter function type for JavaScript target. +/// Asynchronously returns a Promise containing a Result with Response or error string. +@target(javascript) +pub type HttpAdapter = + fn(Request(String)) -> Promise(Result(Response(String), String)) diff --git a/src/squall/adapter/erlang.gleam b/src/squall/adapter/erlang.gleam new file mode 100644 index 0000000..df5cfd6 --- /dev/null +++ b/src/squall/adapter/erlang.gleam @@ -0,0 +1,20 @@ +@target(erlang) +import gleam/http/request.{type Request} +@target(erlang) +import gleam/http/response.{type Response} +@target(erlang) +import gleam/httpc +@target(erlang) +import gleam/result +@target(erlang) +import squall/adapter.{type HttpAdapter} + +@target(erlang) +/// Erlang-specific HTTP adapter using gleam_httpc. +/// This adapter works on the Erlang/OTP target. +pub fn adapter() -> HttpAdapter { + fn(req: Request(String)) -> Result(Response(String), String) { + httpc.send(req) + |> result.map_error(fn(_) { "HTTP request failed" }) + } +} diff --git a/src/squall/adapter/javascript.gleam b/src/squall/adapter/javascript.gleam new file mode 100644 index 0000000..423daab --- /dev/null +++ b/src/squall/adapter/javascript.gleam @@ -0,0 +1,24 @@ +@target(javascript) +import gleam/fetch +@target(javascript) +import gleam/http/request +@target(javascript) +import gleam/http/response +@target(javascript) +import gleam/javascript/promise.{type Promise} +@target(javascript) +import gleam/result +@target(javascript) +import squall/adapter + +@target(javascript) +/// JavaScript-specific HTTP adapter using the Fetch API via gleam_fetch. +/// This adapter works on both browser and Node.js JavaScript targets. +/// Returns a Promise that resolves to a Result. +pub fn adapter() -> adapter.HttpAdapter { + fn(req: request.Request(String)) -> Promise(Result(response.Response(String), String)) { + fetch.send(req) + |> promise.try_await(fetch.read_text_body) + |> promise.map(result.map_error(_, fn(_) { "HTTP request failed" })) + } +} diff --git a/src/squall/internal/codegen.gleam b/src/squall/internal/codegen.gleam index 474cef9..10d6928 100644 --- a/src/squall/internal/codegen.gleam +++ b/src/squall/internal/codegen.gleam @@ -90,16 +90,6 @@ fn block(body: List(Document)) -> Document { |> doc.concat } -/// A pretty printed let assignment. -fn let_var(name: String, body: Document) -> Document { - doc.group( - doc.concat([ - doc.from_string("let " <> name <> " ="), - doc.nest(doc.concat([doc.line, body]), by: indent), - ]), - ) -} - /// A pretty printed Gleam string with proper escaping. fn string_doc(content: String) -> Document { let escaped_string = @@ -218,14 +208,10 @@ fn imports_doc( needs_dynamic: Bool, needs_option_constructors: Bool, ) -> Document { + // Minimal imports - just what we need for decoders and the squall client let core_imports = [ "import gleam/dynamic/decode", - "import gleam/http", - "import gleam/http/request", - "import gleam/httpc", "import gleam/json", - "import gleam/list", - "import gleam/result", "import squall", ] @@ -1260,129 +1246,22 @@ fn generate_function( } } - // Build function body - let body_docs = [ - let_var("query", string_doc(query_string)), - let_var("variables", variables_code), - let_var( - "body", - call_doc("json.object", [ - comma_list( - "[", - [ - doc.from_string("#(\"query\", json.string(query))"), - doc.from_string("#(\"variables\", variables)"), - ], - "]", - ), - ]), - ), - doc.concat([ - doc.from_string("use req <- result.try("), - doc.concat([ - doc.line, - call_doc("request.to", [doc.from_string("client.endpoint")]), - doc.line, - doc.from_string( - "|> result.map_error(fn(_) { \"Invalid endpoint URL\" }),", - ), - ]) - |> doc.nest(by: indent), - doc.line, - doc.from_string(")"), - ]), - let_var( - "req", - doc.concat([ - doc.from_string("req"), - doc.line, - doc.from_string("|> request.set_method(http.Post)"), - doc.line, - doc.from_string("|> request.set_body(json.to_string(body))"), - doc.line, - doc.from_string( - "|> request.set_header(\"content-type\", \"application/json\")", - ), - ]), - ), - let_var( - "req", - doc.concat([ - doc.from_string("list.fold(client.headers, req, fn(r, header) {"), - doc.line, - doc.from_string(" request.set_header(r, header.0, header.1)"), - doc.line, - doc.from_string("})"), - ]), - ), - doc.concat([ - doc.from_string("use resp <- result.try("), - doc.concat([ - doc.line, - call_doc("httpc.send", [doc.from_string("req")]), - doc.line, - doc.from_string( - "|> result.map_error(fn(_) { \"HTTP request failed\" }),", - ), - ]) - |> doc.nest(by: indent), - doc.line, - doc.from_string(")"), - ]), - doc.concat([ - doc.from_string("use json_value <- result.try("), - doc.concat([ - doc.line, - call_doc("json.parse", [ - doc.from_string("from: resp.body"), - doc.from_string("using: decode.dynamic"), - ]), - doc.line, - doc.from_string( - "|> result.map_error(fn(_) { \"Failed to decode JSON response\" }),", - ), - ]) - |> doc.nest(by: indent), - doc.line, - doc.from_string(")"), - ]), - doc.concat([ - doc.from_string("let data_and_response_decoder = {"), - doc.line |> doc.nest(by: indent), - doc.concat([ - doc.from_string( - "use data <- decode.field(\"data\", " - <> snake_case(response_type_name) - <> "_decoder())", - ), - doc.line, - doc.from_string("decode.success(data)"), - ]) - |> doc.nest(by: indent), - doc.line, - doc.from_string("}"), - ]), - doc.concat([ - doc.from_string("decode.run(json_value, data_and_response_decoder)"), - doc.line, - doc.from_string( - "|> result.map_error(fn(_) { \"Failed to decode response data\" })", - ), - ]), - ] + // Build simplified function body that just calls squall.execute_query + let body_doc = + call_doc("squall.execute_query", [ + doc.from_string("client"), + string_doc(query_string), + variables_code, + doc.from_string(snake_case(response_type_name) <> "_decoder()"), + ]) // Build function signature - // Don't use call_doc for return type to prevent it from breaking before parameters - let return_type = - doc.from_string("Result(" <> response_type_name <> ", String)") - + // The return type is handled by squall.execute_query (Promise on JS, Result on Erlang) doc.concat([ doc.from_string("pub fn " <> function_name), comma_list("(", param_docs, ")"), - doc.from_string(" -> "), - return_type, doc.from_string(" "), - block(body_docs), + block([body_doc]), ]) }