Skip to content

Latest commit

History

103 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Iris

I'm discontinuing this project.

When I wrote this library, dart-lang/rpc did not exist yet. I think that their approach, with the Discovery Document, is more future proof and makes it easier for other developers to consume the API.

I personally like the idea of using Protocol Buffers as a serialization format (which is the format I used in iris), but JSON definitely makes it easier to be used, and doesn't require third party developers to use a compiled client to communicate with the API.

There are two main advantages with Protocol Buffers:

  1. They handle serialization automatically (and check that all values are set properly)
  2. They are fast

The first advantage is handled by the RPC library itself, and the second one is not as important in a public API that has the overhead of HTTP requests and is mostly used on the internet (compared to the intranet).


Build Status

A complete abstraction of client ↔ server communication.

It is basically a remote procedure call implementation in dart. You can call the methods on your remotes and get the result back in futures without having to think about the communication.

Usage

You can look at the example repository for an implementation.

The typical setup is as follows:

  1. Setup your server to generate protocol buffer messages
  2. Write your procedures that handle the requests.
  3. Create an iris object that group your remotes together and setup a server.
  4. Create a server binary which you can then execute to start your iris server.
  5. Setup the build.dart file to generate the client library.
  6. Use the library on the client

As you go along you will need more control over your configuration:

Setup protocol buffers

Protocol buffers are a method of serializing structured data. They are fast and performant, developed and used by Google, and are a great way to define the data being passed between remotes (in contrast to JSON where you have to take care of validating the data yourself, and always need to look at the documentation to see what you actually receive).

The way they work in dart is: you define your messages in .proto files and a library converts them to dart classes (subclasses of GeneratedMessage) which are typed and allow for auto completion and type checking.

Whenever a message in iris is sent or received, it is an instance of GeneratedMessage.

Write procedures on server

Remotes basically are bundles of Procedures. If you have a remote class named RemoteUser with a procedure (a method on this class, with the Procedure annotation) named create, then you will be able to call this remote procedure from the client with remoteUser.create().

Every procedure receives a Context as first parameter and can accept a GeneratedMessage (protocol buffer message) as a second parameter.
The Context contains basic request information (like cookies). If you want to add additional information to the Context object, see the context initializers section.

This is a simple remote example:

classRemoteUserextendsRemote {
/** * This procedure has both, a return type ([CreateUserResponse]) and an * expected request message ([CreateUserRequest]). */@Procedure()
Future<CreateUserResponse> create(Context context, CreateUserRequest request) {
// Create the user, and return a CreateUserResponse
}
/** * This procedure has no return type, so `iris` will assume that nothing will * be sent back to the client. It will just await the execution. */@Procedure()
Futuredelete(Context context, DeleteUserRequest request) {
// Delete the user, and return a resolved Future
}
/** * This is an example procedure that receives and returns no message. */@Procedure()
Futureping(Context context) =>newFuture.value();
}

As you can see, procedures can either accept and return GeneratedMessages or not. Iris understands this, and builds your client library accordingly so you have proper auto completion when writing your client library.

Create an iris object

In a separate file you create a function that returns an Iris object. This object will be used to start the server, and to build the files for the client.

Example lib/iris.dart:

library remote_definitions;
import"package:iris/remote/iris.dart";
// This is the file that contains all your remotesimport"remotes/remotes.dart";
IrisgetIris() {
returnnewIris()
// Add the remotes you want to be served
..addRemote(newRemoteUser())
..addRemote(newRemoteAuthentication())
// Add the servers you want to use
..addServer(newHttpIrisServer("localhost", 8088, allowOrigins:const ['http://127.0.0.1:3030']));
}

Create a server binary

To actually start the iris server which will listen on incoming connections, you simply include Iris and call .startServers() on it.

Example bin/start_server.dart:

import"../lib/iris.dart";
main() {
// Starts all servers that have been added with `.addServer()`.getIris().startServers();
}

Setup build.dart

Now everything on your server is ready! The remotes are served automatically and are listening for incoming requests.

To use these remotes on the client, iris generates a library to be used on the client. This allows you to have completely typed classes that you can use, with autocompletion and request / return types.

To let iris build your client libraries, you need to edit your build.dart and add this build command:

library build;
import'package:iris/builder/builder.dart'as iris_builder;
import"lib/iris.dart";
constIRIS_TARGET="lib/client_remotes";
constIRIS_PROTO_BUFFER_MESSAGES="lib/proto/messages.dart";
constIRIS_REMOTES_DIR="lib/remotes/";
voidmain(List<String> args) {
iris_builder.build(getIris(), IRIS_TARGET, IRIS_PROTO_BUFFER_MESSAGES, args: args, includePbMessages:true, remotesDirectory:IRIS_REMOTES_DIR);
}

The builder will now rebuild your client library every time either your protocol buffer messages or your remotes (only if you specify remotesDirectory) change.

See the standalone library section for more information on how to setup your build.dart file to create a standalone library that can be distributed separately.

On the client

Iris provides two types of client libraries: one is meant to be used on a server, and one for the browser.

Here's an example of using the remotes in a browser:

import"package:iris/client/browser_http_client.dart";
// This includes your generated libraryimport"package:my-generated-lib/remotes.dart";
main() {
var client =newHttpIrisClient(Uri.parse("http://localhost:8088"));
// Create an instance of your remotesvar remotes =newRemotes(client);
// And you're good to go!AuthenticationRequest req =newAuthenticationRequest()
..email ="e@mail.com"
..password ="password";
remotes.remoteUser.auth(req).then((User user) =>doSomething(user));
}

Advanced configuration

Error codes

If an error occurs anywhere in a remote request you always get an IrisException on the client. This IrisException has an errorCode and an internalMessage.

Never show the internalMessage to the user! It is only meant to be logged or inspected by developers.

errorCodes are all you need to tell the client what's wrong. Every time you encounter a problem in your remote, think about what you want to tell the client and create an error code for it.

This is how you setup error codes on the server:

classErrorCodeextendsIrisErrorCode {
staticconstINVALID_USERNAME_OR_PASSWORD=constErrorCode._(0);
staticconstINVALID_EMAIL=constErrorCode._(1);
constErrorCode._(int value) :super(value);
}

and this is how you would throw an error code in a procedure:

classRemoteUserextendsRemote {
@Procedure()
Futurecreate(MyContext context) {
thrownewProcedureException(ErrorCode.INVALID_EMAIL, "Oh noes.");
}
}

on your client:

remotes.remoteUser.create().then(print)
.catchError((IrisException ex) {
if (ex.errorCode ==ErrorCode.INVALID_EMAIL) {
alert("Please provide a valid email address");
}
log.info(ex.internalMessage);
});

There are several internal error codes that you can receive on the client as well. Look at the IrisErrorCode class to see what they are.

If you provide this ErrorCode class to the build function of the builder, an error_code.dart file is generated, containing all error codes as integers to be used on the client.

Context initializers

Every procedure and procedure filter receives a Context object that gets instantiated for every request. If you don't define a ContextInitializer yourself, you will always receive the default Context implementation, which only holds the IrisRequest object.

If you want to have additional information in you context (like session data), you can define your own context class and provide a ContextInitializer to create that object for you.

ContextInitializers are the first thing called when a request comes in. After that all filters are called sequentially, and then your procedure with the initialized Context.

This is the typedef for ContextInitializers:

typedefFuture<Context> ContextInitializer(IrisRequest req);

and here an example implementation:

/** * Your own `Context` class */classMyContextextendsContext {
/// An additional field in your context to hold the session information.finalSession session;
MyContext(IrisRequest req, this.session) :super(req);
}
/** * Now define your context initializer */Future<MyContext> myContextInitializer(IrisRequest req) {
// This can do anything needed for context initialization. Example:// Load session info from the memory cache
myMemoryCache.loadSession(req.cookies["sessionId"])
.then((Session session) {
// And return your context, *with* a sessionreturnnewMyContext(req, session);
})
}
IrisgetIris() {
// And where you create you remote definitions, you now pass the context// initializerreturnnewIris(myContextInitializer)
..addRemote(RemoteUser)
..etc...
}

So, every time you receive a Context object, it is now a MyContext instance.

Filters

Often you need your procedures to be filtered, for example if you need authentication.

Filters are defined with the Remote or the Procedure annotation and this is their typedef:

typedefFuture<bool> FilterFunction(Context context);

You can define filters in your remote like this:

Future<bool> authenticationFilter(Context context) {
// Make sure the user is authenticated.returnnewFuture.value(true);
}
Future<bool> adminRightsFilter(Context context) {
// Make sure the user has admin rightsreturnnewFuture.value(true);
}
/// All procedures in this remote will have the `authenticationFilter`.@Remote(filters:const [authenticationFilter])
classRemoteUserextendsRemote {
/// In addition to the `authenticationFilter` this procedure also has the /// `adminRightsFilter`.@Procedure(filters:const [adminRightsFilter])
Future<CreateUserResponse> create(Context context, CreateUserRequest request) =>newFuture.value();
}

If a filter returns false, the procedure will not be called, and an error will be sent to the client. If you want the client to receive a specific error code, then you can use the ProcedureException for that.

After the ContextInitializer function, all defined filters will be called sequentially and in the defined order and processing the request is immediately stopped when one filter returns false.
Remote filters are always the first filters to run.

If you have set a ContextInitializer all filter functions will receive the context returned by this function.

Standalone library

There are two ways you can distribute your remote remotes:

  1. As part of your server library
  2. As a separate, standalone library

Releasing the remotes as part of your library is easier. You can just let the build script create the necessary client files in your lib/ directory, and users can use your server as a dependency, and import the generated iris files. This means that the user has access to your protocol buffer and ErrorCode files (since they are already in your server library).

The disadvantage of this approach is, of course, that your whole server needs to be exposed. This is fine if your library is only used internally (since you can have a dependency on a private repository), but if you want to distribute the generated client library to other users this won't be working anymore.

This is why iris has the ability to include all necessary resources in the generated library so it can be shipped as a separate library, namely:

  • All protocol buffer messages
  • The error codes

When invoking the build function of the builder, you can additionally pass the ErrorCode class with the errorCodes parameter. Iris will then generate a error_code.dart file with an ErrorCode class that contains all error codes.

If you set the includePbMessages option to true, iris will also copy over all protocol buffer messages, and put them in the proto/ folder.

With the targetDirectory argument (the second positional argument), you can define a directory outside your server directory, which is the library that you can ship without having to worry about leaking sensitive code.

License

(The MIT License)

Copyright (c) 2014 Matias Meno <m@tias.me>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

A complete abstraction of client <-> server communication

Resources

Stars

10 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - enyo/iris: A complete abstraction of client <-> server communication · GitHub
Skip to content

