Skip to content

Commit

Permalink
Merge pull request #1027 from apollographql/sb/codegen-edits
Browse files Browse the repository at this point in the history
Edits to Swift scripting article
  • Loading branch information
designatednerd authored Feb 19, 2020
2 parents eb9625d + dd7b83c commit d0cdf75
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 62 deletions.
3 changes: 2 additions & 1 deletion docs/gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ module.exports = {
'mutations',
'fragments',
'caching',
'subscriptions'
'subscriptions',
'swift-scripting'
]
}
}
Expand Down
126 changes: 65 additions & 61 deletions docs/source/swift-scripting.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
---
title: Swift Scripting
title: Swift scripting
sidebar_title: Swift scripting (beta)
---

⚠️ **PLEASE NOTE: THIS FUNCTIONALITY IS IN BETA** ⚠️
⚠️ **This functionality is in beta.** ⚠️

Some functions you used to have to call using Bash have been adapted to allow the use of Swift scripting with Swift Package Manager executables.

This document will guide you through setting up your executable and then using it to:
Apollo Client for iOS enables you to use Swift scripting to perform certain operations that otherwise require the command line. This document guides you through setting up a Swift Package Manager executable and then using it to:

- Download a schema
- Generate Swift code for your model object based on your schema + operations
- Generate Swift code for your model object based on your schema and operations

## Setting up a Swift Package Manager executable
## Setup

To begin, you need to set up a Swift Package Manager executable.
To begin, let's set up a Swift Package Manager executable:

1. Using Terminal, `cd` into your project's `SRCROOT`. This is generally the folder containing your `.xcodeproj` or `.xcworkspace` file.
2. Create a new folder for the Codegen executable, change directories into the folder, then initialize an SPM executable using the following commands:
1. Using Terminal, `cd` into your project's `SRCROOT`. This is generally the directory that contains your `.xcodeproj` or `.xcworkspace` file.
2. Create a new directory for the Codegen executable, `cd` into it, and initialize an SPM executable using the following commands:

```
mkdir Codegen
cd Codegen
swift package init --type executable
```
3. Double click on the `Package.swift` in this new folder (or run `open Package.swift` in Terminal). This will open the package you've just created in Xcode.
3. Double-click `Package.swift` in this new folder (or run `open Package.swift` in Terminal). This opens the package you've just created in Xcode.
4. Update the `dependencies` section to grab the Apollo iOS library:
Expand Down Expand Up @@ -55,11 +54,11 @@ Now it's time to use the executable to do some stuff for you!
Because Swift Package manager doesn't have an environment, there's no good way to access the `$SRCROOT` variable if you're running it directly from the command line or using a scheme in Xcode.
Since almost everything the code generation can do requires access to the file tree where your code lives, there needs to be an alternate method to pass this through.
Because almost everything the code generation can do requires access to the file tree where your code lives, there needs to be an alternate method to pass this through.
Fortunately, there's a class for that: `FileFinder` will automatically use the calling `#file` as a way to access the swift file you're currently editing.
Fortunately, there's a class for that: `FileFinder` automatically uses the calling `#file` as a way to access the Swift file you're currently editing.
For example, let's take a `main.swift` in a folder in `apollo-ios/Codegen/Sources`, assuming `apollo-ios` is the source root. Here's how you'd grab the parent folder of the script, then use that to get back to your source root:
For example, let's take a `main.swift` in a folder in `apollo-ios/Codegen/Sources`, assuming `apollo-ios` is the source root. Here's how you obtain the parent folder of the script, then use that to get back to your source root:
```swift:title=main.swift
let parentFolderOfScriptFile = FileFinder.findParentFolder()
Expand All @@ -69,7 +68,7 @@ let sourceRootURL = parentFolderOfScriptFile
.deletingLastPathComponent() // apollo-ios
```

Then, you can use this to get the URL of the folder you plan to download the CLI to:
You can use this to get the URL of the folder you plan to download the CLI to:

```swift:title=main.swift
let cliFolderURL = sourceRootURL
Expand All @@ -83,29 +82,29 @@ Now, with access to both the `sourceRootURL` and the `cliFolderURL`, it's time t

One of the convenience wrappers available to you in the target is `ApolloSchemaDownloader`. This allows you to use an `ApolloSchemaOptions` object to set up how you would like to download the schema.

1. Set up access to the endpoint you'll be downloading this from. This may be directly from your server or from [Apollo Graph Manager](https://engine.apollographql.com), but for this example, let's download directly from the server:
1. Set up access to the endpoint you'll be downloading this from. This might be directly from your server, or from [Apollo Graph Manager](https://engine.apollographql.com). For this example, let's download directly from the server:

```swift:title=main.swift
let endpoint = URL(string: "http://localhost:8080/graphql")!
```

2. Set up the URL for the folder where you would like the schema to be downloaded:
2. Set up the URL for the folder where you want to download the schema:

```swift:title=main.swift
let output = sourceRootURL
.appendingPathComponent("Sources")
.appendingPathComponent("MyTarget")
```

You may want to make sure the folder exists before proceeding:
You might want to make sure the folder exists before proceeding:

```swift:title=main.swift
try FileManager
.default
.apollo_createFolderIfNeeded(at: output)
```

3. Set up your `ApolloSchemaOptions` object. In this case, we'll use the [default arguments for all the constructor parameters which take them](./api/ApolloCodegenLib/structs/ApolloSchemaOptions#methods), and only pass in the endpoint to download from and the folder to put the downloaded file into:
3. Set up your `ApolloSchemaOptions` object. In this case, we'll use the [default arguments for all the constructor parameters that take them](./api/ApolloCodegenLib/structs/ApolloSchemaOptions#methods), and only pass in the endpoint to download from and the folder to put the downloaded file into:

```swift:title=main.swift
let options = ApolloSchemaOptions(endpointURL: endpoint,
Expand All @@ -124,87 +123,92 @@ One of the convenience wrappers available to you in the target is `ApolloSchemaD
exit(1)
}
```
Note that `catch`'ing and manually calling `exit` with a non-zero code leaves you with a much more legible error message than simply letting the method throw.
Note that `catch`ing and manually calling `exit` with a non-zero code leaves you with a much more legible error message than simply letting the method throw.

5. Build and run using the Xcode project. Note that if you're on Catalina you may get a warning asking if your executable can access files in a particular folder like this:
5. Build and run using the Xcode project. Note that if you're on Catalina you might get a warning asking if your executable can access files in a particular folder like this:

![permission prompt](screenshot/would_like_to_access.png)

Click the "OK" button. Your CLI output will look something like this:
Click **OK**. Your CLI output will look something like this:

![log barf for successful run](screenshot/schema_download_success.png)
![log output for successful run](screenshot/schema_download_success.png)

Those last two lines - "Saving schema started" and "Saving schema completed" indicate that the schema has successfully downloaded.
The last two lines (`Saving schema started` and `Saving schema completed`) indicate that the schema has successfully downloaded.

Note the warning: This isn't relevant for schema downloading, but it *is* relevant for generating code: In order to generate code, you need both the schema and some kind of operation. Now that you've got the schema, it's time to
Note the warning: This isn't relevant for schema downloading, but it *is* relevant for generating code: In order to generate code, you need both the schema and some kind of operation.

## Codegen theory + creating a `.graphql` file with an operation
## Using codegen to create a `.graphql` file with an operation

Code generation takes a combination of the **schema**, which defines what it's *possible* for you to request from or send to your server, and your **operations**, which define what you are *actually* requesting from the server.
Code generation requires both of the following to run:

An operation can be one of three things:
* Your **schema**, which defines what it's *possible* for you to request from or send to your server
* One or more **operations**, which define what you are *actually* requesting from the server

- A **query**, which is a one-time request for specific data
- A **mutation**, which changes data on the server and then receives updated data back
- A **subscription**, which allows you to listen for changes to a particular object or type of object
If you're missing either of these, codegen can't run. If you define operations but no schema, the operations can't be validated. If you define a schema but no operations, there's nothing to validate or generate code for.

Or, more succinctly:

The code generation takes your operations and compares them to the schema to validate that what you are asking for is, in fact, possible. If it's not possible, the whole process errors out. If it is possible, it generates Swift code that gives you end-to-end type safety for each operation.
```
schema + operations = code
```

Thus, a simple equation can be used to describe generating code:
Each operation you define can be one of the following:

`schema + operations = code`
- A **query**, which is a one-time request for specific data
- A **mutation**, which changes data on the server and then receives updated data back
- A **subscription**, which allows you to listen for changes to a particular object or type of object

If you're missing either of the first two parts, the code can't be generated. If there's operations but no schema, the operations can't be validated, so we can't know if code should be generated. If there's a schema but no operations, then there's nothing to validate or generate code for.
Code generation takes your operations and compares them to the schema to confirm that they're valid. If an operation _isn't_ valid, the whole process errors out. If all operations are valid, codegen generates Swift code that gives you end-to-end type safety for each operation.

Since you've already [downloaded a schema](#downloading-a-schema), you can now proceed to creating an operation. The easiest and most common type of operation to create is a Query.
Because you've already [downloaded a schema](#downloading-a-schema), you can now proceed to creating an operation. The easiest and most common type of operation to create is a Query.

Identify where your server's [GraphiQL](https://github.com/graphql/graphiql) instance lives. GraphiQL is a simple web interface for interacting with and testing out a GraphQL server. This can generally be accessed by going to the same URL as your GraphQL endpoint in a web browser, but you may need to talk to your backend team if they've got it in a different place.
Identify where your server's [GraphiQL](https://github.com/graphql/graphiql) instance lives. GraphiQL is a helpful web interface for interacting with and testing out a GraphQL server. This can generally be accessed by going to the same URL as your GraphQL endpoint in a web browser, but you might need to talk to your backend team if they host it in a different place.

You'll see something that looks like this:

![GraphiQL Empty](screenshot/graphiql_empty.png)
<img alt="GraphiQL empty" src="screenshot/graphiql_empty.png" class="screenshot"/>

In the "Docs" tab on the right hand side, you should be able to access a list of the various queries you can make to your server:
In the "Docs" tab on the right-hand side, you should be able to access a list of the various queries you can make to your server:

![docs tab](screenshot/graphiql_docs_tab.png)
<img alt="Docs tab" src="screenshot/graphiql_docs_tab.png" class="screenshot"/>

You can then type out a GraphQL query on the left hand side and have it give you auto-completion for your queries and the properties you can ask for on the returned data. Clicking the play button will execute the query, so you can validate that the query works:
You can then type out a GraphQL query on the left-hand side and have it give you auto-completion for your queries and the properties you can ask for on the returned data. Clicking the play button will execute the query, so you can validate that the query works:

![completed query](screenshot/graphiql_query.png)
<img alt="Completed query" src="screenshot/graphiql_query.png" class="screenshot"/>

You can then create a new empty file in your Xcode project, give it the same name as your query and have the file end in `.graphql`, and paste in the query. Here, for example, is what this looks like in a file for one of the queries in our [tutorial application](./tutorial/tutorial-introduction):
Now you can create a new empty `.graphql` file in your Xcode project, give it the same name as your query, and paste in the query. Here, for example, is what this looks like in a file for one of the queries in our [tutorial application](./tutorial/tutorial-introduction):

![launch list file](screenshot/graphql_file_launchlist.png)
<img alt="Launch list file" src="screenshot/graphql_file_launchlist.png" class="screenshot"/>

>**NOTE** It's generally a good idea to put your query file in the filesystem somewhere above your `SRCROOT`, otherwise you'll need to manually pass the URL of your GraphQL files to your code generation step.
>**Note:** It's generally a good idea to put your query file in the filesystem somewhere above your `SRCROOT`. Otherwise, you'll need to manually pass the URL of your GraphQL files to your code generation step.

## Generating code for a target

>**BEFORE YOU START**: Remember, you need to have a locally downloaded copy of your schema and at least one `.graphql` file containing an operation in your file tree. If you don't have **both** of these, code generation will fail. Read the section above if you don't have an operation set up!
>**Before you start**: Remember, you need to have a locally downloaded copy of your schema and at least one `.graphql` file containing an operation in your file tree. If you don't have **both** of these, code generation will fail. [Read the section above](#using-codegen-to-create-a-graphql-file-with-an-operation) if you don't have an operation set up!

1. Set up the URL for the folder where the root of your target that you wish to generate code for is:
1. Specify the URL for the root of the target you're generating code for:

```swift:title=main.swift
let targetURL = sourceRootURL
.appendingPathComponent("Sources")
.appendingPathComponent("MyTarget")
```

Again, you may want to make sure the folder exists before proceeding:
Again, you might want to make sure the folder exists before proceeding:

```swift:title=main.swift
try FileManager
.default
.apollo_createFolderIfNeeded(at: targetURL)
```

2. Set up your `ApolloCodegenOptions` object. In this case, we'll use the constructor that [sets a bunch of defaults for you automatically](./api/ApolloCodegenLib/structs/ApolloCodegenOptions#methods):
2. Set up your `ApolloCodegenOptions` object. In this case, we'll use the constructor that [sets defaults for you automatically](./api/ApolloCodegenLib/structs/ApolloCodegenOptions#methods):

```swift:title=main.swift
let options = ApolloCodegenOptions(targetRootURL: targetRootURL)
```

This will create a single file called `API.swift` in the target's root folder.
This creates a single file called `API.swift` in the target's root folder.

3. Add the code to run code generation:

Expand All @@ -218,23 +222,23 @@ You can then create a new empty file in your Xcode project, give it the same nam
}
```

Note that again, `catch`'ing and manually calling `exit` with a non-zero code leaves you with a much more legible error message than simply letting the method throw.
Note that again, `catch`ing and manually calling `exit` with a non-zero code leaves you with a much more legible error message than simply letting the method throw.

4. Build and run using the Xcode project. Note that if you're on Catalina you may get a warning asking if your executable can access files in a particular folder like this:
4. Build and run using the Xcode project. Note that if you're on Catalina you might get a warning asking if your executable can access files in a particular folder like this:

![permission prompt](screenshot/would_like_to_access.png)

Click the "OK" button. Your CLI output will look something like this:
Click **OK**. Your CLI output will look something like this:

![log barf for successful run](screenshot/codegen_success.png)
![log output for successful run](screenshot/codegen_success.png)

The final lines about loading the Apollo project and generating query files are what indicate your code has been generated successfully.
The final lines about loading the Apollo project and generating query files indicate that your code has been generated successfully.

Now, you're able to generate code from a debuggable Swift Package Manager executable. All that's left to do is set it up to run from your Xcode project!

## Running Your Executable From Your Main Project
## Running your executable from your main project

1. Select the target in your project or workspace you want to have run the code generation, and go to the `Build Phases` tab.
1. Select the target in your project or workspace you want to run code generation, and go to the `Build Phases` tab.

2. Create a new Run Script Build Phase by selecting the **+** button in the upper left-hand corner:

Expand All @@ -247,12 +251,12 @@ Now, you're able to generate code from a debuggable Swift Package Manager execut
swift run
```

