Skip to main content

Wrap Schema

Every wrap project has a schema. The schema defines the wrap's dependencies, methods, and custom types. In short, it's an interface describing how to use the wrap.

The schema is a sort of contract between a wrap's developers and its users. A Wasm wrap will not compile unless it implements and exports all of the methods declared in the schema. A Wasm wrap's users can be certain that all of the methods declared in a schema are callable using the declared method signatures. Plugin wraps do not have this same guarantee. However, the client will throw an exception if a user attempts to invoke a method and the method is not found.

Declaration

A wrap project's schema must be declared in the project manifest (Polywrap Manifest or Plugin Manifest).

source:
module: ./src/index.ts
schema: ./src/schema.graphql

Codegen

The wrap schema tells the Polywrap compiler which types and serialization logic to generate. Generated types are updated every time you build the wrap. The Polywrap CLI also provides a codegen command to quickly update the generated wrap folder where generated types are stored.

The Polywrap compiler generates types for method arguments and custom schema types. Wrap developers must use the generated method arguments for methods declared in the schema, and generated types for method return values when the return value is of a custom type. The generated types come with serialization logic that allows Wraps to communicate with the client.

The following example demonstrates this practice.

type Module {
sampleMethod(arg: String!): SampleResult!
}

type SampleResult {
result: String!
}

Language Syntax

Wrap schemas are written in Polywrap's variant of the GraphQL schema definition language (SDL). Polywrap's GraphQL variant is simpler than standard GraphQL and easy to learn.

Custom Types

Custom types are declared with the type keyword. They can be given any number of properties. Properties can be of any supported type, including other custom types.

type CustomType {
myString: String!
myObject: AnotherType!
}

type AnotherType {
myInt: Int!
}

Nullability

As in standard GraphQL, properties, method arguments, and method return values are declared non-nullable by appending a ! at the end of the type name. The absence of a ! indicates that a value is nullable.

type CustomType {
nullable: String # can be String or null
nonNullable: String! # must be String
}

Methods

Methods are declared within the Module type. Methods must return a value.

type Module {
foo(
arg: CustomType!
bar: CustomType
): ReturnType!
}

Custom types and modules can be imported from other wraps by placing an import statement at the top of the schema. Imports become available in the schema immediately and in the wrap following codegen.

Imports

Imported types and modules must be assigned a namespace. References to them are prepended with the namespace.

#import { Module, CustomType } into Namespace from "wrap://authority/path"

type CustomType {
prop: Namespace_CustomType!
}

Example

The following example uses all of the elements we've discussed so far.

#import { Module, Connection } into Ethereum from "wrap://ens/wraps.eth:ethereum@2.0.0"
#import { Module } into Ipfs from "ens/wraps.eth:ipfs-http-client@1.0.0"

type Module { # methods are declared in type Module

getIpfsData(
address: String! # types ending with ! are non-nullable
ipfsProvider: String!
connection: Ethereum_Connection # imported types are used like local types
): String!

setIpfsData(
options: SetIpfsDataOptions! # custom types can be arguments
ipfsProvider: String!
connection: Ethereum_Connection
): SetIpfsDataResult! # custom types can be return values
}

type SetIpfsDataOptions { # custom types are declared with "type" keyword
address: String!
data: String!
}

type SetIpfsDataResult {
ipfsHash: String!
txReceipt: String!
}

Default Types

Polywrap's schema definition language supports three kinds of default types:

  • Basic types like integers and strings
  • Complex types like BigInt and JSON
  • Generic types like Array and Map

These types may be implemented differently in different programming languages. The language-specific implementations are clarified in each of the following sections.

Basic Types

Basic types include primitive types like integers and boolean values, as well as Strings and Bytes.

Schema TypeDescription
UInt32-bit unsigned integer
UInt88-bit unsigned integer
UInt1616-bit unsigned integer
UInt3232-bit unsigned integer
Int32-bit signed integer
Int88-bit signed integer
Int1616-bit signed integer
Int3232-bit signed integer
StringUTF-8 string
BooleanTrue or false stored as 1 byte
BytesArray of 8-bit unsigned integers

Complex Types

Complex types are represented as basic types to the Polywrap client and plugin wraps. Within Wasm wraps, they are implemented as classes or structs with behavior.

For example, consider an application developer invoking a Wasm wrap with the JavaScript client. If the invoked method requires an argument of type BigInt, the application developer will provide a string representation of an integer number to satisfy the argument. Within the wrap, the string is deserialized into an instantion of the wrap language's implementation of a BigInt. If the invoked method returns a BigInt, the wrap will serialize the returned BigInt, which the client will then deserialize into a string before returning it to the application developer.

Schema TypeDescription
BigIntMultiple precision integer
BigNumberMultiple precision float
JSONJSON object

Generic Types

When using a generic type, with the exception of arrays, schema writers must add a directive annotation declaring its subtype(s).

The directive takes the form @annotate(type: "TypeName<T\>"), where T is the type of the subtype. The generic type and its subtype(s) can be independently nullable or non-nullable. The following example illustrates use of the generic type directive with the Map type.

type Module {
getKey(
key: String!
map: Map! @annotate(type: "Map<String!, Int!>!")
): Int!

returnMap(
map: Map! @annotate(type: "Map<String!, Int!>!")
): Map! @annotate(type: "Map<String!, Int!>!")
}

Like complex types, the implementation of generic types can differ by language.

Schema TypeDescription
[Type]Array of elements.
MapMap of key-value pairs.

Interfaces

A module can inherit the method declarations of any other module by using the implements keyword. A module that implements an interface in this manner is required to implement and export the interface module's declared methods.

Similarly, a type can inherit the property declarations of any other type by using the implements keyword. If a type implements an interface, it inherits the interface type's property declarations as though the properties were declared explicitly.

#import { Module, InterfaceType } into Interface from "wrap://ens/interface.eth"

type Module implements Interface_Module {
# declares methods of Interface_Module
}

type ImplementationType implements Interface_InterfaceType {
# declares properties of Interface_InterfaceType
}

Environmental Variables

Wrap developers can declare a map of environmental variables for application developers to provide. To use environmental variables, declare an Env type and add a directive to each schema method that will use it.

You can learn how to access environmental variables in code by reading the Access Env in the module section of our Environmental Variables documentation for wrap developers.

Declare the Env object

Environmental variables are declared in a custom type named Env.

type Env {
str: String!
number: Int!
bool: Boolean!
}

Add the @env directive

To access an Env object during a method invocation, add the @env directive to a method. The @env directive has one attribute that indicates whether the Env is required or optional.

type Module {
# App developers are required to provide Env to invoke this method
methodRequireEnv(
arg: String!
): String! @env(required: true)

# Env is optional when invoking this method
methodOptionalEnv(
arg: String!
): String! @env(required: false)
}