Latest commit

History

103 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Iris

I'm discontinuing this project.

When I wrote this library, dart-lang/rpc did not exist yet. I think that their approach, with the Discovery Document, is more future proof and makes it easier for other developers to consume the API.

I personally like the idea of using Protocol Buffers as a serialization format (which is the format I used in iris), but JSON definitely makes it easier to be used, and doesn't require third party developers to use a compiled client to communicate with the API.

There are two main advantages with Protocol Buffers:

  1. They handle serialization automatically (and check that all values are set properly)
  2. They are fast

The first advantage is handled by the RPC library itself, and the second one is not as important in a public API that has the overhead of HTTP requests and is mostly used on the internet (compared to the intranet).


Build Status

A complete abstraction of client ↔ server communication.

It is basically a remote procedure call implementation in dart. You can call the methods on your remotes and get the result back in futures without having to think about the communication.

Usage

You can look at the example repository for an implementation.

The typical setup is as follows:

  1. Setup your server to generate protocol buffer messages
  2. Write your procedures that handle the requests.
  3. Create an iris object that group your remotes together and setup a server.
  4. Create a server binary which you can then execute to start your iris server.
  5. Setup the build.dart file to generate the client library.
  6. Use the library on the client

As you go along you will need more control over your configuration:

Setup protocol buffers

Protocol buffers are a method of serializing structured data. They are fast and performant, developed and used by Google, and are a great way to define the data being passed between remotes (in contrast to JSON where you have to take care of validating the data yourself, and always need to look at the documentation to see what you actually receive).

The way they work in dart is: you define your messages in .proto files and a library converts them to dart classes (subclasses of GeneratedMessage) which are typed and allow for auto completion and type checking.

Whenever a message in iris is sent or received, it is an instance of GeneratedMessage.

Write procedures on server

Remotes basically are bundles of Procedures. If you have a remote class named RemoteUser with a procedure (a method on this class, with the Procedure annotation) named create, then you will be able to call this remote procedure from the client with remoteUser.create().

Every procedure receives a Context as first parameter and can accept a GeneratedMessage (protocol buffer message) as a second parameter.
The Context contains basic request information (like cookies). If you want to add additional information to the Context object, see the context initializers section.

This is a simple remote example:

classRemoteUserextendsRemote {
/** * This procedure has both, a return type ([CreateUserResponse]) and an * expected request message ([CreateUserRequest]). */@Procedure()
Future<CreateUserResponse> create(Context context, CreateUserRequest request) {
// Create the user, and return a CreateUserResponse
}
/** * This procedure has no return type, so `iris` will assume that nothing will * be sent back to the client. It will just await the execution. */@Procedure()
Futuredelete(Context context, DeleteUserRequest request) {
// Delete the user, and return a resolved Future
}
/** * This is an example procedure that receives and returns no message. */@Procedure()
Futureping(Context context) =>newFuture.value();
}

As you can see, procedures can either accept and return GeneratedMessages or not. Iris understands this, and builds your client library accordingly so you have proper auto completion when writing your client library.

Create an iris object

In a separate file you create a function that returns an Iris object. This object will be used to start the server, and to build the files for the client.

Example lib/iris.dart:

library remote_definitions;
import"package:iris/remote/iris.dart";
// This is the file that contains all your remotesimport"remotes/remotes.dart";
IrisgetIris() {
returnnewIris()
// Add the remotes you want to be served
..addRemote(newRemoteUser())
..addRemote(newRemoteAuthentication())
// Add the servers you want to use
..addServer(newHttpIrisServer("localhost", 8088, allowOrigins:const ['http://127.0.0.1:3030']));
}

Create a server binary

To actually start the iris server which will listen on incoming connections, you simply include Iris and call .startServers() on it.

Example bin/start_server.dart:

import"../lib/iris.dart";
main() {
// Starts all servers that have been added with `.addServer()`.getIris().startServers();
}

Setup build.dart

Now everything on your server is ready! The remotes are served automatically and are listening for incoming requests.

To use these remotes on the client, iris generates a library to be used on the client. This allows you to have completely typed classes that you can use, with autocompletion and request / return types.

To let iris build your client libraries, you need to edit your build.dart and add this build command:

library build;
import'package:iris/builder/builder.dart'as iris_builder;
import"lib/iris.dart";
constIRIS_TARGET="lib/client_remotes";
constIRIS_PROTO_BUFFER_MESSAGES="lib/proto/messages.dart";
constIRIS_REMOTES_DIR="lib/remotes/";
voidmain(List<String> args) {
iris_builder.build(getIris(), IRIS_TARGET, IRIS_PROTO_BUFFER_MESSAGES, args: args, includePbMessages:true, remotesDirectory:IRIS_REMOTES_DIR);
}

The builder will now rebuild your client library every time either your protocol buffer messages or your remotes (only if you specify remotesDirectory) change.

See the standalone library section for more information on how to setup your build.dart file to create a standalone library that can be distributed separately.

On the client

Iris provides two types of client libraries: one is meant to be used on a server, and one for the browser.

Here's an example of using the remotes in a browser:

import"package:iris/client/browser_http_client.dart";
// This includes your generated libraryimport"package:my-generated-lib/remotes.dart";
main() {
var client =newHttpIrisClient(Uri.parse("http://localhost:8088"));
// Create an instance of your remotesvar remotes =newRemotes(client);
// And you're good to go!AuthenticationRequest req =newAuthenticationRequest()
..email ="e@mail.com"
..password ="password";
remotes.remoteUser.auth(req).then((User user) =>doSomething(user));
}

Advanced configuration

Error codes

If an error occurs anywhere in a remote request you always get an IrisException on the client. This IrisException has an errorCode and an internalMessage.

Never show the internalMessage to the user! It is only meant to be logged or inspected by developers.

errorCodes are all you need to tell the client what's wrong. Every time you encounter a problem in your remote, think about what you want to tell the client and create an error code for it.

This is how you setup error codes on the server:

classErrorCodeextendsIrisErrorCode {
staticconstINVALID_USERNAME_OR_PASSWORD=constErrorCode._(0);
staticconstINVALID_EMAIL=constErrorCode._(1);
constErrorCode._(int value) :super(value);
}

and this is how you would throw an error code in a procedure:

classRemoteUserextendsRemote {
@Procedure()
Futurecreate(MyContext context) {
thrownewProcedureException(ErrorCode.INVALID_EMAIL, "Oh noes.");
}
}

on your client:

remotes.remoteUser.create().then(print)
.catchError((IrisException ex) {
if (ex.errorCode ==ErrorCode.INVALID_EMAIL) {
alert("Please provide a valid email address");
}
log.info(ex.internalMessage);
});

There are several internal error codes that you can receive on the client as well. Look at the IrisErrorCode class to see what they are.

If you provide this ErrorCode class to the build function of the builder, an error_code.dart file is generated, containing all error codes as integers to be used on the client.

Context initializers

Every procedure and procedure filter receives a Context object that gets instantiated for every request. If you don't define a ContextInitializer yourself, you will always receive the default Context implementation, which only holds the IrisRequest object.

If you want to have additional information in you context (like session data), you can define your own context class and provide a ContextInitializer to create that object for you.

ContextInitializers are the first thing called when a request comes in. After that all filters are called sequentially, and then your procedure with the initialized Context.

This is the typedef for ContextInitializers:

typedefFuture<Context> ContextInitializer(IrisRequest req);

and here an example implementation:

/** * Your own `Context` class */classMyContextextendsContext {
/// An additional field in your context to hold the session information.finalSession session;
MyContext(IrisRequest req, this.session) :super(req);
}
/** * Now define your context initializer */Future<MyContext> myContextInitializer(IrisRequest req) {
// This can do anything needed for context initialization. Example:// Load session info from the memory cache
myMemoryCache.loadSession(req.cookies["sessionId"])
.then((Session session) {
// And return your context, *with* a sessionreturnnewMyContext(req, session);
})
}
IrisgetIris() {
// And where you create you remote definitions, you now pass the context// initializerreturnnewIris(myContextInitializer)
..addRemote(RemoteUser)
..etc...
}

So, every time you receive a Context object, it is now a MyContext instance.

Filters

Often you need your procedures to be filtered, for example if you need authentication.

Filters are defined with the Remote or the Procedure annotation and this is their typedef:

typedefFuture<bool> FilterFunction(Context context);

You can define filters in your remote like this:

Future<bool> authenticationFilter(Context context) {
// Make sure the user is authenticated.returnnewFuture.value(true);
}
Future<bool> adminRightsFilter(Context context) {
// Make sure the user has admin rightsreturnnewFuture.value(true);
}
/// All procedures in this remote will have the `authenticationFilter`.@Remote(filters:const [authenticationFilter])
classRemoteUserextendsRemote {
/// In addition to the `authenticationFilter` this procedure also has the /// `adminRightsFilter`.@Procedure(filters:const [adminRightsFilter])
Future<CreateUserResponse> create(Context context, CreateUserRequest request) =>newFuture.value();
}

If a filter returns false, the procedure will not be called, and an error will be sent to the client. If you want the client to receive a specific error code, then you can use the ProcedureException for that.

After the ContextInitializer function, all defined filters will be called sequentially and in the defined order and processing the request is immediately stopped when one filter returns false.
Remote filters are always the first filters to run.

If you have set a ContextInitializer all filter functions will receive the context returned by this function.

Standalone library

There are two ways you can distribute your remote remotes:

  1. As part of your server library
  2. As a separate, standalone library

Releasing the remotes as part of your library is easier. You can just let the build script create the necessary client files in your lib/ directory, and users can use your server as a dependency, and import the generated iris files. This means that the user has access to your protocol buffer and ErrorCode files (since they are already in your server library).

The disadvantage of this approach is, of course, that your whole server needs to be exposed. This is fine if your library is only used internally (since you can have a dependency on a private repository), but if you want to distribute the generated client library to other users this won't be working anymore.

This is why iris has the ability to include all necessary resources in the generated library so it can be shipped as a separate library, namely:

  • All protocol buffer messages
  • The error codes

When invoking the build function of the builder, you can additionally pass the ErrorCode class with the errorCodes parameter. Iris will then generate a error_code.dart file with an ErrorCode class that contains all error codes.

If you set the includePbMessages option to true, iris will also copy over all protocol buffer messages, and put them in the proto/ folder.

With the targetDirectory argument (the second positional argument), you can define a directory outside your server directory, which is the library that you can ship without having to worry about leaking sensitive code.

License

(The MIT License)

Copyright (c) 2014 Matias Meno <m@tias.me>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

A complete abstraction of client <-> server communication

Resources

Stars

10 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - enyo/iris: A complete abstraction of client <-> server communication · GitHub
Skip to content