>**NOTE**: If your package ever seems to have problems with caching, run `swift package clean` before `swift run` for a totally clean build. It is not recommended to do this by default since it substantially increases build time.
>**Note**: If your package ever seems to have problems with caching, run `swift package clean` before `swift run` for a totally clean build. It is not recommended to do this by default, because it substantially increases build time.

4. Build your target. Since `swift run` is being called from within your target, all of the pieces of the environment, including `$SRCROOT`, will automatically be passed to the environment of the executable, and you don't have to worry about passing anything manually.
4. Build your target.

Now, every time you build your project, this script will get called. Since Swift knows not to recompile everything unless something's changed, it should not have a significant impact on your build time.
Now, every time you build your project, this script gets called. Because Swift knows not to recompile everything unless something's changed, it should not have a significant impact on your build time.

## Swift-specific troubleshooting

- If at any point you start seeing a bunch of errors around `SecTaskLoadEntitlements` resulting in an immediate exit of the script rather than showing the permission prompt, validate that all the folders you're looking for exist and are at the *exact* path you think they are. You may have a typo in one of your paths.
If you encounter errors around `SecTaskLoadEntitlements` that result in an immediate exit of the script instead of showing the permission prompt, verify that all the folders you're looking for exist at the correct path. This error is often caused by a typo.

0 comments on commit d0cdf75

Please sign in to comment.