Skip to content

Commit 673d5b3

Browse files
docs: Restructure docs with generation examples
These improve documentation clarity and provide concrete examples for developers to understand the exact GraphQL-to-Swift mappings. In particular: - Reorganize Design section into separate "Design Philosophy" and "GraphQL to Swift Type Mappings" sections - Add examples showing GraphQL schema definitions alongside their generated Swift code for all type categories - Clarify enum type generation: explicitly state they are Swift enums with String raw values - Add direct links to example projects (HelloWorldServer and StarWars) in Quick Start - Remove prescriptive language about type conversions in favor of objective API documentation - Include implementation examples for Object Types showing protocol flexibility
1 parent 09f9fd0 commit 673d5b3

1 file changed

Lines changed: 215 additions & 35 deletions

File tree

README.md

Lines changed: 215 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
***WARNING***: This package is in beta. It's API is still evolving and is subject to breaking changes.
1+
***WARNING***: This package is in v0.x beta. It's API is still evolving and is subject to breaking changes in minor version bumps.
22

33
# GraphQL Generator for Swift
44

@@ -36,7 +36,9 @@ targets: [
3636

3737
## Quick Start
3838

39-
*Protip*: Take a look at the projects in the `Examples` directory to see real, fully featured examples.
39+
Take a look at the example projects to see real, fully featured implementations:
40+
- [HelloWorldServer](Examples/HelloWorldServer) - Demonstrates all GraphQL type mappings with a comprehensive schema
41+
- [StarWars](Examples/StarWars) - A production-like example using the SWAPI with DataLoader for caching
4042

4143
### 1. Create a GraphQL Schema
4244

@@ -82,7 +84,7 @@ struct Resolvers: GraphQLGenerated.Resolvers {
8284
}
8385
```
8486

85-
As you build the `Query`, `Mutation`, and `Subscription` types and their resolution logic, you will be forced to define a concrete type for every reachable GraphQL result, according to its generated protocol:
87+
As you build the `Query`, `Mutation`, and `Subscription` types and their resolution logic, you will be forced to define a concrete type for every reachable GraphQL type, according to its generated protocol:
8688

8789
```swift
8890
struct Query: GraphQLGenerated.Query {
@@ -126,62 +128,243 @@ let result = try await graphql(schema: schema, request: "{ users { name email }
126128
print(result)
127129
```
128130

129-
## Design
131+
## Design Philosophy
130132

131-
All generated types other than `GraphQLContext` and scalar types are namespaced inside of `GraphQLGenerated` to minimize polluting the inheriting package's type namespace.
133+
This generator is designed with the following guiding principles:
132134

133-
### Root Types
134-
GraphQL root types (Query, Mutation, and Subscription) are modeled as Swift protocols with static method for each GraphQL field. The user must implement these types and provide them to the `buildGraphQLSchema` function via the `Resolvers` typealiases.
135+
- **Protocol-based flexibility**: GraphQL types are generated as Swift protocols (except where concrete types are needed), allowing you to implement backing types however you want - structs, actors, classes, or any combination.
136+
- **Explicit over implicit**: No default resolvers based on reflection. While more verbose, this provides better performance and clearer schema evolution handling.
137+
- **Type safety**: Leverage Swift's type system to ensure compile-time conformance with your GraphQL schema.
138+
- **Namespace isolation**: All generated types (except `GraphQLContext` and custom scalars) are namespaced inside `GraphQLGenerated` to avoid polluting your package's type namespace.
139+
140+
## GraphQL to Swift Type Mappings
141+
142+
This section describes how each GraphQL type is converted to Swift code, with concrete examples from the [HelloWorldServer](Examples/HelloWorldServer) example. Note that all generated types are namespaced inside `GraphQLGenerated`
143+
144+
### Root Types (Query, Mutation, Subscription)
145+
146+
GraphQL root types are generated as Swift protocols with static methods for each field.
147+
148+
**GraphQL:**
149+
```graphql
150+
type Query {
151+
user(id: ID!): User
152+
users: [User!]!
153+
}
154+
155+
type Mutation {
156+
upsertUser(userInfo: UserInfo!): User!
157+
}
158+
159+
type Subscription {
160+
watchUser(id: ID!): User
161+
}
162+
```
163+
164+
**Generated Swift:**
165+
```swift
166+
protocol Query: Sendable {
167+
static func user(id: String, context: GraphQLContext, info: GraphQLResolveInfo) async throws -> (any User)?
168+
static func users(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> [any User]
169+
}
170+
171+
protocol Mutation: Sendable {
172+
static func upsertUser(userInfo: UserInfo, context: GraphQLContext, info: GraphQLResolveInfo) async throws -> any User
173+
}
174+
175+
protocol Subscription: Sendable {
176+
static func watchUser(id: String, context: GraphQLContext, info: GraphQLResolveInfo) async throws -> AnyAsyncSequence<(any User)?>
177+
}
178+
```
135179

136180
### Object Types
137-
GraphQL object types are modeled as Swift protocols with a method for each GraphQL field. This allows the Swift implementation to be very flexible. Internally, GraphQL passes result objects directly through to subsequent resolvers. By only specifying the interface, we allow the backing types to be incredibly dynamic - they can be simple codable structs or complex stateful actors, reference or values types, or any other type configuration, as long as they conform to the generated protocol.
138181

139-
Furthermore, by only referencing protocols, we can have multiple Swift types back a particular GraphQL type, and can easily mock portions of the schema. As an example, consider the following schema snippet:
182+
GraphQL object types are generated as Swift protocols with instance methods for each field. This allows for flexible implementations - you can use structs, actors, classes, or any other type that conforms to the protocol.
183+
184+
**GraphQL:**
140185
```graphql
141-
type A {
142-
foo: String
186+
type User {
187+
id: ID!
188+
name: String!
189+
email: EmailAddress!
190+
age: Int
143191
}
144192
```
145193

146-
This would result in the following generated protocol:
194+
**Generated Swift:**
147195
```swift
148-
protocol A: Sendable {
149-
func foo(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String
196+
protocol User: Sendable {
197+
func id(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String
198+
func name(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String
199+
func email(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> GraphQLScalars.EmailAddress
200+
func age(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> Int?
150201
}
151202
```
152203

153-
You could define two conforming types. To use `ATest` in tests, simply return it from the relevant resolvers.
204+
**Example Implementation:**
154205
```swift
155-
struct A: GraphQLGenerated.A {
156-
let foo: String
157-
func foo(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String {
158-
return foo
206+
struct User: GraphQLGenerated.User {
207+
let id: String
208+
let name: String
209+
let emailAddress: String
210+
let age: Int?
211+
212+
func id(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String {
213+
return id
159214
}
160-
}
161-
struct ATest: GraphQLGenerated.A {
162-
func foo(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String {
163-
return "test"
215+
func name(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String {
216+
return name
217+
}
218+
func email(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> GraphQLScalars.EmailAddress {
219+
return .init(email: emailAddress)
220+
}
221+
func age(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> Int? {
222+
return age
164223
}
165224
}
166225
```
167226

168-
This package does not provide default resolvers based on reflection of the type's properties. While this can cause the conformance code to be more verbose, it was chosen to improve performance and better handle schema evolution.
227+
Because these are protocols, you can have multiple implementations of the same GraphQL type (useful for testing or different data sources):
228+
229+
```swift
230+
struct MockUser: GraphQLGenerated.User {
231+
func id(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String { "test-id" }
232+
func name(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String { "Test User" }
233+
func email(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> GraphQLScalars.EmailAddress {
234+
.init(email: "test@example.com")
235+
}
236+
func age(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> Int? { nil }
237+
}
238+
```
169239

170240
### Interface Types
171-
GraphQL interfaces are modeled as a Swift protocol with required methods for each GraphQL field. Implementing objects and interfaces are marked as requiring conformance to the interface protocol.
241+
242+
GraphQL interfaces are generated as Swift protocols with required methods for each field. Types implementing the interface will have their protocol marked as conforming to the interface protocol.
243+
244+
**GraphQL:**
245+
```graphql
246+
interface HasEmail {
247+
email: EmailAddress!
248+
}
249+
250+
type User implements HasEmail {
251+
id: ID!
252+
name: String!
253+
email: EmailAddress!
254+
}
255+
```
256+
257+
**Generated Swift:**
258+
```swift
259+
protocol HasEmail: Sendable {
260+
func email(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> GraphQLScalars.EmailAddress
261+
}
262+
263+
protocol User: HasEmail, Sendable {
264+
func id(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String
265+
func name(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String
266+
func email(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> GraphQLScalars.EmailAddress
267+
}
268+
```
172269

173270
### Union Types
174-
GraphQL union types are modeled as a Swift marker protocol, with no required properties or functions. The members of the union have their generated Swift protocol marked as conforming to the to the union protocol.
271+
272+
GraphQL union types are generated as Swift marker protocols with no required properties or methods. Union member types have their protocols marked as conforming to the union protocol.
273+
274+
**GraphQL:**
275+
```graphql
276+
union UserOrPost = User | Post
277+
278+
type User {
279+
id: ID!
280+
name: String!
281+
}
282+
283+
type Post {
284+
id: ID!
285+
title: String!
286+
}
287+
```
288+
289+
**Generated Swift:**
290+
```swift
291+
protocol UserOrPost: Sendable {}
292+
293+
protocol User: UserOrPost, Sendable {
294+
func id(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String
295+
func name(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String
296+
}
297+
298+
protocol Post: UserOrPost, Sendable {
299+
func id(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String
300+
func title(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String
301+
}
302+
```
175303

176304
### Input Object Types
177-
GraphQL input object types are modeled as a concrete Swift struct with a property for each of the GraphQL fields.
305+
306+
GraphQL input object types are generated as concrete Swift structs with properties for each field. These are `Codable` and `Sendable`.
307+
308+
**GraphQL:**
309+
```graphql
310+
input UserInfo {
311+
id: ID!
312+
name: String!
313+
email: EmailAddress!
314+
age: Int
315+
role: Role = USER
316+
}
317+
```
318+
319+
**Generated Swift:**
320+
```swift
321+
struct UserInfo: Codable, Sendable {
322+
let id: String
323+
let name: String
324+
let email: GraphQLScalars.EmailAddress
325+
let age: Int?
326+
let role: Role?
327+
}
328+
```
178329

179330
### Enum Types
180-
GraphQL enum types are modeled as a concrete Swift enum with a string case for each the GraphQL cases.
331+
332+
GraphQL enum types are generated as concrete Swift enums with raw `String` values. Each GraphQL enum case becomes a Swift enum case with its raw value matching the GraphQL case name.
333+
334+
**GraphQL:**
335+
```graphql
336+
enum Role {
337+
ADMIN
338+
USER
339+
GUEST
340+
}
341+
```
342+
343+
**Generated Swift:**
344+
```swift
345+
enum Role: String, Codable, Sendable {
346+
case admin = "ADMIN"
347+
case user = "USER"
348+
case guest = "GUEST"
349+
}
350+
```
351+
352+
These generated enums can be used directly in your code without any additional implementation.
181353

182354
### Scalar Types
183-
GraphQL scalar types are not modeled by the generator. They are simply referenced as `GraphQLScalars.<name>`, and you are expected to define the type and conform it to `GraphQLScalar`. Since GraphQL uses a different serialization system than Swift, you should be sure that the type's conformance to Swift's `Codable` and GraphQL's `GraphQLScalar` agree on a representation. Here is an example that represents an email address as a raw String:
184355

356+
GraphQL scalar types are not generated by the plugin. Instead, they are referenced as `GraphQLScalars.<name>`, and you must define the type and conform it to `GraphQLScalar`.
357+
358+
**GraphQL:**
359+
```graphql
360+
scalar EmailAddress
361+
362+
type User {
363+
email: EmailAddress!
364+
}
365+
```
366+
367+
**Required Implementation:**
185368
```swift
186369
extension GraphQLScalars {
187370
struct EmailAddress: GraphQLScalar {
@@ -191,15 +374,15 @@ extension GraphQLScalars {
191374
self.email = email
192375
}
193376

194-
// Codability conformance. Represent simply as `email` string.
377+
// Codable conformance - for Swift serialization
195378
init(from decoder: any Decoder) throws {
196379
self.email = try decoder.singleValueContainer().decode(String.self)
197380
}
198381
func encode(to encoder: any Encoder) throws {
199382
try self.email.encode(to: encoder)
200383
}
201384

202-
// Scalar conformance. Parse & serialize simply as `email` string.
385+
// GraphQLScalar conformance - for GraphQL serialization
203386
static func serialize(this: Self) throws -> Map {
204387
return .string(this.email)
205388
}
@@ -224,7 +407,4 @@ extension GraphQLScalars {
224407
}
225408
```
226409

227-
## Development Roadmap
228-
229-
1. Add Directive support
230-
2. Add configuration to reference different `.graphql` source file locations.
410+
Ensure that your `Codable` and `GraphQLScalar` conformances agree on the same representation format.

0 commit comments

Comments
 (0)