Latest commit

History

103 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Iris

I'm discontinuing this project.

When I wrote this library, dart-lang/rpc did not exist yet. I think that their approach, with the Discovery Document, is more future proof and makes it easier for other developers to consume the API.

I personally like the idea of using Protocol Buffers as a serialization format (which is the format I used in iris), but JSON definitely makes it easier to be used, and doesn't require third party developers to use a compiled client to communicate with the API.

There are two main advantages with Protocol Buffers:

  1. They handle serialization automatically (and check that all values are set properly)
  2. They are fast

The first advantage is handled by the RPC library itself, and the second one is not as important in a public API that has the overhead of HTTP requests and is mostly used on the internet (compared to the intranet).


Build Status

A complete abstraction of client ↔ server communication.

It is basically a remote procedure call implementation in dart. You can call the methods on your remotes and get the result back in futures without having to think about the communication.

Usage

You can look at the example repository for an implementation.

The typical setup is as follows:

  1. Setup your server to generate protocol buffer messages
  2. Write your procedures that handle the requests.
  3. Create an iris object that group your remotes together and setup a server.
  4. Create a server binary which you can then execute to start your iris server.
  5. Setup the build.dart file to generate the client library.
  6. Use the library on the client

As you go along you will need more control over your configuration:

Setup protocol buffers

Protocol buffers are a method of serializing structured data. They are fast and performant, developed and used by Google, and are a great way to define the data being passed between remotes (in contrast to JSON where you have to take care of validating the data yourself, and always need to look at the documentation to see what you actually receive).

The way they work in dart is: you define your messages in .proto files and a library converts them to dart classes (subclasses of GeneratedMessage) which are typed and allow for auto completion and type checking.

Whenever a message in iris is sent or received, it is an instance of GeneratedMessage.

Write procedures on server

Remotes basically are bundles of Procedures. If you have a remote class named RemoteUser with a procedure (a method on this class, with the Procedure annotation) named create, then you will be able to call this remote procedure from the client with remoteUser.create().

Every procedure receives a Context as first parameter and can accept a GeneratedMessage (protocol buffer message) as a second parameter.
The Context contains basic request information (like cookies). If you want to add additional information to the Context object, see the context initializers section.

This is a simple remote example:

classRemoteUserextendsRemote {
/** * This procedure has both, a return type ([CreateUserResponse]) and an * expected request message ([CreateUserRequest]). */@Procedure()
Future<CreateUserResponse> create(Context context, CreateUserRequest request) {
// Create the user, and return a CreateUserResponse
}
/** * This procedure has no return type, so `iris` will assume that nothing will * be sent back to the client. It will just await the execution. */@Procedure()
Futuredelete(Context context, DeleteUserRequest request) {
// Delete the user, and return a resolved Future
}
/** * This is an example procedure that receives and returns no message. */@Procedure()
Futureping(Context context) =>newFuture.value();
}

As you can see, procedures can either accept and return GeneratedMessages or not. Iris understands this, and builds your client library accordingly so you have proper auto completion when writing your client library.

Create an iris object

In a separate file you create a function that returns an Iris object. This object will be used to start the server, and to build the files for the client.

Example lib/iris.dart:

library remote_definitions;
import"package:iris/remote/iris.dart";
// This is the file that contains all your remotesimport"remotes/remotes.dart";
IrisgetIris() {
returnnewIris()
// Add the remotes you want to be served
..addRemote(newRemoteUser())
..addRemote(newRemoteAuthentication())
// Add the servers you want to use
..addServer(newHttpIrisServer("localhost", 8088, allowOrigins:const ['http://127.0.0.1:3030']));
}

Create a server binary

To actually start the iris server which will listen on incoming connections, you simply include Iris and call .startServers() on it.

Example bin/start_server.dart:

import"../lib/iris.dart";
main() {
// Starts all servers that have been added with `.addServer()`.getIris().startServers();
}

Setup build.dart

Now everything on your server is ready! The remotes are served automatically and are listening for incoming requests.

To use these remotes on the client, iris generates a library to be used on the client. This allows you to have completely typed classes that you can use, with autocompletion and request / return types.

To let iris build your client libraries, you need to edit your build.dart and add this build command:

library build;
import'package:iris/builder/builder.dart'as iris_builder;
import"lib/iris.dart";
constIRIS_TARGET="lib/client_remotes";
constIRIS_PROTO_BUFFER_MESSAGES="lib/proto/messages.dart";
constIRIS_REMOTES_DIR="lib/remotes/";
voidmain(List<String> args) {
iris_builder.build(getIris(), IRIS_TARGET, IRIS_PROTO_BUFFER_MESSAGES, args: args, includePbMessages:true, remotesDirectory:IRIS_REMOTES_DIR);
}

The builder will now rebuild your client library every time either your protocol buffer messages or your remotes (only if you specify remotesDirectory) change.

See the standalone library section for more information on how to setup your build.dart file to create a standalone library that can be distributed separately.

On the client

Iris provides two types of client libraries: one is meant to be used on a server, and one for the browser.

Here's an example of using the remotes in a browser:

import"package:iris/client/browser_http_client.dart";
// This includes your generated libraryimport"package:my-generated-lib/remotes.dart";
main() {
var client =newHttpIrisClient(Uri.parse("http://localhost:8088"));
// Create an instance of your remotesvar remotes =newRemotes(client);
// And you're good to go!AuthenticationRequest req =newAuthenticationRequest()
..email ="e@mail.com"
..password ="password";
remotes.remoteUser.auth(req).then((User user) =>doSomething(user));
}

Advanced configuration

Error codes

If an error occurs anywhere in a remote request you always get an IrisException on the client. This IrisException has an errorCode and an internalMessage.

Never show the internalMessage to the user! It is only meant to be logged or inspected by developers.

errorCodes are all you need to tell the client what's wrong. Every time you encounter a problem in your remote, think about what you want to tell the client and create an error code for it.

This is how you setup error codes on the server:

classErrorCodeextendsIrisErrorCode {
staticconstINVALID_USERNAME_OR_PASSWORD=constErrorCode._(0);
staticconstINVALID_EMAIL=constErrorCode._(1);
constErrorCode._(int value) :super(value);
}

and this is how you would throw an error code in a procedure:

classRemoteUserextendsRemote {
@Procedure()
Futurecreate(MyContext context) {
thrownewProcedureException(ErrorCode.INVALID_EMAIL, "Oh noes.");
}
}

on your client:

remotes.remoteUser.create().then(print)
.catchError((IrisException ex) {
if (ex.errorCode ==ErrorCode.INVALID_EMAIL) {
alert("Please provide a valid email address");
}
log.info(ex.internalMessage);
});

There are several internal error codes that you can receive on the client as well. Look at the IrisErrorCode class to see what they are.

If you provide this ErrorCode class to the build function of the builder, an error_code.dart file is generated, containing all error codes as integers to be used on the client.

Context initializers

Every procedure and procedure filter receives a Context object that gets instantiated for every request. If you don't define a ContextInitializer yourself, you will always receive the default Context implementation, which only holds the IrisRequest object.

If you want to have additional information in you context (like session data), you can define your own context class and provide a ContextInitializer to create that object for you.

ContextInitializers are the first thing called when a request comes in. After that all filters are called sequentially, and then your procedure with the initialized Context.

This is the typedef for ContextInitializers:

typedefFuture<Context> ContextInitializer(IrisRequest req);

and here an example implementation:

/** * Your own `Context` class */classMyContextextendsContext {
/// An additional field in your context to hold the session information.finalSession session;
MyContext(IrisRequest req, this.session) :super(req);
}
/** * Now define your context initializer */Future<MyContext> myContextInitializer(IrisRequest req) {
// This can do anything needed for context initialization. Example:// Load session info from the memory cache
myMemoryCache.loadSession(req.cookies["sessionId"])
.then((Session session) {
// And return your context, *with* a sessionreturnnewMyContext(req, session);
})
}
IrisgetIris() {
// And where you create you remote definitions, you now pass the context// initializerreturnnewIris(myContextInitializer)
..addRemote(RemoteUser)
..etc...
}

So, every time you receive a Context object, it is now a MyContext instance.

Filters

Often you need your procedures to be filtered, for example if you need authentication.

Filters are defined with the Remote or the Procedure annotation and this is their typedef:

typedefFuture<bool> FilterFunction(Context context);

You can define filters in your remote like this:

Future<bool> authenticationFilter(Context context) {
// Make sure the user is authenticated.returnnewFuture.value(true);
}
Future<bool> adminRightsFilter(Context context) {
// Make sure the user has admin rightsreturnnewFuture.value(true);
}
/// All procedures in this remote will have the `authenticationFilter`.@Remote(filters:const [authenticationFilter])
classRemoteUserextendsRemote {
/// In addition to the `authenticationFilter` this procedure also has the /// `adminRightsFilter`.@Procedure(filters:const [adminRightsFilter])
Future<CreateUserResponse> create(Context context, CreateUserRequest request) =>newFuture.value();
}

If a filter returns false, the procedure will not be called, and an error will be sent to the client. If you want the client to receive a specific error code, then you can use the ProcedureException for that.

After the ContextInitializer function, all defined filters will be called sequentially and in the defined order and processing the request is immediately stopped when one filter returns false.
Remote filters are always the first filters to run.

If you have set a ContextInitializer all filter functions will receive the context returned by this function.

Standalone library

There are two ways you can distribute your remote remotes:

  1. As part of your server library
  2. As a separate, standalone library

Releasing the remotes as part of your library is easier. You can just let the build script create the necessary client files in your lib/ directory, and users can use your server as a dependency, and import the generated iris files. This means that the user has access to your protocol buffer and ErrorCode files (since they are already in your server library).

The disadvantage of this approach is, of course, that your whole server needs to be exposed. This is fine if your library is only used internally (since you can have a dependency on a private repository), but if you want to distribute the generated client library to other users this won't be working anymore.

This is why iris has the ability to include all necessary resources in the generated library so it can be shipped as a separate library, namely:

  • All protocol buffer messages
  • The error codes

When invoking the build function of the builder, you can additionally pass the ErrorCode class with the errorCodes parameter. Iris will then generate a error_code.dart file with an ErrorCode class that contains all error codes.

If you set the includePbMessages option to true, iris will also copy over all protocol buffer messages, and put them in the proto/ folder.

With the targetDirectory argument (the second positional argument), you can define a directory outside your server directory, which is the library that you can ship without having to worry about leaking sensitive code.

License

(The MIT License)

Copyright (c) 2014 Matias Meno <m@tias.me>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

A complete abstraction of client <-> server communication

Resources

Stars

10 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - enyo/iris: A complete abstraction of client <-> server communication · GitHub
Skip to content

