Schema and Codegen
You're reading Part One of the Create Wraps tutorial, where we learn everything you need to know to productively develop Polywrap wraps.
Every wrap, plugin, and "interface wrap" has a polywrap.graphql
schema that defines its public interface. The schema is parsed and composed into a wrap.info
ABI file that is included in the wrap package along with the wrap.wasm
bytecode module.
Polywrap uses the schema to generate bindings for your wrap. The bindings handle serialization logic--so that data can pass between the Wasm module and the Polywrap Client--and provide a lightweight structure to validate the wrap's content.
Wrap schemas use a simplified version of GraphQL schema syntax. We provide detailed reference documentation in Wrap Schema, but you'll learn most of it just by following along in this guide.
Since the schema's primary purpose is to communicate which code bindings should be generated, we'll learn a bit about the generated bindings as well. The generated bindings are what makes wraps special, so it's important to understand how they work.
In the next section, we'll use what we've learned to write the first method of "Oracle Wrap".
Initial Schema
First, let's take a look at the polywrap.graphql
schema we generated when we initialized our project with the Polywrap CLI. It's located in the project root.
type Module {
sampleMethod(arg: String!): SampleResult!
}
type SampleResult {
result: String!
}
We have a Module
and a custom type named SampleResult
.
Every wrap has exactly one declaration of type Module
that defines the wrap's methods. Without a module, there would be nothing to invoke. If we want two or more modules, we can simply create more wraps and our wrap can invoke them. When one wrap invokes another, we call it a "subinvocation".
The sample module has one method, sampleMethod(arg: String!): SampleResult!
, that accepts a single argument named arg
of type String!
and returns a SampleResult!
.
Following GraphQL syntax, a !
means a type cannot be null
. The absence of a !
would mean a property or argument is optional, and it's okay for users to pass in null
. Also, primitive types start with capital letters.
We'll use this schema to generate code bindings and see how they are used.
Codegen
To generate the code bindings, change your current working directory to the project root and run polywrap codegen
using the Polywrap CLI. A new directory called wrap
will be generated next to your module entry file.
- Rust
- Go
- TypeScript
- AssemblyScript
src/
├── lib.rs # Entry point; exports module defined in schema
└── wrap # Generated types
module/
├── module.go # Entry point; exports module defined in schema
├── __tests__/ # Integration tests
└── wrap # Generated types
src/
├── index.ts # Entry point; exports module defined in schema
├── __tests__/ # Integration tests
└── wrap # Generated types
src/
├── index.ts # Entry point; exports module defined in schema
├── __tests__/ # Integration tests
└── wrap # Generated types
By default, codegen is automatically run before you build your project with polywrap build
.
Wrap Directory
The bindings contain generated types that we must use to implement the wrap module, as well as additional logic related to serialization and interaction with the Polywrap Client.
- Rust
- Go
- TypeScript
- AssemblyScript
wrap/
├── module/
│ ├── mod.rs
│ ├── module.rs # Module Trait
│ └── wrapped.rs # Args types and method wrappers
├── sample_result/
│ └── mod.rs # Custom type implementations
├── mod.rs
├── entry.rs # Wrap entry file
└── prelude.rs
wrap/
├── main/
│ └── main.go # Wrap entry file
├── module_wrapped/
│ ├── module_serialization.go # Module serialization logic
│ └── module_wrapped.go # Method wrappers
├── types/
│ ├── module_args.go # Module interface and Args types
│ ├── object_sample_result.go # Custom type implementations
│ └── object_sample_result_serialization.go # Custom type serialization logic
└── wrap.go
wrap/
├── common.ts # Core types available in every wrap
├── entry.ts # Wrap entry file
├── globals.d.ts # Wrap binding method declarations
├── index.ts
├── module.ts # Module base class and Args types
└── types.ts # Custom type implementations
wrap/
├── Module/
│ ├── index.ts
│ ├── module.ts # Module base class
│ ├── serialization.ts # Args types and Module serialization logic
│ └── wrapped.ts # Method wrappers
├── SampleResult/
│ ├── index.ts # Custom type implementations
│ └── serialization.ts # Custom type serialization logic
├── entry.ts # Wrap entry file
└── index.ts
We're going to keep it simple and discuss only the code snippets you need to be aware of:
- Custom type implementations
- Argument types generated for each module method
- The Module Base (an abstract class, interface, or trait)
Once we've seen the generated wrap bindings, we won't need to look at them again. The wrap schema tells you everything you need to know about the wrap directory's contents.
Custom Type
- Rust
- Go
- TypeScript
- AssemblyScript
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SampleResult {
pub result: String,
}
impl SampleResult {
pub fn new() -> SampleResult {
SampleResult {
result: String::new(),
}
}
}
type SampleResult struct {
Result string `json:"result"`
}
export class SampleResult {
result: string;
}
export class SampleResult {
result: string;
// static methods for serialization
...
}
A class (or struct) has been generated for the SampleResult
type, mirroring its schema definition.
Codegen generates custom types that mirror the custom types defined in the schema. Although we won't review it, the Polywrap CLI also generates serialization logic for each custom type.
Args
- Rust
- Go
- TypeScript
- AssemblyScript
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ArgsSampleMethod {
pub arg: String,
}
type ArgsSampleMethod struct {
Arg string `json:"arg"`
}
export class Args_sampleMethod {
arg: string;
}
export class Args_sampleMethod {
arg: string;
}
Likewise, a class (or struct) named ArgsSampleMethod
(or Args_sampleMethod
) has been generated to hold the arguments of the sampleMethod
method defined in the schema.
Codegen generates argument types for each method defined in the schema. A method's "Args" type must be used to invoke the method. As with custom types, serialization logic is generated for each Args type.
Why do we use an Args type instead of passing method arguments directly?
Recall that when a Polywrap Client user invokes your wrap, the user passes arguments from the Client's host language (e.g. JavaScript) to the Wasm module. The arguments are serialized before being passed into the Wasm module. The Args type simplifies deserialization within the Wasm module. And if the invoked method performs a subinvocation--i.e. invokes a different wrap--the Args type passed into the subinvocation is serialized and passed to the subinvoked wrap.
Module Base
- Rust
- Go
- TypeScript
- AssemblyScript
pub struct Module;
pub trait ModuleTrait {
fn sample_method(args: ArgsSampleMethod) -> Result<SampleResult, String>;
}
type Module interface {
SampleMethod(args *ArgsSampleMethod) SampleResult
}
export abstract class ModuleBase {
abstract sampleMethod(
args: Args_sampleMethod
): Types.SampleResult;
}
export abstract class ModuleBase {
abstract sampleMethod(
args: Types.Args_sampleMethod
): Types.SampleResult;
}
The Module Base is an abstract class or interface that must be implemented in your module entry file. For each method defined in the schema, the module base contains an abstract method with a matching signature.
The Module Base helps wrap developers ensure their module implementation is correct by validating the module implementation at compile time. It is used by the wrap bindings to call the module's methods when a Polywrap Client invokes the wrap.
Initial Module
From this point forward, we won't need to look at the generated code bindings again. We can instead focus on the schema and the module entry file, which is where we'll write our code.
The module entry file already contains an implementation of the Module Base, which mirrors the Module
defined in the schema. It imports the generated ArgsSampleMethod
(or Args_sampleMethod
) and SampleResult
types, and uses them in to implement the sampleMethod
method.
- Rust
- Go
- TypeScript
- AssemblyScript
pub mod wrap;
pub use wrap::prelude::*;
impl ModuleTrait for Module {
fn sample_method(args: ArgsSampleMethod) -> Result<SampleResult, String> {
return Ok(SampleResult {
result: format!("{} from sample_method", args.arg),
});
}
}
package module
import (
"example.com/template-wasm-go/module/wrap/types"
)
func SampleMethod(args *types.ArgsSampleMethod) types.SampleResult {
return types.SampleResult{
Result: args.Arg,
}
}
import { Args_sampleMethod, SampleResult, ModuleBase } from "./wrap";
export class Module extends ModuleBase {
sampleMethod(args: Args_sampleMethod): SampleResult {
return {
result: args.arg,
};
}
}
import { Args_sampleMethod, SampleResult, ModuleBase } from "./wrap";
export class Module extends ModuleBase {
sampleMethod(args: Args_sampleMethod): SampleResult {
return {
result: args.arg,
};
}
}
Next Steps
In the next section, we'll finally get to write some code. We'll implement the first method of "Oracle Wrap" and build our project with the Polywrap CLI.