#neon #frame #macro #apps #channel

macro neon-frame-macro

Macros to simplify writing Neon apps

2 releases

0.1.1 Oct 2, 2022
0.1.0 Apr 9, 2022

#724 in Procedural macros

Download history 4/week @ 2024-09-19 2/week @ 2024-09-26

263 downloads per month

Apache-2.0

6KB
121 lines

Neon Frame

"Crates",link="https://crates.io/crates/neon-frame" "Crates",link="https://crates.io/crates/neon-frame-macro" "npm (scoped)",link="https://www.npmjs.com/package/@emeraldpay/neon-frame" "License"

A highly opinionated framework to build Neon-based Rust libs for Node. Neon Frame makes a standard interface for a Rust lib to respond back to Javascript. It takes care about handling errors and converting the response back to JS.

Neon Frame simplifies the logic behind handlers by unifying how the result and errors are processed and converted. Now you can just write a usual method that returns a Result<T, E> and it automatically converted and returned to Node.

You can optionally use the Channels, and in this case on the Javascript side of the Neon Frame wraps those calls to get just a plain Promise<T>.

Install

.Cargo.toml [source, toml]

[dependencies]
neon-frame = "0.1"
neon-frame-macro = "0.1"

.package.json [source, json]

"dependencies": {
    "@emeraldpay/neon-frame": "^0.1.1"
}

.JS import [source, typescript]

import {neonFrameCall} from "@emeraldpay/neon-frame";

Short intro to its usage

Rust side

.Now you can mark a function with #[neon_frame_fn] macro: [source, rust]

#[macro_use]
extern crate neon_frame_macro;

#[neon_frame_fn]
pub fn function_hello_world(_cx: &mut FunctionContext) -> Result<String, Errors> {
    Ok("Hello World".to_string())
}

The macro does automatic conversion from Rust to Node types. Instead of String it can be any Serializable structure. Internally it just wraps it into a JSON that can be easily handled on the Node side.

Javascript side

[source, typescript]

import {neonFrameDirectCall} from "@emeraldpay/neon-frame";

// eslint-disable-next-line @typescript-eslint/no-var-requires
const addon = require('path/to/index.node');

export function helloWorld(): string {
    return neonFrameDirectCall<string>(addon, "function_hello_world", []);
}

What if you want to process something in a separate thread? I.e., get it asynchronously as a Promise in Javascript?

Rust side using Channels

.Use #[neon_frame_fn(channel)] macro which provides you with a callback function: [source, rust]

#[neon_frame_fn(channel)]
pub fn function_default_channel<H>(_cx: &mut FunctionContext, handler: H) -> Result<(), Errors>
    where
        H: FnOnce(Result<String, Errors>) + Send + 'static {

    std::thread::spawn(move || {
        let result = "Hello World".to_string();
        handler(Ok(result));
    });

    Ok(())
}

Javascript side using Promise

[source, typescript]

import {neonFrameHandlerCall} from "@emeraldpay/neon-frame";

// eslint-disable-next-line @typescript-eslint/no-var-requires
const addon = require('path/to/index.node');

export function default_channel(): Promise<string> {
    return neonFrameHandlerCall<string>(addon, "function_default_channel", []);
}

Examples

In the /integration-tests directory you can find Interation Tests that can also server as implementation examples.

How it works with details

As a response it uses a JSON like: [source, json]

{
  "succeeded": true,
  "result": {
    "foo": "bar"
  }
}

Where {"foo": "bar"} is an actual result.

If the method failed is returns something like:

[source, json]

{
  "succeeded": false,
  "error": {
    "code": 100,
    "message": "Invalid input value for #0"
  }
}

As a Typescript type it's:

[source, typescript]

type Status<T> = {
    succeeded: boolean,
    result: T | undefined,
    error: {
        code: number,
        message: string
    } | undefined
}

To use Neon Frame simply annotate your method with #[neon_frame_fn]:

[source, rust]

#[neon_frame_fn]
pub fn hello_world(cx: &mut FunctionContext) -> Result<String, MyError> {
    Ok("Hello World".to_string())
}

NOTE: The method is expected to have cx: &mut FunctionContext instead of mut cx: FunctionContext.

Also, you need to write a converter from MyError to (usize, String). I.e. implement the trait impl From<MyError> for (usize, String):

[source, rust]

impl From<MyError> for (usize, String) {
    fn from(err: MyError) -> Self {
        todo!()
    }
}

In addition to the standard synchronous call the library provides same simplification for Channel handlers. At that case you use #[neon_frame_fn(channel)] macro, and use additional parameter to your function for the FnOnce that handles the response:

[source, rust]

// function called from JS as:
//
// hello_world((x) => { ... });
//
#[neon_frame_fn(channel)]
pub fn hello_world<H>(cx: &mut FunctionContext, handler: H) -> Result<(), MyError>
    where
        H: FnOnce(Result<String, MyError>) + Send + 'static {

    std::thread::spawn(move || {
        handler(Ok("Hello World".to_string()));
    });
    Ok(())
}

By default, it uses the first JS argument as a handler function. Buf if you need to use it at a different position you can specify it as parameter like #[neon_frame_fn(channel=2)]

[source, rust]

// function called from JS as:
//
// hello_world("hi", "there", (x) => { ... });
//
// i.e. with handler at the 3rd position, which is 2 starting from zero
//
#[neon_frame_fn(channel=2)]
pub fn hello_world<H>(cx: &mut FunctionContext, handler: H) -> Result<(), MyError>
    where
        H: FnOnce(Result<String, MyError>) + Send + 'static {

    todo!()
}

License

Copyright 2022 EmeraldPay, Inc

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Dependencies

~1.5MB
~37K SLoC