Latest commit

History

103 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Iris

I'm discontinuing this project.

When I wrote this library, dart-lang/rpc did not exist yet. I think that their approach, with the Discovery Document, is more future proof and makes it easier for other developers to consume the API.

I personally like the idea of using Protocol Buffers as a serialization format (which is the format I used in iris), but JSON definitely makes it easier to be used, and doesn't require third party developers to use a compiled client to communicate with the API.

There are two main advantages with Protocol Buffers:

  1. They handle serialization automatically (and check that all values are set properly)
  2. They are fast

The first advantage is handled by the RPC library itself, and the second one is not as important in a public API that has the overhead of HTTP requests and is mostly used on the internet (compared to the intranet).


Build Status

A complete abstraction of client ↔ server communication.

It is basically a remote procedure call implementation in dart. You can call the methods on your remotes and get the result back in futures without having to think about the communication.

Usage

You can look at the example repository for an implementation.

The typical setup is as follows:

  1. Setup your server to generate protocol buffer messages
  2. Write your procedures that handle the requests.
  3. Create an iris object that group your remotes together and setup a server.
  4. Create a server binary which you can then execute to start your iris server.
  5. Setup the build.dart file to generate the client library.
  6. Use the library on the client

As you go along you will need more control over your configuration:

Setup protocol buffers

Protocol buffers are a method of serializing structured data. They are fast and performant, developed and used by Google, and are a great way to define the data being passed between remotes (in contrast to JSON where you have to take care of validating the data yourself, and always need to look at the documentation to see what you actually receive).

The way they work in dart is: you define your messages in .proto files and a library converts them to dart classes (subclasses of GeneratedMessage) which are typed and allow for auto completion and type checking.

Whenever a message in iris is sent or received, it is an instance of GeneratedMessage.

Write procedures on server

Remotes basically are bundles of Procedures. If you have a remote class named RemoteUser with a procedure (a method on this class, with the Procedure annotation) named create, then you will be able to call this remote procedure from the client with remoteUser.create().

Every procedure receives a Context as first parameter and can accept a GeneratedMessage (protocol buffer message) as a second parameter.
The Context contains basic request information (like cookies). If you want to add additional information to the Context object, see the context initializers section.

This is a simple remote example:

classRemoteUserextendsRemote {
/** * This procedure has both, a return type ([CreateUserResponse]) and an * expected request message ([CreateUserRequest]). */@Procedure()
Future<CreateUserResponse> create(Context context, CreateUserRequest request) {
// Create the user, and return a CreateUserResponse
}
/** * This procedure has no return type, so `iris` will assume that nothing will * be sent back to the client. It will just await the execution. */@Procedure()
Futuredelete(Context context, DeleteUserRequest request) {
// Delete the user, and return a resolved Future
}
/** * This is an example procedure that receives and returns no message. */@Procedure()
Futureping(Context context) =>newFuture.value();
}

As you can see, procedures can either accept and return GeneratedMessages or not. Iris understands this, and builds your client library accordingly so you have proper auto completion when writing your client library.

Create an iris object

In a separate file you create a function that returns an Iris object. This object will be used to start the server, and to build the files for the client.

Example lib/iris.dart:

library remote_definitions;
import"package:iris/remote/iris.dart";
// This is the file that contains all your remotesimport"remotes/remotes.dart";
IrisgetIris() {
returnnewIris()
// Add the remotes you want to be served
..addRemote(newRemoteUser())
..addRemote(newRemoteAuthentication())
// Add the servers you want to use
..addServer(newHttpIrisServer("localhost", 8088, allowOrigins:const ['http://127.0.0.1:3030']));
}

Create a server binary

To actually start the iris server which will listen on incoming connections, you simply include Iris and call .startServers() on it.

Example bin/start_server.dart:

import"../lib/iris.dart";
main() {
// Starts all servers that have been added with `.addServer()`.getIris().startServers();
}

Setup build.dart

Now everything on your server is ready! The remotes are served automatically and are listening for incoming requests.

To use these remotes on the client, iris generates a library to be used on the client. This allows you to have completely typed classes that you can use, with autocompletion and request / return types.

To let iris build your client libraries, you need to edit your build.dart and add this build command:

library build;
import'package:iris/builder/builder.dart'as iris_builder;
import"lib/iris.dart";
constIRIS_TARGET="lib/client_remotes";
constIRIS_PROTO_BUFFER_MESSAGES="lib/proto/messages.dart";
constIRIS_REMOTES_DIR="lib/remotes/";
voidmain(List<String> args) {
iris_builder.build(getIris(), IRIS_TARGET, IRIS_PROTO_BUFFER_MESSAGES, args: args, includePbMessages:true, remotesDirectory:IRIS_REMOTES_DIR);
}

The builder will now rebuild your client library every time either your protocol buffer messages or your remotes (only if you specify remotesDirectory) change.

See the standalone library section for more information on how to setup your build.dart file to create a standalone library that can be distributed separately.

On the client

Iris provides two types of client libraries: one is meant to be used on a server, and one for the browser.

Here's an example of using the remotes in a browser:

import"package:iris/client/browser_http_client.dart";
// This includes your generated libraryimport"package:my-generated-lib/remotes.dart";
main() {
var client =newHttpIrisClient(Uri.parse("http://localhost:8088"));
// Create an instance of your remotesvar remotes =newRemotes(client);
// And you're good to go!AuthenticationRequest req =newAuthenticationRequest()
..email ="e@mail.com"
..password ="password";
remotes.remoteUser.auth(req).then((User user) =>doSomething(user));
}

Advanced configuration

Error codes

If an error occurs anywhere in a remote request you always get an IrisException on the client. This IrisException has an errorCode and an internalMessage.

Never show the internalMessage to the user! It is only meant to be logged or inspected by developers.

errorCodes are all you need to tell the client what's wrong. Every time you encounter a problem in your remote, think about what you want to tell the client and create an error code for it.

This is how you setup error codes on the server:

classErrorCodeextendsIrisErrorCode {
staticconstINVALID_USERNAME_OR_PASSWORD=constErrorCode._(0);
staticconstINVALID_EMAIL=constErrorCode._(1);
constErrorCode._(int value) :super(value);
}

and this is how you would throw an error code in a procedure:

classRemoteUserextendsRemote {
@Procedure()
Futurecreate(MyContext context) {
thrownewProcedureException(ErrorCode.INVALID_EMAIL, "Oh noes.");
}
}

on your client:

remotes.remoteUser.create().then(print)
.catchError((IrisException ex) {
if (ex.errorCode ==ErrorCode.INVALID_EMAIL) {
alert("Please provide a valid email address");
}
log.info(ex.internalMessage);
});

There are several internal error codes that you can receive on the client as well. Look at the IrisErrorCode class to see what they are.

If you provide this ErrorCode class to the build function of the builder, an error_code.dart file is generated, containing all error codes as integers to be used on the client.

Context initializers

Every procedure and procedure filter receives a Context object that gets instantiated for every request. If you don't define a ContextInitializer yourself, you will always receive the default Context implementation, which only holds the IrisRequest object.

If you want to have additional information in you context (like session data), you can define your own context class and provide a ContextInitializer to create that object for you.

ContextInitializers are the first thing called when a request comes in. After that all filters are called sequentially, and then your procedure with the initialized Context.

This is the typedef for ContextInitializers:

typedefFuture<Context> ContextInitializer(IrisRequest req);

and here an example implementation:

/** * Your own `Context` class */classMyContextextendsContext {
/// An additional field in your context to hold the session information.finalSession session;
MyContext(IrisRequest req, this.session) :super(req);
}
/** * Now define your context initializer */Future<MyContext> myContextInitializer(IrisRequest req) {
// This can do anything needed for context initialization. Example:// Load session info from the memory cache
myMemoryCache.loadSession(req.cookies["sessionId"])
.then((Session session) {
// And return your context, *with* a sessionreturnnewMyContext(req, session);
})
}
IrisgetIris() {
// And where you create you remote definitions, you now pass the context// initializerreturnnewIris(myContextInitializer)
..addRemote(RemoteUser)
..etc...
}

So, every time you receive a Context object, it is now a MyContext instance.

Filters

Often you need your procedures to be filtered, for example if you need authentication.

Filters are defined with the Remote or the Procedure annotation and this is their typedef:

typedefFuture<bool> FilterFunction(Context context);

You can define filters in your remote like this:

Future<bool> authenticationFilter(Context context) {
// Make sure the user is authenticated.returnnewFuture.value(true);
}
Future<bool> adminRightsFilter(Context context) {
// Make sure the user has admin rightsreturnnewFuture.value(true);
}
/// All procedures in this remote will have the `authenticationFilter`.@Remote(filters:const [authenticationFilter])
classRemoteUserextendsRemote {
/// In addition to the `authenticationFilter` this procedure also has the /// `adminRightsFilter`.@Procedure(filters:const [adminRightsFilter])
Future<CreateUserResponse> create(Context context, CreateUserRequest request) =>newFuture.value();
}

If a filter returns false, the procedure will not be called, and an error will be sent to the client. If you want the client to receive a specific error code, then you can use the ProcedureException for that.

After the ContextInitializer function, all defined filters will be called sequentially and in the defined order and processing the request is immediately stopped when one filter returns false.
Remote filters are always the first filters to run.

If you have set a ContextInitializer all filter functions will receive the context returned by this function.

Standalone library

There are two ways you can distribute your remote remotes:

  1. As part of your server library
  2. As a separate, standalone library

Releasing the remotes as part of your library is easier. You can just let the build script create the necessary client files in your lib/ directory, and users can use your server as a dependency, and import the generated iris files. This means that the user has access to your protocol buffer and ErrorCode files (since they are already in your server library).

The disadvantage of this approach is, of course, that your whole server needs to be exposed. This is fine if your library is only used internally (since you can have a dependency on a private repository), but if you want to distribute the generated client library to other users this won't be working anymore.

This is why iris has the ability to include all necessary resources in the generated library so it can be shipped as a separate library, namely:

  • All protocol buffer messages
  • The error codes

When invoking the build function of the builder, you can additionally pass the ErrorCode class with the errorCodes parameter. Iris will then generate a error_code.dart file with an ErrorCode class that contains all error codes.

If you set the includePbMessages option to true, iris will also copy over all protocol buffer messages, and put them in the proto/ folder.

With the targetDirectory argument (the second positional argument), you can define a directory outside your server directory, which is the library that you can ship without having to worry about leaking sensitive code.

License

(The MIT License)

Copyright (c) 2014 Matias Meno <m@tias.me>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

A complete abstraction of client <-> server communication

Resources

Stars

