This plugin works as Postman, which supports:
- Visualize the structure of your HTTP endpoints
- Enable to call your endpoints in EditMode
- Generate boilerplate code based on your structure to simplify the process of calling HTTP endpoints in PlayMode
Because the project leverages some Unity UIToolkit new elements and the Roslyn compiler
Please do not move the plugin folder to a different position because of breaking path constants in the plugin source
1. Asset store
SummerRest.x.y.z.unitypackage
- only necessary assets of the plugin (x.y.z is a semantic version)SummerRestSample.unitypackage
- a sample project shows simple usages of the plugin
You can add https://github.com/risethesummer/Unity-SummerRest.git?path=SummerRest/Assets/Plugins/SummerRest to Package Manager
Or add "com.summer.summer-rest": "https://github.com/risethesummer/Unity-SummerRest.git?path=SummerRest/Assets/Plugins/SummerRest"
to dependencies
section in Packages/manifest.json
{
"dependencies": {
"com.summer.summer-rest": "https://github.com/risethesummer/Unity-SummerRest.git?path=SummerRest/Assets/Plugins/SummerRest",
// ... other packages
}
}
If you want to set a target version, please inserting a release tag x.y.z so you can specify a version on the release page (unless Unity takes the latest version by default). For example "com.summer.summer-rest": "https://github.com/risethesummer/Unity-SummerRest.git?path=SummerRest/Assets/Plugins/SummerRest#1.0.0"
There are some important definitions in the plugin you should know to easily get acquainted with it
First and foremost, we must know the structure of an endpoint tree
Endpoint
: every components below are treated asEndpoint
(technically they inherit it)Domain
: This is the root component of a single backend, you may have multiple domains in your project.- For example, you have a master service (my-master-service.com) and a storage service (my-storage-service.com), they possibly come up with 2 completely distinct domains and structures
Master domain (my-master-service.com) User serivice GetUser PostUser Storage domain (my-storage-service.com) GetImage GetVideoClip
- The main reason why we made this component is
API Versioning
: A domain has usually more than one origin (dev, prod, test...), and you have to select the active origin.
- For example, you have a master service (my-master-service.com) and a storage service (my-storage-service.com), they possibly come up with 2 completely distinct domains and structures
Service
: A service is nothing but an Endpoint container, it's only used to build API structureRequest
: The terminal component of an Endpoint tree (It's not allowed to have any child)- Inheriting resource path from parents stands out as the most crucial benefit of this plugin (technically string concatenation). So, the url of an element must be influenced by its parents (please note the url field of the previous captures)
Additionally, you may see these things everywhere in the plugin
- None, Inherit, Custom, AppendToParent: a field marked with this attribute is able to leverage value from its closest parent, you may set
None
to leave it default - Raw text or custom class: at some point, the plugin allows to use a custom class instead of raw text (request body, auth data)
-
Showing all types of a project is a performance killer (in spite of EditMode). So, we force you to implement predefined interfaces (IRequestBodyData, IAuthData) before showing your types in the dropdown
-
Finally, fields of a serializable class are exposed and serialized thanks to Unity Serialization, check the example below if your custom classes do not work as expected
[Serializable] //Essential attribute to make this class work with Unity Serialization class MyRequestBody : IRequestBodyData { [SerializeField] private int privateFieldMustBeAnnotatedWithSerializeField; // does not show up on the Inspector public int NotExposedBecauseUnityDoesNotRecognizeProperty { get; set; } //[field: SerializeField] => the baking field is shown (but serialized name is <WrongName>K_BakingField) [field: SerializeField] public int WrongName { get; set; } }
You may observe that "notSerializedFieldBecausePrivate" has the wrong name, and "NotExposedBecauseUnityDoesNotRecognizeProperty" is missing from the Inspector
Please note that, we encounter the serilization constraints because we are using Unity Serialization. Ignore them if you plan to use your own data serializer eg. NewtonSoft, System.Text.Json...
-
- After installing the plugin, click on the
Tools/SummerRest
to open the plugin window - The plugin works on an asset named "SummerRestConfiguration" (please do not modify it manually!), an initializing panel will be shown if the plugin does not detect the asset. You have to select a folder which contains assets belonging to the plugin
- Initially, you need to define at least 1 domain, click on
Add
to create a new domain
- A Domain must have at least 1 origin, please note that origins should be absolute URLs eg. https://dummyjson.com (this is a public service for testing only, please do not compromise it)
- Right click (or
Add
button) on an item of the domain tree view to create/delete its children- Domain and Service are not callable, only Request offers that feature. There are some notable fields:
Name
: name of generated class associated with this endpoint Source GenerationPath
: relative path from its parentUrl
: absolute url formed from the parents' path and its path
- Domain and Service are not callable, only Request offers that feature. There are some notable fields:
- We will create a Service named
Products
(relative path is activity) - Then, create a GET Request to get the information of product
1
(or you may directly create this Request without the previous Service (Products), but remember to fill out the relative path correctly eg. products/1) - Click on
Do Request
to call your endpoint in EditMode - We are tightly sticking the request to the product
1
(we need to change the path in case we refer to another product). The plugin supportssmart string
in typing the relative path of a Request (we consider the paths of Domain and Service are stable, so currently we do not allow it) - You may create a search request by using the request parameters
- Another request which posts data, please note the request body and method
- Another one downloading an image (we have created a domain named DummyJsonCdn)
- And another uploading multipart form data (this endpoint does not exist, only used to depict a multipart uploading request)
- Please have a look at Sample project for further usages
The guidances above are only applied for public APIs. Most of the time, you work with secured APIs that need some factors (eg. JWT, api key, username/password pair...) to authenticate and authorize your operations
- The plugin supports to append auth information to your requests automatically
- Click on
Advanced settings
to open the auth settings section - You will see a list of auth containers, each of them contains a record of key, appender type and secret value
Key
: unique value, which being referred by endpointsSecret value
: the value will be only used for EditMode requests, and resolved by an ISecretRepository in PlayModeAppender type
: how the secret value will be appended into a request (typically modify the request's header), currently we supportBearerToken
,Basic(Username/password)
,... You can make your own appender by- Not reusable: Manually modify params or headers of the request
- Reusable: implement IAuthAppender, then the class will be listed in the type dropdown
- For example: if you use
BearerTokenAuthAppender
with valuemy-data
and keymy-key
, every requests refer to this container (key=my-key
) will be added with a header"Authorization":"Bearer my-data"
- Storing your secrets on RAM maybe a bad idea for several reasons:
- Can not remember logged in sessions
- Easy to be exploited by attackers
- ... no idea :)
- The plugin provides a single place resolving your secrets; So a request only keeps an auth key, it needs to query a repository about the secret value
sequenceDiagram
participant A as Auth Appender
participant R as Requester
participant S as Secrets Repository
R->>S: This is an auth key, please give me the secret value of it
S->>R: Here it is
R->>A: Please add the secret to my requests
- The default repository is PlayerPrefsSecretRepository based on Unity PlayerPrefs. But you can implement your own by:
- Inherit ISecretRepository
- Modify the default repository to your class
To illustrate what we have discussed on this topic so far. We're going to use a short example by calling an GetCurrentAuthUser
api, since this endpoint requires an bearer token through a header named Authorization
Although this type of behaviour is supported basically BearerTokenAuthAppender. To make it clear, we still make a new appender by implementing IAuthAppender
// This behaves the same as what BearerTokenAuthAppender does
public class DummyJsonApiAuthAppender : IAuthAppender<DummyJsonApiAuthAppender, string>
{
private const string AuthKeyword = "Bearer";
public void Append<TResponse>(string data, IWebRequestAdaptor<TResponse> requestAdaptor)
{
// Append a header "Authorization: Bearer <my-token>"
requestAdaptor.SetHeader("Authorization", $"{AuthKeyword} {data}");
}
}
First, we need to create a request to login (and get access token)
Then, create the respective auth container in the plugin window. Select the class you have just created as the appender and input the received token
In any Endpoint, refer to this container if you're about to authenticate the requests arisen from it
If you only call in EditMode, you are able to call the request up to now, because we are taking the secret value from the window. The window is useless in PlayMode; Before calling an endpoint in PlayMode, please make sure that current ISecretRepository can resolve the auth key of the request
// Save the secret value
// 1. This method is easiser and works with every type of data
ISecretRepository.Current.Save("dummy-json-token", "my long token...");
// 2. If you are using the default one based on PlayerPrefs
// You can directly access PlayerPrefs yourself because they query the same source
PlayerPrefs.SetString("dummy-json-token", "my long token...");
If you find this way too complex, you can easily add a header to the domain, then inheriting the header in every child requests.
The plugin helps to leverage your structure to automatically generate corresponding source code called in PlayMode. Click Generate source to
to initiate the process (since this process is kind of heavy, it's wise to let you run it manually)
The generated source will be structured as what you have designed in the Editor. The name of each class reflects on the name of the associated endpoint
public static class MyDomain {
public class MyRequest1 { ...
}
public static class MyService {
public class MyRequest2 { ...
}
...
}
- Because of C# limitations, we can not have an embedded class having the same name as its parent and siblings, so you must manage to avoid class name collisions (use distinct names to easily address this problem)
MyService
MyService
public static class MyService {
public static class MyService {} // => This causes the name collision error
}
The examples below are extracted from the Sample project
A class generated from Request
comes up with some utility methods for calling the respective endpoint
- First, create a request object by invoking static
Create()
method (after a very long road :))var request = MyDomain.MyService.MyRequest2.Create();
- Originally, a request's information (headers, params, url...) is initially alighted with what you assigned in the Editor
- Technically, we copied your inputs to generated classes
// This code only illustrates a generated request, do not write it yourself // The properties of this class initially copy your configures public PostRequest() : base("http://my-domain.com/data", "http://my-domain.com/data", IRequestModifier<AuthRequestModifier<SummerRest.Runtime.Authenticate.Appenders.BearerTokenAuthAppender, System.String>>.GetSingleton()) { Method = HttpMethod.Post; Headers.Add(Keys.Headers.Header1, "header-value-1"); BodyFormat = DataFormat.Json; InitializedSerializedBody = @"i am a big cat"; }
- With
text or data
request body: we keep the serialized text in the Editor - With
multipart form
request: only text rows are copied, your file rows are only used in EditMode (but the file keys are still generated)
- Technically, we copied your inputs to generated classes
- You can modify the cloned values through the object's properties (The auth key is modifiable but the appender is not). Please note that, a request object is reusable, you can keep it as a field permanently
// Allias to the long name using Request2 = MyDomain.MyService.MyRequest2; public class MyBehaviour : MonoBehaviour { private Request2 _myRequest; private void CreateRequest() { _myRequest = Request2.Create(); // Instead of typing the keys yourself, you should access Keys class for getting predefined strings // Request2.Keys.Headers.RunTimeHeader results in "run-time-header" _myRequest.Headers.Add(Request2.Keys.Headers.RunTimeHeader, "run-time-value"); // Request2.Keys.Headers.UrlFormat.ProductId results in "product-id" _myRequest.SetUrlValue(Request2.Keys.Headers.UrlFormat.ProductId, "my-product"); // Request2.Keys.Params.Search results in "search" _myRequest.Params.SetSingleParam(Request2.Keys.Params.Search, "player has just typed something"); } }
- The plugin supports 3 types of request:
data
,texture
,audio clip
. Each of them has 2 versions: Simple (only return response body) and Detailed (Reponse body, code, headers...)... // Continue from the last example using Request2 = MyDomain.MyService.MyRequest2; public class MyResponseData { public string Name { get; set; } public int Age { get; set; } } public class MyBehaviour : MonoBehaviour { private Request2 _myRequest; private void DoRequest() { // Request normal data StartCoroutine(_myRequest.DataRequestCoroutine<MyResponseData>(HandleResponse, HandleError)); // Request texture StartCoroutine(_myRequest.TextureRequestCoroutine(HandleResponseTexture, true)); // Request audio clip StartCoroutine(_myRequest.AudioRequestCoroutine(HandleResponseAudioClip, AudioType.WAV)); } private void HandleResponse(MyResponseData responseData) { ... } private void HandleResponseTexture(Texture2D texture) { ... } private void HandleResponseAudioClip(AudioClip audioClip) { ... } // OnError is optional private void HandleError(ResponseError error) { ... } }
- Simple methods only provide you with a response body, in case you want to delve into the response. You should consider leveraging detailed methods. Please note that you must call
IWebResponse<>.Dispose()
after finishing using it (or wrap it with ausing
statement)private void DoDetailedRequest() { // Request normal data StartCoroutine(_myRequest.DetailedDataRequestCoroutine<MyResponseData>(HandleDetailedResponse)); // Request audioClip/texture is similar to the previous step ... } private void HandleDetailedResponse(IWebResponse<MyResponseData> responseData) { // Please wrap the response inside a using statement (or call Dispose manually) using (responseData) { Debug.Log(responseData.StatusCode); Debug.Log(responseData.RawData); Debug.Log(responseData.Data); } // In case you do not leverage using statement // responseData.Dispose() ... }
- In case you need to make undefined requests (eg. an image url). You would rather use WebRequestUtility
public void GetImageByAbsoluteUrl(string url) { StartCoroutine(WebRequestUtility.TextureRequestCoroutine( url: url, nonReadable: false, doneCallback: ShowImage, // Use a builder to modify the request adaptorBuilder: b => { // Change request data if neccessary b.RedirectLimit = 3; b.SetHeader("my-header", "my-value"); })); } private void ShowImage(Texture2D text) { ... }
- Please have a look at Sample project for complete examples
- Normally, generated classes only have coroutine methods.
- You can enable async methods by add "SUMMER_REST_TASK" Scripting Define Symbol and import UniTask package. Async methods are highly recommended because of simplicity
- Please note that async methods throw exceptions on error instead of callbacks
private async UniTaskVoid GetProductDataAsync(int productId) { var getProduct = GetProduct.Create(); // Simple response try { // Please wrap the response inside a using statement (or call Dispose manually) using var productResponse = await getProduct.DetailedDataRequestAsync<Product>(); Debug.LogFormat("My response {0}", productResponse); } catch (ResponseErrorException responseErrorException) { //Undefined exception Debug.Log("Network error {0}", responseErrorException.Error); } catch (Exception e) { //Undefined exception Debug.LogException(e); } }
The plugin provides the most common way to deal with HTTP requests. But, you are able to embed your implementations easily
- Data serializer: the default serializer bases on JsonUtility and XmlSerializer, you can adapt it through the plugin window (Advanced settings section) or
IDataSerializer.Current
- ISecretRepository: the default repository bases on Unity PlayerPrefs, you can adapt it through the plugin window (Advanced settings section) or
ISecretRepository.Current
- There are more considerations like IContentTypeParser, IUrlBuilder... we do not offer default selections for them in the window because we suppose there is no need to change their logic
We would like to express our sincere gratitude to the following dependencies
- DummyJson: provides different types of REST Endpoints filled with JSON data
- UniTask: provides an efficient async/await integration to Unity
We extend our thanks to the developers and maintainers of these dependencies for their outstanding work and contribution to the open-source community.