Skip to content

Commit

Permalink
feat(Node bindings): Add plugins module
Browse files Browse the repository at this point in the history
  • Loading branch information
nokome committed Apr 17, 2021
1 parent 3f3c8b3 commit 71fe53d
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 2 deletions.
1 change: 1 addition & 0 deletions node/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * as plugins from './plugins'
export * as config from './config'
63 changes: 63 additions & 0 deletions node/lib/plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { fromJSON } from './prelude'

const addon = require('../native')

export type Installation = 'binary' | 'docker' | 'package'

export interface Plugin {
// Properties from the plugin's codemeta.json file
name: string
softwareVersion: string
description: string
installUrl: string[]
featureList: Record<string, unknown>[]

// If installed, the installation type
installation?: Installation

// The current alias for this plugin, if any
alias?: string
}

/**
* List the installed plugins
*
* @returns An array of plugins
*/
export function list(): Plugin[] {
return fromJSON<Plugin[]>(addon.pluginsList())
}

/**
* Install a plugin
*
* @param spec A plugin identifier e.g. `javascript@0.50.1`
* @param installations An array of installation methods to try
* @return An array of installed plugins
*/
export function install(
spec: string,
installations?: Installation | Installation[]
): Plugin[] {
return fromJSON<Plugin[]>(addon.pluginsInstall(spec, installations ?? []))
}

/**
* Uninstall a plugin
*
* @param alias The alias or name of the plugin
* @returns An array of installed plugins
*/
export function uninstall(alias: string): Plugin[] {
return fromJSON<Plugin[]>(addon.pluginsUninstall(alias))
}