10 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - enyo/iris: A complete abstraction of client <-> server communication · GitHub
Skip to content

Latest commit

History

103 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Iris

I'm discontinuing this project.

When I wrote this library, dart-lang/rpc did not exist yet. I think that their approach, with the Discovery Document, is more future proof and makes it easier for other developers to consume the API.

I personally like the idea of using Protocol Buffers as a serialization format (which is the format I used in iris), but JSON definitely makes it easier to be used, and doesn't require third party developers to use a compiled client to communicate with the API.

There are two main advantages with Protocol Buffers:

  1. They handle serialization automatically (and check that all values are set properly)
  2. They are fast

The first advantage is handled by the RPC library itself, and the second one is not as important in a public API that has the overhead of HTTP requests and is mostly used on the internet (compared to the intranet).


Build Status

A complete abstraction of client ↔ server communication.

It is basically a remote procedure call implementation in dart. You can call the methods on your remotes and get the result back in futures without having to think about the communication.

Usage

You can look at the example repository for an implementation.

The typical setup is as follows:

  1. Setup your server to generate protocol buffer messages
  2. Write your procedures that handle the requests.
  3. Create an iris object that group your remotes together and setup a server.
  4. Create a server binary which you can then execute to start your iris server.
  5. Setup the build.dart file to generate the client library.
  6. Use the library on the client

As you go along you will need more control over your configuration:

Setup protocol buffers

Protocol buffers are a method of serializing structured data. They are fast and performant, developed and used by Google, and are a great way to define the data being passed between remotes (in contrast to JSON where you have to take care of validating the data yourself, and always need to look at the documentation to see what you actually receive).

The way they work in dart is: you define your messages in .proto files and a library converts them to dart classes (subclasses of GeneratedMessage) which are typed and allow for auto completion and type checking.

Whenever a message in iris is sent or received, it is an instance of GeneratedMessage.

Write procedures on server

Remotes basically are bundles of Procedures. If you have a remote class named RemoteUser with a procedure (a method on this class, with the Procedure annotation) named create, then you will be able to call this remote procedure from the client with remoteUser.create().

Every procedure receives a Context as first parameter and can accept a GeneratedMessage (protocol buffer message) as a second parameter.
The Context contains basic request information (like cookies). If you want to add additional information to the Context object, see the context initializers section.

This is a simple remote example:

classRemoteUserextendsRemote {
/** * This procedure has both, a return type ([CreateUserResponse]) and an * expected request message ([CreateUserRequest]). */@Procedure()
Future<CreateUserResponse> create(Context context, CreateUserRequest request) {
// Create the user, and return a CreateUserResponse
}
/** * This procedure has no return type, so `iris` will assume that nothing will * be sent back to the client. It will just await the execution. */@Procedure()
Futuredelete(Context context, DeleteUserRequest request) {
// Delete the user, and return a resolved Future
}
/** * This is an example procedure that receives and returns no message. */@Procedure()
Futureping(Context context) =>newFuture.value();
}

As you can see, procedures can either accept and return GeneratedMessages or not. Iris understands this, and builds your client library accordingly so you have proper auto completion when writing your client library.

Create an iris object

In a separate file you create a function that returns an Iris object. This object will be used to start the server, and to build the files for the client.

Example lib/iris.dart:

library remote_definitions;
import"package:iris/remote/iris.dart";
// This is the file that contains all your remotesimport"remotes/remotes.dart";
IrisgetIris() {
returnnewIris()
// Add the remotes you want to be served
..addRemote(newRemoteUser())
..addRemote(newRemoteAuthentication())
// Add the servers you want to use
..addServer(newHttpIrisServer("localhost", 8088, allowOrigins:const ['http://127.0.0.1:3030']));
}

Create a server binary

To actually start the iris server which will listen on incoming connections, you simply include Iris and call .startServers() on it.

Example bin/start_server.dart:

import"../lib/iris.dart";
main() {
// Starts all servers that have been added with `.addServer()`.getIris().startServers();
}

Setup build.dart

Now everything on your server is ready! The remotes are served automatically and are listening for incoming requests.

To use these remotes on the client, iris generates a library to be used on the client. This allows you to have completely typed classes that you can use, with autocompletion and request / return types.

To let iris build your client libraries, you need to edit your build.dart and add this build command:

library build;
import'package:iris/builder/builder.dart'as iris_builder;
import"lib/iris.dart";
constIRIS_TARGET="lib/client_remotes";
constIRIS_PROTO_BUFFER_MESSAGES="lib/proto/messages.dart";
constIRIS_REMOTES_DIR="lib/remotes/";
voidmain(List<String> args) {
iris_builder.build(getIris(), IRIS_TARGET, IRIS_PROTO_BUFFER_MESSAGES, args: args, includePbMessages:true, remotesDirectory:IRIS_REMOTES_DIR);
}

The builder will now rebuild your client library every time either your protocol buffer messages or your remotes (only if you specify remotesDirectory) change.

See the standalone library section for more information on how to setup your build.dart file to create a standalone library that can be distributed separately.

On the client

Iris provides two types of client libraries: one is meant to be used on a server, and one for the browser.

Here's an example of using the remotes in a browser:

import"package:iris/client/browser_http_client.dart";
// This includes your generated libraryimport"package:my-generated-lib/remotes.dart";
main() {
var client =newHttpIrisClient(Uri.parse("http://localhost:8088"));
// Create an instance of your remotesvar remotes =newRemotes(client);
// And you're good to go!AuthenticationRequest req =newAuthenticationRequest()
..email ="e@mail.com"
..password ="password";
remotes.remoteUser.auth(req).then((User user) =>doSomething(user));
}

Advanced configuration

Error codes

If an error occurs anywhere in a remote request you always get an IrisException on the client. This IrisException has an errorCode and an internalMessage.

Never show the internalMessage to the user! It is only meant to be logged or inspected by developers.

errorCodes are all you need to tell the client what's wrong. Every time you encounter a problem in your remote, think about what you want to tell the client and create an error code for it.

This is how you setup error codes on the server:

classErrorCodeextendsIrisErrorCode {
staticconstINVALID_USERNAME_OR_PASSWORD=constErrorCode._(0);
staticconstINVALID_EMAIL=constErrorCode._(1);
constErrorCode._(int value) :super(value);
}

and this is how you would throw an error code in a procedure:

classRemoteUserextendsRemote {
@Procedure()
Futurecreate(MyContext context) {
thrownewProcedureException(ErrorCode.INVALID_EMAIL, "Oh noes.");
}
}

on your client:

remotes.remoteUser.create().then(print)
.catchError((IrisException ex) {
if (ex.errorCode ==ErrorCode.INVALID_EMAIL) {
alert("Please provide a valid email address");
}
log.info(ex.internalMessage);
});

There are several internal error codes that you can receive on the client as well. Look at the IrisErrorCode class to see what they are.

If you provide this ErrorCode class to the build function of the builder, an error_code.dart file is generated, containing all error codes as integers to be used on the client.

Context initializers

Every procedure and procedure filter receives a Context object that gets instantiated for every request. If you don't define a ContextInitializer yourself, you will always receive the default Context implementation, which only holds the IrisRequest object.

If you want to have additional information in you context (like session data), you can define your own context class and provide a ContextInitializer to create that object for you.

ContextInitializers are the first thing called when a request comes in. After that all filters are called sequentially, and then your procedure with the initialized Context.

This is the typedef for ContextInitializers:

typedefFuture<Context> ContextInitializer(IrisRequest req);

and here an example implementation:

/** * Your own `Context` class */classMyContextextendsContext {
/// An additional field in your context to hold the session information.finalSession session;
MyContext(IrisRequest req, this.session) :super(req);
}
/** * Now define your context initializer */Future<MyContext> myContextInitializer(IrisRequest req) {
// This can do anything needed for context initialization. Example:// Load session info from the memory cache
myMemoryCache.loadSession(req.cookies["sessionId"])
.then((Session session) {
// And return your context, *with* a sessionreturnnewMyContext(req, session);
})
}
IrisgetIris() {
// And where you create you remote definitions, you now pass the context// initializerreturnnewIris(myContextInitializer)
..addRemote(RemoteUser)
..etc...
}

So, every time you receive a Context object, it is now a MyContext instance.

Filters

Often you need your procedures to be filtered, for example if you need authentication.

Filters are defined with the Remote or the Procedure annotation and this is their typedef:

typedefFuture<bool> FilterFunction(Context context);

You can define filters in your remote like this:

Future<bool> authenticationFilter(Context context) {
// Make sure the user is authenticated.returnnewFuture.value(true);
}
Future<bool> adminRightsFilter(Context context) {
// Make sure the user has admin rightsreturnnewFuture.value(true);
}
/// All procedures in this remote will have the `authenticationFilter`.@Remote(filters:const [authenticationFilter])
classRemoteUserextendsRemote {
/// In addition to the `authenticationFilter` this procedure also has the /// `adminRightsFilter`.@Procedure(filters:const [adminRightsFilter])
Future<CreateUserResponse> create(Context context, CreateUserRequest request) =>newFuture.value();
}

If a filter returns false, the procedure will not be called, and an error will be sent to the client. If you want the client to receive a specific error code, then you can use the ProcedureException for that.

After the ContextInitializer function, all defined filters will be called sequentially and in the defined order and processing the request is immediately stopped when one filter returns false.
Remote filters are always the first filters to run.

If you have set a ContextInitializer all filter functions will receive the context returned by this function.

Standalone library

There are two ways you can distribute your remote remotes:

  1. As part of your server library
  2. As a separate, standalone library

Releasing the remotes as part of your library is easier. You can just let the build script create the necessary client files in your lib/ directory, and users can use your server as a dependency, and import the generated iris files. This means that the user has access to your protocol buffer and ErrorCode files (since they are already in your server library).

The disadvantage of this approach is, of course, that your whole server needs to be exposed. This is fine if your library is only used internally (since you can have a dependency on a private repository), but if you want to distribute the generated client library to other users this won't be working anymore.

This is why iris has the ability to include all necessary resources in the generated library so it can be shipped as a separate library, namely:

  • All protocol buffer messages
  • The error codes

When invoking the build function of the builder, you can additionally pass the ErrorCode class with the errorCodes parameter. Iris will then generate a error_code.dart file with an ErrorCode class that contains all error codes.

If you set the includePbMessages option to true, iris will also copy over all protocol buffer messages, and put them in the proto/ folder.

With the targetDirectory argument (the second positional argument), you can define a directory outside your server directory, which is the library that you can ship without having to worry about leaking sensitive code.

License

(The MIT License)

Copyright (c) 2014 Matias Meno <m@tias.me>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

A complete abstraction of client <-> server communication

Resources

Stars

