#![warn(missing_docs)]
#[allow(missing_docs, clippy::all)]
pub mod bindings {
wit_bindgen::generate!({
path: "wit",
world: "rift:plugin/plugin@0.2.0",
pub_export_macro: true,
export_macro_name: "__export_rift_plugin",
default_bindings_module: "rift_plugin::bindings",
generate_all,
});
}
pub use bindings::rift::plugin::types::{
BossBarSpec, BusMessage, ChatAttempt, ChatKind, CommandSpec, Connection, Context,
DialogResponse, Hook, HttpRequest, HttpResponse, Level, Ping, PlayerInfo, PlayerRecord,
ProfileProperty, Registration, RiftEvent, RouteDecision, StatusOverride, SuggestKind,
Suggestion, Text, TitleSpec,
};
pub type Action = bindings::rift::plugin::types::Decision;
use serde::de::DeserializeOwned;
#[allow(unused_variables)]
pub trait Plugin: Default + 'static {
fn init(&mut self, config: &str) -> Result<Registration, String> {
Ok(Registration::new())
}
fn reload(&mut self, config: &str) -> Result<(), String> {
Ok(())
}
fn shutdown(&mut self, reason: &str) {}
fn route(&mut self, connection: Connection) -> RouteDecision {
RouteDecision::Default
}
fn login(&mut self, ctx: Context) -> Action {
Action::Continue
}
fn initial_server(&mut self, ctx: Context) -> Action {
Action::Continue
}
fn before_transfer(&mut self, ctx: Context) -> Action {
Action::Continue
}
fn command(&mut self, ctx: Context) -> Action {
Action::Continue
}
fn suggest(&mut self, ctx: Context) -> Vec<Suggestion> {
Vec::new()
}
fn status(&mut self, ping: Ping) -> Option<StatusOverride> {
None
}
fn http(&mut self, request: HttpRequest) -> Option<HttpResponse> {
None
}
fn chat(&mut self, attempt: ChatAttempt) -> Action {
Action::Continue
}
fn kick(&mut self, ctx: Context) -> Action {
Action::Continue
}
fn join(&mut self, ctx: Context) {}
fn after_transfer(&mut self, ctx: Context) {}
fn disconnect(&mut self, ctx: Context) {}
fn message(&mut self, message: BusMessage) {}
fn timer(&mut self, handle: u64) {}
fn event(&mut self, event: RiftEvent) {}
fn dialog(&mut self, response: DialogResponse) {}
fn job_done(&mut self, handle: u64, outcome: Result<Vec<u8>, String>) {}
fn work_init(&mut self, config: &str) -> Result<(), String> {
Ok(())
}
fn work(&mut self, payload: Vec<u8>) -> Result<Vec<u8>, String> {
Err("this plugin has no background work".into())
}
}
#[macro_export]
macro_rules! export {
($plugin:ty) => {
#[doc(hidden)]
type __RiftPluginExport = $crate::__private::Adapter<$plugin>;
$crate::bindings::__export_rift_plugin!(__RiftPluginExport with_types_in $crate::bindings);
};
}
pub fn config<T: DeserializeOwned>(json: &str) -> Result<T, String> {
serde_json::from_str(json).map_err(|error| format!("invalid plugin configuration: {error}"))
}
impl Text {
pub fn plain(text: impl Into<String>) -> Self {
Self::Plain(text.into())
}
pub fn json(json: impl Into<String>) -> Self {
Self::Json(json.into())
}
}
impl From<&str> for Text {
fn from(text: &str) -> Self {
Self::Plain(text.to_owned())
}
}
impl From<String> for Text {
fn from(text: String) -> Self {
Self::Plain(text)
}
}
impl From<&String> for Text {
fn from(text: &String) -> Self {
Self::Plain(text.clone())
}
}
impl From<serde_json::Value> for Text {
fn from(component: serde_json::Value) -> Self {
Self::Json(component.to_string())
}
}
impl From<&serde_json::Value> for Text {
fn from(component: &serde_json::Value) -> Self {
Self::Json(component.to_string())
}
}
impl PartialEq for Text {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Plain(a), Self::Plain(b)) | (Self::Json(a), Self::Json(b)) => a == b,
_ => false,
}
}
}
impl Registration {
pub fn new() -> Self {
Self {
commands: Vec::new(),
hooks: Vec::new(),
}
}
pub fn command(mut self, command: CommandSpec) -> Self {
self.commands.push(command);
self
}
pub fn hook(mut self, hook: Hook) -> Self {
self.hooks.push(hook);
self
}
pub fn hooks(mut self, hooks: impl IntoIterator<Item = Hook>) -> Self {
self.hooks.extend(hooks);
self
}
}
impl Default for Registration {
fn default() -> Self {
Self::new()
}
}
impl CommandSpec {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
aliases: Vec::new(),
permission: String::new(),
suggest: SuggestKind::None,
console: false,
}
}
pub fn alias(mut self, alias: impl Into<String>) -> Self {
self.aliases.push(alias.into());
self
}
pub fn aliases<S: Into<String>>(mut self, aliases: impl IntoIterator<Item = S>) -> Self {
self.aliases.extend(aliases.into_iter().map(Into::into));
self
}
pub fn permission(mut self, node: impl Into<String>) -> Self {
self.permission = node.into();
self
}
pub fn suggest(mut self, kind: SuggestKind) -> Self {
self.suggest = kind;
self
}
pub fn console(mut self) -> Self {
self.console = true;
self
}
}
impl Action {
pub fn deny(reason: impl Into<Text>) -> Self {
Self::Deny(reason.into())
}
pub fn message(text: impl Into<Text>) -> Self {
Self::Message(text.into())
}
pub fn server(name: impl Into<String>) -> Self {
Self::Server(name.into())
}
pub fn queue(name: impl Into<String>) -> Self {
Self::Queue(name.into())
}
}
impl PartialEq for Action {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Continue, Self::Continue) | (Self::LeaveQueue, Self::LeaveQueue) => true,
(Self::Deny(a), Self::Deny(b)) | (Self::Message(a), Self::Message(b)) => a == b,
(Self::Server(a), Self::Server(b)) | (Self::Queue(a), Self::Queue(b)) => a == b,
_ => false,
}
}
}
impl DialogResponse {
pub fn values<T: DeserializeOwned>(&self) -> Result<T, String> {
serde_json::from_str(&self.values_json)
.map_err(|error| format!("invalid dialog values: {error}"))
}
pub fn payload<T: DeserializeOwned>(&self) -> Result<Option<T>, String> {
self.payload_json
.as_deref()
.map(serde_json::from_str)
.transpose()
.map_err(|error| format!("invalid dialog payload: {error}"))
}
}
impl Suggestion {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
tooltip: None,
}
}
pub fn tooltip(mut self, tooltip: impl Into<Text>) -> Self {
self.tooltip = Some(tooltip.into());
self
}
}
impl Default for StatusOverride {
fn default() -> Self {
Self {
motd: None,
version: None,
protocol: None,
online: None,
max_players: None,
sample: None,
favicon: None,
}
}
}
impl TitleSpec {
pub fn new(title: impl Into<Text>) -> Self {
Self {
title: Some(title.into()),
subtitle: None,
fade_in: 10,
stay: 70,
fade_out: 20,
}
}
pub fn subtitle_only(subtitle: impl Into<Text>) -> Self {
Self {
title: None,
subtitle: Some(subtitle.into()),
fade_in: 10,
stay: 70,
fade_out: 20,
}
}
pub fn subtitle(mut self, subtitle: impl Into<Text>) -> Self {
self.subtitle = Some(subtitle.into());
self
}
pub fn times(mut self, fade_in: i32, stay: i32, fade_out: i32) -> Self {
self.fade_in = fade_in;
self.stay = stay;
self.fade_out = fade_out;
self
}
}
pub mod players {
use crate::Text;
use crate::bindings::rift::plugin::players as host;
pub use host::{
boss_bar, clear_dialog, dialog, get, list, lookup, names, search, set_language, title,
transfer,
};
pub fn message(target: &str, text: impl Into<Text>) -> Result<bool, String> {
host::message(target, &text.into())
}
pub fn action_bar(target: &str, text: impl Into<Text>) -> Result<bool, String> {
host::action_bar(target, &text.into())
}
pub fn kick(target: &str, reason: impl Into<Text>) -> Result<bool, String> {
host::kick(target, &reason.into())
}
pub fn broadcast(text: impl Into<Text>) -> Result<u32, String> {
host::broadcast(&text.into())
}
pub fn tab_list(
target: &str,
header: Option<Text>,
footer: Option<Text>,
) -> Result<bool, String> {
host::tab_list(target, header.as_ref(), footer.as_ref())
}
pub fn show_dialog(target: &str, dialog: &serde_json::Value) -> Result<bool, String> {
self::dialog(target, &dialog.to_string())
}
}
pub mod store {
pub use crate::bindings::rift::plugin::store::{delete, get, increment, list, set};
use serde::Serialize;
use serde::de::DeserializeOwned;
pub fn get_json<T: DeserializeOwned>(namespace: &str, key: &str) -> Result<Option<T>, String> {
match get(namespace, key)? {
Some(bytes) => serde_json::from_slice(&bytes)
.map(Some)
.map_err(|error| format!("invalid JSON in {namespace}/{key}: {error}")),
None => Ok(None),
}
}
pub fn set_json<T: Serialize + ?Sized>(
namespace: &str,
key: &str,
value: &T,
) -> Result<(), String> {
let bytes = serde_json::to_vec(value).map_err(|error| error.to_string())?;
set(namespace, key, &bytes)
}
}
pub mod permissions {
pub use crate::bindings::rift::plugin::permissions::{
add_group, groups, has, list_groups, meta, remove_group, set, set_meta,
};
pub const VISIBLE_TO: &str = "rift.visible-to";
}
pub mod bus {
use crate::BusMessage;
pub use crate::bindings::rift::plugin::bus::{
publish, request, stream_publish, subscribe, unsubscribe,
};
pub fn reply(message: &BusMessage, payload: &[u8]) -> Result<Option<u32>, String> {
match &message.reply {
Some(subject) => publish(subject, payload, None).map(Some),
None => Ok(None),
}
}
}
pub mod servers {
pub use crate::bindings::rift::plugin::servers::{
control, create, download, remove, start, stop,
};
}
pub mod timers {
pub use crate::bindings::rift::plugin::timers::{after, cancel, every};
}
pub mod background {
use crate::Action;
use crate::bindings::rift::plugin::background as host;
pub use host::{defer, is_worker, run};
pub fn resolve(ticket: u64, action: Action) -> Result<bool, String> {
host::resolve(ticket, &action)
}
}
pub mod meta {
pub use crate::bindings::rift::plugin::meta::{
api, clock, config_dir, data_dir, name, time, version,
};
}
pub mod log {
pub use crate::bindings::rift::plugin::log::log;
#[doc(no_inline)]
pub use ::log::{debug, error, info, trace, warn};
use crate::Level;
struct HostLogger;
static LOGGER: HostLogger = HostLogger;
impl ::log::Log for HostLogger {
fn enabled(&self, _metadata: &::log::Metadata) -> bool {
true
}
fn log(&self, record: &::log::Record) {
let level = match record.level() {
::log::Level::Trace => Level::Trace,
::log::Level::Debug => Level::Debug,
::log::Level::Info => Level::Info,
::log::Level::Warn => Level::Warn,
::log::Level::Error => Level::Error,
};
match record.args().as_str() {
Some(message) => log(level, message),
None => log(level, &record.args().to_string()),
}
}
fn flush(&self) {}
}
pub(crate) fn install() {
if ::log::set_logger(&LOGGER).is_ok() {
::log::set_max_level(::log::LevelFilter::Trace);
}
}
}
pub mod text {
use serde_json::{Map, Value};
use std::fmt;
pub fn mini(markup: impl Into<String>) -> Mini {
Mini {
markup: markup.into(),
args: Map::new(),
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Mini {
markup: String,
args: Map<String, Value>,
}
impl Mini {
pub fn arg(mut self, name: impl Into<String>, text: impl Into<String>) -> Self {
self.args.insert(name.into(), Value::String(text.into()));
self
}
pub fn arg_component(
mut self,
name: impl Into<String>,
component: impl Into<Value>,
) -> Self {
self.args.insert(name.into(), component.into());
self
}
pub fn to_value(&self) -> Value {
let mut object = Map::new();
object.insert("mini".into(), Value::String(self.markup.clone()));
if !self.args.is_empty() {
object.insert("args".into(), Value::Object(self.args.clone()));
}
Value::Object(object)
}
}
pub fn legacy(text: impl Into<String>) -> Legacy {
Legacy {
text: text.into(),
marker: '&',
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Legacy {
text: String,
marker: char,
}
impl Legacy {
pub fn marker(mut self, marker: char) -> Self {
self.marker = marker;
self
}
pub fn to_value(&self) -> Value {
let mut object = Map::new();
object.insert("legacy".into(), Value::String(self.text.clone()));
object.insert("marker".into(), Value::String(self.marker.to_string()));
Value::Object(object)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Component(Map<String, Value>);
impl Component {
pub fn text(text: impl Into<String>) -> Self {
let mut fields = Map::new();
fields.insert("text".into(), Value::String(text.into()));
Self(fields)
}
pub fn color(self, color: impl Into<String>) -> Self {
self.field("color", color.into())
}
pub fn bold(self) -> Self {
self.field("bold", true)
}
pub fn italic(self) -> Self {
self.field("italic", true)
}
pub fn underlined(self) -> Self {
self.field("underlined", true)
}
pub fn strikethrough(self) -> Self {
self.field("strikethrough", true)
}
pub fn obfuscated(self) -> Self {
self.field("obfuscated", true)
}
fn click(self, action: &str, key: &str, value: impl Into<String>) -> Self {
let mut click = Map::new();
click.insert("action".into(), Value::String(action.into()));
click.insert(key.into(), Value::String(value.into()));
self.field("click_event", Value::Object(click))
}
pub fn run_command(self, command: impl Into<String>) -> Self {
self.click("run_command", "command", command)
}
pub fn suggest_command(self, command: impl Into<String>) -> Self {
self.click("suggest_command", "command", command)
}
pub fn open_url(self, url: impl Into<String>) -> Self {
self.click("open_url", "url", url)
}
pub fn copy_to_clipboard(self, text: impl Into<String>) -> Self {
self.click("copy_to_clipboard", "value", text)
}
pub fn hover_text(self, text: impl Into<String>) -> Self {
self.hover(Value::String(text.into()))
}
pub fn hover(self, component: impl Into<Value>) -> Self {
let mut hover = Map::new();
hover.insert("action".into(), Value::String("show_text".into()));
hover.insert("value".into(), component.into());
self.field("hover_event", Value::Object(hover))
}
pub fn insertion(self, text: impl Into<String>) -> Self {
self.field("insertion", text.into())
}
pub fn extra(mut self, child: impl Into<Value>) -> Self {
let child = child.into();
match self.0.get_mut("extra") {
Some(Value::Array(children)) => children.push(child),
_ => {
self.0.insert("extra".into(), Value::Array(vec![child]));
}
}
self
}
pub fn field(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.0.insert(key.into(), value.into());
self
}
pub fn to_value(&self) -> Value {
Value::Object(self.0.clone())
}
}
macro_rules! json_text {
($($builder:ty),*) => {$(
impl fmt::Display for $builder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.to_value(), f)
}
}
impl From<$builder> for Value {
fn from(builder: $builder) -> Value {
builder.to_value()
}
}
impl From<$builder> for crate::Text {
fn from(builder: $builder) -> crate::Text {
crate::Text::Json(builder.to_string())
}
}
impl From<&$builder> for crate::Text {
fn from(builder: &$builder) -> crate::Text {
crate::Text::Json(builder.to_string())
}
}
)*};
}
json_text!(Mini, Legacy, Component);
}
#[doc(hidden)]
pub mod __private {
use crate::bindings::exports::rift::plugin::handler::Guest;
use crate::*;
use std::any::Any;
use std::cell::RefCell;
use std::marker::PhantomData;
thread_local! {
static INSTANCE: RefCell<Option<Box<dyn Any>>> = const { RefCell::new(None) };
}
fn with<P: Plugin, R>(f: impl FnOnce(&mut P) -> R) -> R {
INSTANCE.with(|slot| {
let mut slot = slot.borrow_mut();
let plugin = slot
.get_or_insert_with(|| Box::new(P::default()))
.downcast_mut::<P>()
.expect("one plugin type per component");
f(plugin)
})
}
pub struct Adapter<P>(PhantomData<P>);
impl<P: Plugin> Guest for Adapter<P> {
fn init(config_json: String) -> Result<Registration, String> {
crate::log::install();
INSTANCE.with(|slot| *slot.borrow_mut() = Some(Box::new(P::default())));
with(|p: &mut P| p.init(&config_json))
}
fn reload(config_json: String) -> Result<(), String> {
with(|p: &mut P| p.reload(&config_json))
}
fn shutdown(reason: String) {
with(|p: &mut P| p.shutdown(&reason));
let instance = INSTANCE.with(|slot| slot.borrow_mut().take());
drop(instance);
}
fn route(connection: Connection) -> RouteDecision {
with(|p: &mut P| p.route(connection))
}
fn login(ctx: Context) -> Action {
with(|p: &mut P| p.login(ctx))
}
fn initial_server(ctx: Context) -> Action {
with(|p: &mut P| p.initial_server(ctx))
}
fn before_transfer(ctx: Context) -> Action {
with(|p: &mut P| p.before_transfer(ctx))
}
fn command(ctx: Context) -> Action {
with(|p: &mut P| p.command(ctx))
}
fn suggest(ctx: Context) -> Vec<Suggestion> {
with(|p: &mut P| p.suggest(ctx))
}
fn status(ping: Ping) -> Option<StatusOverride> {
with(|p: &mut P| p.status(ping))
}
fn http(request: HttpRequest) -> Option<HttpResponse> {
with(|p: &mut P| p.http(request))
}
fn chat(attempt: ChatAttempt) -> Action {
with(|p: &mut P| p.chat(attempt))
}
fn kick(ctx: Context) -> Action {
with(|p: &mut P| p.kick(ctx))
}
fn join(ctx: Context) {
with(|p: &mut P| p.join(ctx))
}
fn after_transfer(ctx: Context) {
with(|p: &mut P| p.after_transfer(ctx))
}
fn disconnect(ctx: Context) {
with(|p: &mut P| p.disconnect(ctx))
}
fn message(message: BusMessage) {
with(|p: &mut P| p.message(message))
}
fn timer(handle: u64) {
with(|p: &mut P| p.timer(handle))
}
fn event(event: RiftEvent) {
with(|p: &mut P| p.event(event))
}
fn dialog(response: DialogResponse) {
with(|p: &mut P| p.dialog(response))
}
fn job_done(handle: u64, outcome: Result<Vec<u8>, String>) {
with(|p: &mut P| p.job_done(handle, outcome))
}
fn work_init(config_json: String) -> Result<(), String> {
crate::log::install();
with(|p: &mut P| p.work_init(&config_json))
}
fn work(payload: Vec<u8>) -> Result<Vec<u8>, String> {
crate::log::install();
with(|p: &mut P| p.work(payload))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn builders_fill_the_wit_records() {
let registration = Registration::new()
.command(
CommandSpec::new("hello")
.alias("hi")
.aliases(["hey", "yo"])
.permission("hello.use")
.suggest(SuggestKind::Players)
.console(),
)
.command(CommandSpec::new("plain"))
.hooks([Hook::Login, Hook::Command])
.hook(Hook::Chat);
let hello = ®istration.commands[0];
assert_eq!(hello.name, "hello");
assert_eq!(hello.aliases, ["hi", "hey", "yo"]);
assert_eq!(hello.permission, "hello.use");
assert!(matches!(hello.suggest, SuggestKind::Players));
assert!(hello.console);
let plain = ®istration.commands[1];
assert!(plain.aliases.is_empty() && plain.permission.is_empty() && !plain.console);
assert!(matches!(plain.suggest, SuggestKind::None));
assert_eq!(
registration.hooks,
vec![Hook::Login, Hook::Command, Hook::Chat]
);
assert!(Registration::default().commands.is_empty());
assert!(Registration::default().hooks.is_empty());
}
#[test]
fn strings_are_plain_and_builders_are_json() {
assert_eq!(Text::from("<red>hi"), Text::Plain("<red>hi".into()));
assert_eq!(Text::from(String::from("a")), Text::plain("a"));
assert_eq!(Text::from(&String::from("a")), Text::plain("a"));
assert_eq!(
Text::from(text::mini("<red>x")),
Text::Json(r#"{"mini":"<red>x"}"#.into())
);
assert_eq!(
Text::from(text::legacy("&cx")),
Text::json(r#"{"legacy":"&cx","marker":"&"}"#)
);
assert_eq!(
Text::from(&text::Component::text("x").bold()),
Text::json(r#"{"bold":true,"text":"x"}"#)
);
assert_eq!(
Text::from(json!({"text": "x"})),
Text::json(r#"{"text":"x"}"#)
);
assert_eq!(Text::from(&json!("a\"b")), Text::json(r#""a\"b""#));
assert_ne!(Text::plain("x"), Text::json("x"));
}
#[test]
fn actions_take_any_text() {
assert_eq!(Action::deny("no"), Action::Deny(Text::Plain("no".into())));
assert_eq!(
Action::deny(json!({"text": "Banned", "color": "red"})),
Action::Deny(Text::Json(r#"{"color":"red","text":"Banned"}"#.into()))
);
assert_eq!(
Action::message(format!("Hi, {}!", "Alex")),
Action::Message(Text::Plain("Hi, Alex!".into()))
);
assert_eq!(
Action::message(text::mini("<gold>Hi")),
Action::Message(Text::Json(r#"{"mini":"<gold>Hi"}"#.into()))
);
assert_eq!(Action::server("lobby"), Action::Server("lobby".into()));
assert_eq!(Action::queue("game"), Action::Queue("game".into()));
assert_ne!(Action::server("a"), Action::queue("a"));
assert_ne!(Action::deny("a"), Action::message("a"));
assert_ne!(Action::Continue, Action::LeaveQueue);
}
#[test]
fn titles_suggestions_and_status_take_any_text() {
let title = TitleSpec::new(text::mini("<red>T"))
.subtitle("sub")
.times(1, 2, 3);
assert_eq!(title.title, Some(Text::json(r#"{"mini":"<red>T"}"#)));
assert_eq!(title.subtitle, Some(Text::plain("sub")));
assert_eq!((title.fade_in, title.stay, title.fade_out), (1, 2, 3));
let plain = TitleSpec::new("T");
assert_eq!(plain.subtitle, None);
assert_eq!((plain.fade_in, plain.stay, plain.fade_out), (10, 70, 20));
let subtitle = TitleSpec::subtitle_only("only").times(0, 20, 0);
assert_eq!(subtitle.title, None);
assert_eq!(subtitle.subtitle, Some(Text::plain("only")));
assert_eq!(subtitle.stay, 20);
assert_eq!(Suggestion::new("a").tooltip, None);
assert_eq!(
Suggestion::new("a").tooltip("tip").tooltip,
Some(Text::plain("tip"))
);
assert_eq!(
Suggestion::new("a").tooltip(text::legacy("&7tip")).tooltip,
Some(Text::json(r#"{"legacy":"&7tip","marker":"&"}"#))
);
let status = StatusOverride {
version: Some("Rift".into()),
sample: Some(vec!["one".into(), text::mini("<green>two").into()]),
..Default::default()
};
assert_eq!(status.version, Some(Text::plain("Rift")));
assert_eq!(status.sample.map(|lines| lines.len()), Some(2));
let nothing = StatusOverride::default();
assert!(nothing.motd.is_none() && nothing.version.is_none() && nothing.sample.is_none());
assert!(nothing.protocol.is_none() && nothing.online.is_none());
assert!(nothing.max_players.is_none() && nothing.favicon.is_none());
}
#[test]
fn hooks_have_defaults() {
#[derive(Default)]
struct Nothing;
impl Plugin for Nothing {}
let mut plugin = Nothing;
let registration = plugin.init("{}").unwrap();
assert!(registration.commands.is_empty() && registration.hooks.is_empty());
assert_eq!(plugin.work_init("{}"), Ok(()));
assert_eq!(
plugin.work(b"job".to_vec()),
Err("this plugin has no background work".to_owned())
);
plugin.job_done(1, Ok(Vec::new()));
plugin.job_done(2, Err("failed".into()));
}
#[test]
fn text_builders_write_escaped_json() {
let welcome = text::mini("<gold>Hi \"<player>\"</gold>")
.arg("player", "St<b>eve\n")
.arg_component("rank", text::legacy("&cAdmin"));
let value: serde_json::Value = serde_json::from_str(&welcome.to_string()).unwrap();
assert_eq!(
value,
json!({
"mini": "<gold>Hi \"<player>\"</gold>",
"args": {"player": "St<b>eve\n", "rank": {"legacy": "&cAdmin", "marker": "&"}},
})
);
assert_eq!(text::mini("<red>x").to_string(), r#"{"mini":"<red>x"}"#);
assert_eq!(
text::legacy("§aHi").marker('§').to_string(),
r#"{"legacy":"§aHi","marker":"§"}"#
);
let component = text::Component::text("[a\"b]")
.color("#ff8800")
.bold()
.italic()
.underlined()
.strikethrough()
.obfuscated()
.suggest_command("/msg ")
.hover(text::mini("<green>Hover"))
.insertion("ins")
.extra("plain")
.extra(text::Component::text("child").open_url("https://example.org"))
.field("font", "minecraft:uniform");
let value: serde_json::Value = serde_json::from_str(&component.to_string()).unwrap();
assert_eq!(
value,
json!({
"text": "[a\"b]", "color": "#ff8800", "bold": true, "italic": true,
"underlined": true, "strikethrough": true, "obfuscated": true,
"click_event": {"action": "suggest_command", "command": "/msg "},
"hover_event": {"action": "show_text", "value": {"mini": "<green>Hover"}},
"insertion": "ins",
"extra": ["plain", {"text": "child", "click_event": {"action": "open_url", "url": "https://example.org"}}],
"font": "minecraft:uniform",
})
);
let value: serde_json::Value = text::Component::text("c")
.copy_to_clipboard("v")
.run_command("/x")
.hover_text("t")
.into();
assert_eq!(
value["click_event"],
json!({"action": "run_command", "command": "/x"})
);
assert_eq!(
value["hover_event"],
json!({"action": "show_text", "value": "t"})
);
}
#[test]
fn config_decodes_json() {
#[derive(serde::Deserialize)]
struct Config {
#[serde(default)]
deny: Option<String>,
}
let config: Config = config(r#"{"deny":"Mallory"}"#).unwrap();
assert_eq!(config.deny.as_deref(), Some("Mallory"));
let empty: Config = super::config("{}").unwrap();
assert_eq!(empty.deny, None);
assert!(super::config::<Config>("nope").is_err());
}
}