/**
* Upgrade a plugin
*
* @param spec A plugin identifier e.g. `javascript`
* @return An array of installed plugins
*/
export function upgrade(spec: string): Plugin[] {
return fromJSON<Plugin[]>(addon.pluginsUpgrade(spec))
}
12 changes: 11 additions & 1 deletion node/native/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::prelude::*;
use neon::prelude::*;
use std::sync::Mutex;
use std::sync::{Mutex, MutexGuard};
use stencila::{
config::{self, Config},
once_cell::sync::Lazy,
Expand All @@ -15,6 +15,16 @@ use stencila::{
pub static CONFIG: Lazy<Mutex<Config>> =
Lazy::new(|| Mutex::new(config::read().expect("Unable to read config")));

pub fn obtain(cx: &mut FunctionContext) -> NeonResult<MutexGuard<'static, Config>> {
match CONFIG.try_lock() {
Ok(guard) => Ok(guard),
Err(error) => cx.throw_error(format!(
"When attempting on obtain config: {}",
error.to_string()
)),
}
}

fn save_then_to_json(cx: FunctionContext, conf: Config) -> JsResult<JsString> {
let mut guard = CONFIG.lock().expect("Unable to lock config");
*guard = conf;
Expand Down
7 changes: 7 additions & 0 deletions node/native/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
use neon::prelude::*;

mod config;
mod plugins;
mod prelude;

register_module!(mut cx, {
cx.export_function("pluginsList", plugins::list)?;
cx.export_function("pluginsInstall", plugins::install)?;
cx.export_function("pluginsUninstall", plugins::uninstall)?;
cx.export_function("pluginsUpgrade", plugins::upgrade)?;

cx.export_function("configRead", config::read)?;
cx.export_function("configWrite", config::write)?;
cx.export_function("configValidate", config::validate)?;
cx.export_function("configSet", config::set)?;
cx.export_function("configReset", config::reset)?;

Ok(())
});
109 changes: 109 additions & 0 deletions node/native/src/plugins.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use crate::{
config::{self},
prelude::*,
};
use neon::{prelude::*, result::Throw};
use std::str::FromStr;
use std::sync::{Mutex, MutexGuard};
use stencila::{
config::Config,
once_cell::sync::Lazy,
plugins::{self, Installation, Plugin, Plugins},
};

/// A global plugins store
///
/// The plugins store needs to be read on startup and then passed to various
/// functions in other modules on each invocation (for delegation to each plugin).
/// As for config, we want to avoid exposing that implementation detail in these bindings
/// so have this global mutable plugins store that gets loaded when the module is loaded,
/// updated in the functions below and then passed on to other functions
pub static PLUGINS: Lazy<Mutex<Plugins>> =
Lazy::new(|| Mutex::new(Plugins::load().expect("Unable to load plugins")));

/// Obtain the plugins store
pub fn obtain(cx: &mut FunctionContext) -> NeonResult<MutexGuard<'static, Plugins>> {
match PLUGINS.try_lock() {
Ok(guard) => Ok(guard),
Err(error) => cx.throw_error(format!(
"When attempting on obtain plugins: {}",
error.to_string()
)),
}
}

/// List plugins
pub fn list(mut cx: FunctionContext) -> JsResult<JsString> {
let aliases = &config::obtain(&mut cx)?.plugins.aliases;
let plugins = &*obtain(&mut cx)?;

to_json(cx, plugins.list_plugins(aliases))
}

/// Install a plugin
pub fn install(mut cx: FunctionContext) -> JsResult<JsString> {
let spec = &cx.argument::<JsString>(0)?.value();

let config = &config::obtain(&mut cx)?;
let installs = &installations(&mut cx, 1, &config)?;
let aliases = &config.plugins.aliases;
let plugins = &mut *obtain(&mut cx)?;

match runtime(&mut cx)?
.block_on(async { Plugin::install(spec, installs, aliases, plugins, None).await })
{
Ok(_) => to_json(cx, plugins.list_plugins(aliases)),
Err(error) => cx.throw_error(error.to_string()),
}
}

/// Uninstall a plugin
pub fn uninstall(mut cx: FunctionContext) -> JsResult<JsString> {
let alias = &cx.argument::<JsString>(0)?.value();
let aliases = &config::obtain(&mut cx)?.plugins.aliases;
let plugins = &mut *obtain(&mut cx)?;

match Plugin::uninstall(alias, aliases, plugins) {
Ok(_) => to_json(cx, plugins.list_plugins(aliases)),
Err(error) => cx.throw_error(error.to_string()),
}
}

/// Upgrade a plugin
pub fn upgrade(mut cx: FunctionContext) -> JsResult<JsString> {
let spec = &cx.argument::<JsString>(0)?.value();
let config = &config::obtain(&mut cx)?;
let installs = &config.plugins.installations;
let aliases = &config.plugins.aliases;
let plugins = &mut *obtain(&mut cx)?;

match runtime(&mut cx)?
.block_on(async { Plugin::upgrade(spec, installs, aliases, plugins).await })
{
Ok(_) => to_json(cx, plugins.list_plugins(aliases)),
Err(error) => cx.throw_error(error.to_string()),
}
}

/// Get the `installations` argument, falling back to the array in `config.plugins.installations`
pub fn installations(
cx: &mut FunctionContext,
position: i32,
config: &Config,
) -> Result<Vec<Installation>, Throw> {
let arg = cx.argument::<JsArray>(position)?.to_vec(cx)?;
if arg.is_empty() {
Ok(config.plugins.installations.clone())
} else {
let mut installations = Vec::new();
for value in arg {
let str = value.to_string(cx)?.value();
let installation = match plugins::Installation::from_str(&str) {
Ok(value) => value,
Err(error) => return cx.throw_error(error.to_string()),
};
installations.push(installation)
}
Ok(installations)
}
}
10 changes: 9 additions & 1 deletion node/native/src/prelude.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use neon::{prelude::*, result::Throw};
use stencila::{
serde::{Deserialize, Serialize},
serde_json,
serde_json, tokio,
};

// We currently JSON serialize / deserialize objects when passing them to / from Rust
Expand All @@ -28,3 +28,11 @@ where
Err(error) => cx.throw_error(error.to_string()),
}
}

/// Create a async runtime to await on async functions
pub fn runtime(cx: &mut FunctionContext) -> Result<tokio::runtime::Runtime, Throw> {
match tokio::runtime::Runtime::new() {
Ok(runtime) => Ok(runtime),
Err(error) => cx.throw_error(error.to_string()),
}
}
19 changes: 19 additions & 0 deletions node/tests/plugins.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { list, install, uninstall, upgrade } from '../lib/plugins'

describe('plugins', () => {
test('list', () => {
expect(list()).toEqual(expect.arrayContaining([]))
})

test('install', () => {
expect(install('javascript')).toEqual(expect.arrayContaining([]))
})

test('uninstall', () => {
expect(uninstall('javascript')).toEqual(expect.arrayContaining([]))
})

test('upgrade', () => {
expect(upgrade('javascript')).toEqual(expect.arrayContaining([]))
})
})

0 comments on commit 71fe53d

Please sign in to comment.