10 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - enyo/iris: A complete abstraction of client <-> server communication · GitHub
Skip to content

Latest commit

History

103 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Iris

I'm discontinuing this project.

When I wrote this library, dart-lang/rpc did not exist yet. I think that their approach, with the Discovery Document, is more future proof and makes it easier for other developers to consume the API.

I personally like the idea of using Protocol Buffers as a serialization format (which is the format I used in iris), but JSON definitely makes it easier to be used, and doesn't require third party developers to use a compiled client to communicate with the API.

There are two main advantages with Protocol Buffers:

  1. They handle serialization automatically (and check that all values are set properly)
  2. They are fast

The first advantage is handled by the RPC library itself, and the second one is not as important in a public API that has the overhead of HTTP requests and is mostly used on the internet (compared to the intranet).


Build Status

A complete abstraction of client ↔ server communication.

It is basically a remote procedure call implementation in dart. You can call the methods on your remotes and get the result back in futures without having to think about the communication.

Usage

You can look at the example repository for an implementation.

The typical setup is as follows:

  1. Setup your server to generate protocol buffer messages
  2. Write your procedures that handle the requests.
  3. Create an iris object that group your remotes together and setup a server.
  4. Create a server binary which you can then execute to start your iris server.
  5. Setup the build.dart file to generate the client library.
  6. Use the library on the client

As you go along you will need more control over your configuration:

Setup protocol buffers

Protocol buffers are a method of serializing structured data. They are fast and performant, developed and used by Google, and are a great way to define the data being passed between remotes (in contrast to JSON where you have to take care of validating the data yourself, and always need to look at the documentation to see what you actually receive).

The way they work in dart is: you define your messages in .proto files and a library converts them to dart classes (subclasses of GeneratedMessage) which are typed and allow for auto completion and type checking.

Whenever a message in iris is sent or received, it is an instance of GeneratedMessage.

Write procedures on server

Remotes basically are bundles of Procedures. If you have a remote class named RemoteUser with a procedure (a method on this class, with the Procedure annotation) named create, then you will be able to call this remote procedure from the client with remoteUser.create().

Every procedure receives a Context as first parameter and can accept a GeneratedMessage (protocol buffer message) as a second parameter.
The Context contains basic request information (like cookies). If you want to add additional information to the Context object, see the context initializers section.

This is a simple remote example:

classRemoteUserextendsRemote {
/** * This procedure has both, a return type ([CreateUserResponse]) and an * expected request message ([CreateUserRequest]). */@Procedure()
Future<CreateUserResponse> create(Context context, CreateUserRequest request) {
// Create the user, and return a CreateUserResponse
}
/** * This procedure has no return type, so `iris` will assume that nothing will * be sent back to the client. It will just await the execution. */@Procedure()
Futuredelete(Context context, DeleteUserRequest request) {
// Delete the user, and return a resolved Future
}
/** * This is an example procedure that receives and returns no message. */@Procedure()
Futureping(Context context) =>newFuture.value();
}

As you can see, procedures can either accept and return GeneratedMessages or not. Iris understands this, and builds your client library accordingly so you have proper auto completion when writing your client library.

Create an iris object

In a separate file you create a function that returns an Iris object. This object will be used to start the server, and to build the files for the client.

Example lib/iris.dart:

library remote_definitions;
import"package:iris/remote/iris.dart";
// This is the file that contains all your remotesimport"remotes/remotes.dart";
IrisgetIris() {
returnnewIris()
// Add the remotes you want to be served
..addRemote(newRemoteUser())
..addRemote(newRemoteAuthentication())
// Add the servers you want to use
..addServer(newHttpIrisServer("localhost", 8088, allowOrigins:const ['http://127.0.0.1:3030']));
}

Create a server binary

To actually start the iris server which will listen on incoming connections, you simply include Iris and call .startServers() on it.

Example bin/start_server.dart:

import"../lib/iris.dart";
main() {
// Starts all servers that have been added with `.addServer()`.getIris().startServers();
}

Setup build.dart

Now everything on your server is ready! The remotes are served automatically and are listening for incoming requests.

To use these remotes on the client, iris generates a library to be used on the client. This allows you to have completely typed classes that you can use, with autocompletion and request / return types.

To let iris build your client libraries, you need to edit your build.dart and add this build command:

library build;
import'package:iris/builder/builder.dart'as iris_builder;
import"lib/iris.dart";
constIRIS_TARGET="lib/client_remotes";
constIRIS_PROTO_BUFFER_MESSAGES="lib/proto/messages.dart";
constIRIS_REMOTES_DIR="lib/remotes/";
voidmain(List<String> args) {
iris_builder.build(getIris(), IRIS_TARGET, IRIS_PROTO_BUFFER_MESSAGES, args: args, includePbMessages:true, remotesDirectory:IRIS_REMOTES_DIR);
}

The builder will now rebuild your client library every time either your protocol buffer messages or your remotes (only if you specify remotesDirectory) change.

See the standalone library section for more information on how to setup your build.dart file to create a standalone library that can be distributed separately.

On the client

Iris provides two types of client libraries: one is meant to be used on a server, and one for the browser.

Here's an example of using the remotes in a browser:

import"package:iris/client/browser_http_client.dart";
// This includes your generated libraryimport"package:my-generated-lib/remotes.dart";
main() {
var client =newHttpIrisClient(Uri.parse("http://localhost:8088"));
// Create an instance of your remotesvar remotes =newRemotes(client);
// And you're good to go!AuthenticationRequest req =newAuthenticationRequest()
..email ="e@mail.com"
..password ="password";
remotes.remoteUser.auth(req).then((User user) =>doSomething(user));
}

Advanced configuration

Error codes

If an error occurs anywhere in a remote request you always get an IrisException on the client. This IrisException has an errorCode and an internalMessage.

Never show the internalMessage to the user! It is only meant to be logged or inspected by developers.

errorCodes are all you need to tell the client what's wrong. Every time you encounter a problem in your remote, think about what you want to tell the client and create an error code for it.

This is how you setup error codes on the server:

classErrorCodeextendsIrisErrorCode {
staticconstINVALID_USERNAME_OR_PASSWORD=constErrorCode._(0);
staticconstINVALID_EMAIL=constErrorCode._(1);
constErrorCode._(int value) :super(value);
}

and this is how you would throw an error code in a procedure:

classRemoteUserextendsRemote {
@Procedure()
Futurecreate(MyContext context) {
thrownewProcedureException(ErrorCode.INVALID_EMAIL, "Oh noes.");
}
}

on your client:

remotes.remoteUser.create().then(print)
.catchError((IrisException ex) {
if (ex.errorCode ==ErrorCode.INVALID_EMAIL) {
alert("Please provide a valid email address");
}
log.info(ex.internalMessage);
});

There are several internal error codes that you can receive on the client as well. Look at the IrisErrorCode class to see what they are.

If you provide this ErrorCode class to the build function of the builder, an error_code.dart file is generated, containing all error codes as integers to be used on the client.

Context initializers

Every procedure and procedure filter receives a Context object that gets instantiated for every request. If you don't define a ContextInitializer yourself, you will always receive the default Context implementation, which only holds the IrisRequest object.

If you want to have additional information in you context (like session data), you can define your own context class and provide a ContextInitializer to create that object for you.

ContextInitializers are the first thing called when a request comes in. After that all filters are called sequentially, and then your procedure with the initialized Context.

This is the typedef for ContextInitializers:

typedefFuture<Context> ContextInitializer(IrisRequest req);

and here an example implementation:

/** * Your own `Context` class */classMyContextextendsContext {
/// An additional field in your context to hold the session information.finalSession session;
MyContext(IrisRequest req, this.session) :super(req);
}
/** * Now define your context initializer */Future<MyContext> myContextInitializer(IrisRequest req) {
// This can do anything needed for context initialization. Example:// Load session info from the memory cache
myMemoryCache.loadSession(req.cookies["sessionId"])
.then((Session session) {
// And return your context, *with* a sessionreturnnewMyContext(req, session);
})
}
IrisgetIris() {
// And where you create you remote definitions, you now pass the context// initializerreturnnewIris(myContextInitializer)
..addRemote(RemoteUser)
..etc...
}

So, every time you receive a Context object, it is now a MyContext instance.

Filters

Often you need your procedures to be filtered, for example if you need authentication.

Filters are defined with the Remote or the Procedure annotation and this is their typedef:

typedefFuture<bool> FilterFunction(Context context);

You can define filters in your remote like this:

Future<bool> authenticationFilter(Context context) {
// Make sure the user is authenticated.returnnewFuture.value(true);
}
Future<bool> adminRightsFilter(Context context) {
// Make sure the user has admin rightsreturnnewFuture.value(true);
}
/// All procedures in this remote will have the `authenticationFilter`.@Remote(filters:const [authenticationFilter])
classRemoteUserextendsRemote {
/// In addition to the `authenticationFilter` this procedure also has the /// `adminRightsFilter`.@Procedure(filters:const [adminRightsFilter])
Future<CreateUserResponse> create(Context context, CreateUserRequest request) =>newFuture.value();
}

If a filter returns false, the procedure will not be called, and an error will be sent to the client. If you want the client to receive a specific error code, then you can use the ProcedureException for that.

After the ContextInitializer function, all defined filters will be called sequentially and in the defined order and processing the request is immediately stopped when one filter returns false.
Remote filters are always the first filters to run.

If you have set a ContextInitializer all filter functions will receive the context returned by this function.

Standalone library

There are two ways you can distribute your remote remotes:

  1. As part of your server library
  2. As a separate, standalone library

Releasing the remotes as part of your library is easier. You can just let the build script create the necessary client files in your lib/ directory, and users can use your server as a dependency, and import the generated iris files. This means that the user has access to your protocol buffer and ErrorCode files (since they are already in your server library).

The disadvantage of this approach is, of course, that your whole server needs to be exposed. This is fine if your library is only used internally (since you can have a dependency on a private repository), but if you want to distribute the generated client library to other users this won't be working anymore.

This is why iris has the ability to include all necessary resources in the generated library so it can be shipped as a separate library, namely:

  • All protocol buffer messages
  • The error codes

When invoking the build function of the builder, you can additionally pass the ErrorCode class with the errorCodes parameter. Iris will then generate a error_code.dart file with an ErrorCode class that contains all error codes.

If you set the includePbMessages option to true, iris will also copy over all protocol buffer messages, and put them in the proto/ folder.

With the targetDirectory argument (the second positional argument), you can define a directory outside your server directory, which is the library that you can ship without having to worry about leaking sensitive code.

License

(The MIT License)

Copyright (c) 2014 Matias Meno <m@tias.me>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

A complete abstraction of client <-> server communication

Resources

Stars

10 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - enyo/iris: A complete abstraction of client <-> server communication · GitHub
Skip to content

Latest commit

History

103 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Iris

I'm discontinuing this project.

When I wrote this library, dart-lang/rpc did not exist yet. I think that their approach, with the Discovery Document, is more future proof and makes it easier for other developers to consume the API.

I personally like the idea of using Protocol Buffers as a serialization format (which is the format I used in iris), but JSON definitely makes it easier to be used, and doesn't require third party developers to use a compiled client to communicate with the API.

There are two main advantages with Protocol Buffers:

  1. They handle serialization automatically (and check that all values are set properly)
  2. They are fast

The first advantage is handled by the RPC library itself, and the second one is not as important in a public API that has the overhead of HTTP requests and is mostly used on the internet (compared to the intranet).


Build Status

A complete abstraction of client ↔ server communication.

It is basically a remote procedure call implementation in dart. You can call the methods on your remotes and get the result back in futures without having to think about the communication.

Usage

You can look at the example repository for an implementation.

The typical setup is as follows:

  1. Setup your server to generate protocol buffer messages
  2. Write your procedures that handle the requests.
  3. Create an iris object that group your remotes together and setup a server.
  4. Create a server binary which you can then execute to start your iris server.
  5. Setup the build.dart file to generate the client library.
  6. Use the library on the client

As you go along you will need more control over your configuration:

Setup protocol buffers

Protocol buffers are a method of serializing structured data. They are fast and performant, developed and used by Google, and are a great way to define the data being passed between remotes (in contrast to JSON where you have to take care of validating the data yourself, and always need to look at the documentation to see what you actually receive).

The way they work in dart is: you define your messages in .proto files and a library converts them to dart classes (subclasses of GeneratedMessage) which are typed and allow for auto completion and type checking.

Whenever a message in iris is sent or received, it is an instance of GeneratedMessage.

Write procedures on server

Remotes basically are bundles of Procedures. If you have a remote class named RemoteUser with a procedure (a method on this class, with the Procedure annotation) named create, then you will be able to call this remote procedure from the client with remoteUser.create().

Every procedure receives a Context as first parameter and can accept a GeneratedMessage (protocol buffer message) as a second parameter.
The Context contains basic request information (like cookies). If you want to add additional information to the Context object, see the context initializers section.

This is a simple remote example:

classRemoteUserextendsRemote {
/** * This procedure has both, a return type ([CreateUserResponse]) and an * expected request message ([CreateUserRequest]). */@Procedure()
Future<CreateUserResponse> create(Context context, CreateUserRequest request) {
// Create the user, and return a CreateUserResponse
}
/** * This procedure has no return type, so `iris` will assume that nothing will * be sent back to the client. It will just await the execution. */@Procedure()
Futuredelete(Context context, DeleteUserRequest request) {
// Delete the user, and return a resolved Future
}
/** * This is an example procedure that receives and returns no message. */@Procedure()
Futureping(Context context) =>newFuture.value();
}

As you can see, procedures can either accept and return GeneratedMessages or not. Iris understands this, and builds your client library accordingly so you have proper auto completion when writing your client library.

Create an iris object

In a separate file you create a function that returns an Iris object. This object will be used to start the server, and to build the files for the client.

Example lib/iris.dart:

library remote_definitions;
import"package:iris/remote/iris.dart";
// This is the file that contains all your remotesimport"remotes/remotes.dart";
IrisgetIris() {
returnnewIris()
// Add the remotes you want to be served
..addRemote(newRemoteUser())
..addRemote(newRemoteAuthentication())
// Add the servers you want to use
..addServer(newHttpIrisServer("localhost", 8088, allowOrigins:const ['http://127.0.0.1:3030']));
}

Create a server binary

To actually start the iris server which will listen on incoming connections, you simply include Iris and call .startServers() on it.

Example bin/start_server.dart:

import"../lib/iris.dart";
main() {
// Starts all servers that have been added with `.addServer()`.getIris().startServers();
}

Setup build.dart

Now everything on your server is ready! The remotes are served automatically and are listening for incoming requests.

To use these remotes on the client, iris generates a library to be used on the client. This allows you to have completely typed classes that you can use, with autocompletion and request / return types.

To let iris build your client libraries, you need to edit your build.dart and add this build command:

library build;
import'package:iris/builder/builder.dart'as iris_builder;
import"lib/iris.dart";
constIRIS_TARGET="lib/client_remotes";
constIRIS_PROTO_BUFFER_MESSAGES="lib/proto/messages.dart";
constIRIS_REMOTES_DIR="lib/remotes/";
voidmain(List<String> args) {
iris_builder.build(getIris(), IRIS_TARGET, IRIS_PROTO_BUFFER_MESSAGES, args: args, includePbMessages:true, remotesDirectory:IRIS_REMOTES_DIR);
}

The builder will now rebuild your client library every time either your protocol buffer messages or your remotes (only if you specify remotesDirectory) change.

See the standalone library section for more information on how to setup your build.dart file to create a standalone library that can be distributed separately.

On the client

Iris provides two types of client libraries: one is meant to be used on a server, and one for the browser.

Here's an example of using the remotes in a browser:

import"package:iris/client/browser_http_client.dart";
// This includes your generated libraryimport"package:my-generated-lib/remotes.dart";
main() {
var client =newHttpIrisClient(Uri.parse("http://localhost:8088"));
// Create an instance of your remotesvar remotes =newRemotes(client);
// And you're good to go!AuthenticationRequest req =newAuthenticationRequest()
..email ="e@mail.com"
..password ="password";
remotes.remoteUser.auth(req).then((User user) =>doSomething(user));
}

Advanced configuration

Error codes

If an error occurs anywhere in a remote request you always get an IrisException on the client. This IrisException has an errorCode and an internalMessage.

Never show the internalMessage to the user! It is only meant to be logged or inspected by developers.

errorCodes are all you need to tell the client what's wrong. Every time you encounter a problem in your remote, think about what you want to tell the client and create an error code for it.

This is how you setup error codes on the server:

classErrorCodeextendsIrisErrorCode {
staticconstINVALID_USERNAME_OR_PASSWORD=constErrorCode._(0);
staticconstINVALID_EMAIL=constErrorCode._(1);
constErrorCode._(int value) :super(value);
}

and this is how you would throw an error code in a procedure:

classRemoteUserextendsRemote {
@Procedure()
Futurecreate(MyContext context) {
thrownewProcedureException(ErrorCode.INVALID_EMAIL, "Oh noes.");
}
}

on your client:

remotes.remoteUser.create().then(print)
.catchError((IrisException ex) {
if (ex.errorCode ==ErrorCode.INVALID_EMAIL) {
alert("Please provide a valid email address");
}
log.info(ex.internalMessage);
});

There are several internal error codes that you can receive on the client as well. Look at the IrisErrorCode class to see what they are.

If you provide this ErrorCode class to the build function of the builder, an error_code.dart file is generated, containing all error codes as integers to be used on the client.

Context initializers

Every procedure and procedure filter receives a Context object that gets instantiated for every request. If you don't define a ContextInitializer yourself, you will always receive the default Context implementation, which only holds the IrisRequest object.

If you want to have additional information in you context (like session data), you can define your own context class and provide a ContextInitializer to create that object for you.

ContextInitializers are the first thing called when a request comes in. After that all filters are called sequentially, and then your procedure with the initialized Context.

This is the typedef for ContextInitializers:

typedefFuture<Context> ContextInitializer(IrisRequest req);

and here an example implementation:

/** * Your own `Context` class */classMyContextextendsContext {
/// An additional field in your context to hold the session information.finalSession session;
MyContext(IrisRequest req, this.session) :super(req);
}
/** * Now define your context initializer */Future<MyContext> myContextInitializer(IrisRequest req) {
// This can do anything needed for context initialization. Example:// Load session info from the memory cache
myMemoryCache.loadSession(req.cookies["sessionId"])
.then((Session session) {
// And return your context, *with* a sessionreturnnewMyContext(req, session);
})
}
IrisgetIris() {
// And where you create you remote definitions, you now pass the context// initializerreturnnewIris(myContextInitializer)
..addRemote(RemoteUser)
..etc...
}

So, every time you receive a Context object, it is now a MyContext instance.

Filters

Often you need your procedures to be filtered, for example if you need authentication.

Filters are defined with the Remote or the Procedure annotation and this is their typedef:

typedefFuture<bool> FilterFunction(Context context);

You can define filters in your remote like this:

Future<bool> authenticationFilter(Context context) {
// Make sure the user is authenticated.returnnewFuture.value(true);
}
Future<bool> adminRightsFilter(Context context) {
// Make sure the user has admin rightsreturnnewFuture.value(true);
}
/// All procedures in this remote will have the `authenticationFilter`.@Remote(filters:const [authenticationFilter])
classRemoteUserextendsRemote {
/// In addition to the `authenticationFilter` this procedure also has the /// `adminRightsFilter`.@Procedure(filters:const [adminRightsFilter])
Future<CreateUserResponse> create(Context context, CreateUserRequest request) =>newFuture.value();
}

If a filter returns false, the procedure will not be called, and an error will be sent to the client. If you want the client to receive a specific error code, then you can use the ProcedureException for that.

After the ContextInitializer function, all defined filters will be called sequentially and in the defined order and processing the request is immediately stopped when one filter returns false.
Remote filters are always the first filters to run.

If you have set a ContextInitializer all filter functions will receive the context returned by this function.

Standalone library

There are two ways you can distribute your remote remotes:

  1. As part of your server library
  2. As a separate, standalone library

Releasing the remotes as part of your library is easier. You can just let the build script create the necessary client files in your lib/ directory, and users can use your server as a dependency, and import the generated iris files. This means that the user has access to your protocol buffer and ErrorCode files (since they are already in your server library).

The disadvantage of this approach is, of course, that your whole server needs to be exposed. This is fine if your library is only used internally (since you can have a dependency on a private repository), but if you want to distribute the generated client library to other users this won't be working anymore.

This is why iris has the ability to include all necessary resources in the generated library so it can be shipped as a separate library, namely:

  • All protocol buffer messages
  • The error codes

When invoking the build function of the builder, you can additionally pass the ErrorCode class with the errorCodes parameter. Iris will then generate a error_code.dart file with an ErrorCode class that contains all error codes.

If you set the includePbMessages option to true, iris will also copy over all protocol buffer messages, and put them in the proto/ folder.

With the targetDirectory argument (the second positional argument), you can define a directory outside your server directory, which is the library that you can ship without having to worry about leaking sensitive code.

License

(The MIT License)

Copyright (c) 2014 Matias Meno <m@tias.me>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

A complete abstraction of client <-> server communication

Resources

Stars

10 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - enyo/iris: A complete abstraction of client <-> server communication · GitHub
Skip to content

Latest commit

History

103 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Iris

I'm discontinuing this project.

When I wrote this library, dart-lang/rpc did not exist yet. I think that their approach, with the Discovery Document, is more future proof and makes it easier for other developers to consume the API.

I personally like the idea of using Protocol Buffers as a serialization format (which is the format I used in iris), but JSON definitely makes it easier to be used, and doesn't require third party developers to use a compiled client to communicate with the API.

There are two main advantages with Protocol Buffers:

  1. They handle serialization automatically (and check that all values are set properly)
  2. They are fast

The first advantage is handled by the RPC library itself, and the second one is not as important in a public API that has the overhead of HTTP requests and is mostly used on the internet (compared to the intranet).


Build Status

A complete abstraction of client ↔ server communication.

It is basically a remote procedure call implementation in dart. You can call the methods on your remotes and get the result back in futures without having to think about the communication.

Usage

You can look at the example repository for an implementation.

The typical setup is as follows:

  1. Setup your server to generate protocol buffer messages
  2. Write your procedures that handle the requests.
  3. Create an iris object that group your remotes together and setup a server.
  4. Create a server binary which you can then execute to start your iris server.
  5. Setup the build.dart file to generate the client library.
  6. Use the library on the client

As you go along you will need more control over your configuration:

Setup protocol buffers

Protocol buffers are a method of serializing structured data. They are fast and performant, developed and used by Google, and are a great way to define the data being passed between remotes (in contrast to JSON where you have to take care of validating the data yourself, and always need to look at the documentation to see what you actually receive).

The way they work in dart is: you define your messages in .proto files and a library converts them to dart classes (subclasses of GeneratedMessage) which are typed and allow for auto completion and type checking.

Whenever a message in iris is sent or received, it is an instance of GeneratedMessage.

Write procedures on server

Remotes basically are bundles of Procedures. If you have a remote class named RemoteUser with a procedure (a method on this class, with the Procedure annotation) named create, then you will be able to call this remote procedure from the client with remoteUser.create().

Every procedure receives a Context as first parameter and can accept a GeneratedMessage (protocol buffer message) as a second parameter.
The Context contains basic request information (like cookies). If you want to add additional information to the Context object, see the context initializers section.

This is a simple remote example:

classRemoteUserextendsRemote {
/** * This procedure has both, a return type ([CreateUserResponse]) and an * expected request message ([CreateUserRequest]). */@Procedure()
Future<CreateUserResponse> create(Context context, CreateUserRequest request) {
// Create the user, and return a CreateUserResponse
}
/** * This procedure has no return type, so `iris` will assume that nothing will * be sent back to the client. It will just await the execution. */@Procedure()
Futuredelete(Context context, DeleteUserRequest request) {
// Delete the user, and return a resolved Future
}
/** * This is an example procedure that receives and returns no message. */@Procedure()
Futureping(Context context) =>newFuture.value();
}

As you can see, procedures can either accept and return GeneratedMessages or not. Iris understands this, and builds your client library accordingly so you have proper auto completion when writing your client library.

Create an iris object

In a separate file you create a function that returns an Iris object. This object will be used to start the server, and to build the files for the client.

Example lib/iris.dart:

library remote_definitions;
import"package:iris/remote/iris.dart";
// This is the file that contains all your remotesimport"remotes/remotes.dart";
IrisgetIris() {
returnnewIris()
// Add the remotes you want to be served
..addRemote(newRemoteUser())
..addRemote(newRemoteAuthentication())
// Add the servers you want to use
..addServer(newHttpIrisServer("localhost", 8088, allowOrigins:const ['http://127.0.0.1:3030']));
}

Create a server binary

To actually start the iris server which will listen on incoming connections, you simply include Iris and call .startServers() on it.

Example bin/start_server.dart:

import"../lib/iris.dart";
main() {
// Starts all servers that have been added with `.addServer()`.getIris().startServers();
}

Setup build.dart

Now everything on your server is ready! The remotes are served automatically and are listening for incoming requests.

To use these remotes on the client, iris generates a library to be used on the client. This allows you to have completely typed classes that you can use, with autocompletion and request / return types.

To let iris build your client libraries, you need to edit your build.dart and add this build command:

library build;
import'package:iris/builder/builder.dart'as iris_builder;
import"lib/iris.dart";
constIRIS_TARGET="lib/client_remotes";
constIRIS_PROTO_BUFFER_MESSAGES="lib/proto/messages.dart";
constIRIS_REMOTES_DIR="lib/remotes/";
voidmain(List<String> args) {
iris_builder.build(getIris(), IRIS_TARGET, IRIS_PROTO_BUFFER_MESSAGES, args: args, includePbMessages:true, remotesDirectory:IRIS_REMOTES_DIR);
}

The builder will now rebuild your client library every time either your protocol buffer messages or your remotes (only if you specify remotesDirectory) change.

See the standalone library section for more information on how to setup your build.dart file to create a standalone library that can be distributed separately.

On the client

Iris provides two types of client libraries: one is meant to be used on a server, and one for the browser.

Here's an example of using the remotes in a browser:

import"package:iris/client/browser_http_client.dart";
// This includes your generated libraryimport"package:my-generated-lib/remotes.dart";
main() {
var client =newHttpIrisClient(Uri.parse("http://localhost:8088"));
// Create an instance of your remotesvar remotes =newRemotes(client);
// And you're good to go!AuthenticationRequest req =newAuthenticationRequest()
..email ="e@mail.com"
..password ="password";
remotes.remoteUser.auth(req).then((User user) =>doSomething(user));
}

Advanced configuration

Error codes

If an error occurs anywhere in a remote request you always get an IrisException on the client. This IrisException has an errorCode and an internalMessage.

Never show the internalMessage to the user! It is only meant to be logged or inspected by developers.

errorCodes are all you need to tell the client what's wrong. Every time you encounter a problem in your remote, think about what you want to tell the client and create an error code for it.

This is how you setup error codes on the server:

classErrorCodeextendsIrisErrorCode {
staticconstINVALID_USERNAME_OR_PASSWORD=constErrorCode._(0);
staticconstINVALID_EMAIL=constErrorCode._(1);
constErrorCode._(int value) :super(value);
}

and this is how you would throw an error code in a procedure:

classRemoteUserextendsRemote {
@Procedure()
Futurecreate(MyContext context) {
thrownewProcedureException(ErrorCode.INVALID_EMAIL, "Oh noes.");
}
}

on your client:

remotes.remoteUser.create().then(print)
.catchError((IrisException ex) {
if (ex.errorCode ==ErrorCode.INVALID_EMAIL) {
alert("Please provide a valid email address");
}
log.info(ex.internalMessage);
});

There are several internal error codes that you can receive on the client as well. Look at the IrisErrorCode class to see what they are.

If you provide this ErrorCode class to the build function of the builder, an error_code.dart file is generated, containing all error codes as integers to be used on the client.

Context initializers

Every procedure and procedure filter receives a Context object that gets instantiated for every request. If you don't define a ContextInitializer yourself, you will always receive the default Context implementation, which only holds the IrisRequest object.

If you want to have additional information in you context (like session data), you can define your own context class and provide a ContextInitializer to create that object for you.

ContextInitializers are the first thing called when a request comes in. After that all filters are called sequentially, and then your procedure with the initialized Context.

This is the typedef for ContextInitializers:

typedefFuture<Context> ContextInitializer(IrisRequest req);

and here an example implementation:

/** * Your own `Context` class */classMyContextextendsContext {
/// An additional field in your context to hold the session information.finalSession session;
MyContext(IrisRequest req, this.session) :super(req);
}
/** * Now define your context initializer */Future<MyContext> myContextInitializer(IrisRequest req) {
// This can do anything needed for context initialization. Example:// Load session info from the memory cache
myMemoryCache.loadSession(req.cookies["sessionId"])
.then((Session session) {
// And return your context, *with* a sessionreturnnewMyContext(req, session);
})
}
IrisgetIris() {
// And where you create you remote definitions, you now pass the context// initializerreturnnewIris(myContextInitializer)
..addRemote(RemoteUser)
..etc...
}

So, every time you receive a Context object, it is now a MyContext instance.

Filters

Often you need your procedures to be filtered, for example if you need authentication.

Filters are defined with the Remote or the Procedure annotation and this is their typedef:

typedefFuture<bool> FilterFunction(Context context);

You can define filters in your remote like this:

Future<bool> authenticationFilter(Context context) {
// Make sure the user is authenticated.returnnewFuture.value(true);
}
Future<bool> adminRightsFilter(Context context) {
// Make sure the user has admin rightsreturnnewFuture.value(true);
}
/// All procedures in this remote will have the `authenticationFilter`.@Remote(filters:const [authenticationFilter])
classRemoteUserextendsRemote {
/// In addition to the `authenticationFilter` this procedure also has the /// `adminRightsFilter`.@Procedure(filters:const [adminRightsFilter])
Future<CreateUserResponse> create(Context context, CreateUserRequest request) =>newFuture.value();
}

If a filter returns false, the procedure will not be called, and an error will be sent to the client. If you want the client to receive a specific error code, then you can use the ProcedureException for that.

After the ContextInitializer function, all defined filters will be called sequentially and in the defined order and processing the request is immediately stopped when one filter returns false.
Remote filters are always the first filters to run.

If you have set a ContextInitializer all filter functions will receive the context returned by this function.

Standalone library

There are two ways you can distribute your remote remotes:

  1. As part of your server library
  2. As a separate, standalone library

Releasing the remotes as part of your library is easier. You can just let the build script create the necessary client files in your lib/ directory, and users can use your server as a dependency, and import the generated iris files. This means that the user has access to your protocol buffer and ErrorCode files (since they are already in your server library).

The disadvantage of this approach is, of course, that your whole server needs to be exposed. This is fine if your library is only used internally (since you can have a dependency on a private repository), but if you want to distribute the generated client library to other users this won't be working anymore.

This is why iris has the ability to include all necessary resources in the generated library so it can be shipped as a separate library, namely:

  • All protocol buffer messages
  • The error codes

When invoking the build function of the builder, you can additionally pass the ErrorCode class with the errorCodes parameter. Iris will then generate a error_code.dart file with an ErrorCode class that contains all error codes.

If you set the includePbMessages option to true, iris will also copy over all protocol buffer messages, and put them in the proto/ folder.

With the targetDirectory argument (the second positional argument), you can define a directory outside your server directory, which is the library that you can ship without having to worry about leaking sensitive code.

License

(The MIT License)

Copyright (c) 2014 Matias Meno <m@tias.me>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

A complete abstraction of client <-> server communication

Resources

Stars

10 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages