Skip to content

Commit

Permalink
feat(Web client): Expose load and dump functions for documents
Browse files Browse the repository at this point in the history
  • Loading branch information
nokome authored and alex-ketch committed Feb 16, 2022
1 parent 2804b3a commit cacbb15
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 35 deletions.
24 changes: 24 additions & 0 deletions fixtures/nodes/creative-work.json
@@ -0,0 +1,24 @@
{
"type": "CreativeWork",
"content": [
{
"type": "Paragraph",
"content": [
"A fixture that is a creative work containing code nodes. ",
"Why? Because in some circumstances, instead of nested, structured content (e.g. an `Article`), ",
"we want to use a flat array of nodes."
]
},
{
"type": "CodeChunk",
"id": "cc-1",
"text": "# Some code",
"outputs": [
{
"type": "ImageObject",
"contentUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mNkYvhfz0AEYBxVSF+FAA/eDwup9bzCAAAAAElFTkw"
}
]
}
]
}
2 changes: 1 addition & 1 deletion node/src/documents.rs
Expand Up @@ -129,7 +129,7 @@ pub fn dump(mut cx: FunctionContext) -> JsResult<JsString> {

let result = RUNTIME.block_on(async {
match DOCUMENTS.get(id).await {
Ok(document) => document.lock().await.dump(format).await,
Ok(document) => document.lock().await.dump(format, None).await,
Err(error) => Err(error),
}
});
Expand Down
20 changes: 15 additions & 5 deletions rust/stencila/src/documents.rs
Expand Up @@ -12,6 +12,7 @@ use node_execute::{
ExecuteRequest, ExecuteResponse, PatchRequest, PatchResponse, RequestId,
};
use node_patch::{apply, diff, merge, Patch};
use node_pointer::resolve;
use node_reshape::reshape;
use notify::DebouncedEvent;
use once_cell::sync::Lazy;
Expand Down Expand Up @@ -644,7 +645,7 @@ impl Document {
let content_to_write = if let Some(input_format) = format.as_ref() {
let input_format = formats::match_path(&input_format).spec();
if input_format != self.format {
self.dump(None).await?
self.dump(None, None).await?
} else {
self.content.clone()
}
Expand Down Expand Up @@ -708,15 +709,24 @@ impl Document {
///
/// - `format`: the format to dump the content as; if not supplied assumed to be
/// the document's existing format.
///
/// - `node_id`: the id of the node within the document to dump
#[tracing::instrument(skip(self))]
pub async fn dump(&self, format: Option<String>) -> Result<String> {
pub async fn dump(&self, format: Option<String>, node_id: Option<String>) -> Result<String> {
let format = match format {
Some(format) => format,
None => return Ok(self.content.clone()),
};

let root = &*self.root.read().await;
codecs::to_string(root, &format, None).await
if let Some(node_id) = node_id {
let address = self.addresses.read().await.get(&node_id).cloned();
let pointer = resolve(root, address, Some(node_id))?;
let node = pointer.to_node()?;
codecs::to_string(&node, &format, None).await
} else {
codecs::to_string(root, &format, None).await
}
}

/// Load content into the document
Expand Down Expand Up @@ -2326,7 +2336,7 @@ pub mod commands {
let out = output.display().to_string();
if out == "-" {
let format = self.to.clone().unwrap_or_else(|| "json".to_string());
let content = document.dump(Some(format.clone())).await?;
let content = document.dump(Some(format.clone()), None).await?;
return result::content(&format, &content);
} else {
document
Expand Down Expand Up @@ -2421,7 +2431,7 @@ pub mod commands {
None => "json".to_string(),
Some(format) => format.clone(),
};
let content = document.dump(Some(format.clone())).await?;
let content = document.dump(Some(format.clone()), None).await?;
result::content(&format, &content)
} else {
document
Expand Down
40 changes: 36 additions & 4 deletions rust/stencila/src/rpc.rs
Expand Up @@ -57,6 +57,8 @@ impl Request {
"documents.create" => documents_create(&self.params).await,
"documents.open" => documents_open(&self.params).await,
"documents.close" => documents_close(&self.params).await,
"documents.load" => documents_load(&self.params).await,
"documents.dump" => documents_dump(&self.params).await,
"documents.patch" => documents_patch(&self.params).await,
"documents.execute" => documents_execute(&self.params).await,
"documents.cancel" => documents_cancel(&self.params).await,
Expand Down Expand Up @@ -305,6 +307,36 @@ async fn documents_close(params: &Params) -> Result<(serde_json::Value, Subscrip
Ok((json!(document), Subscription::None))
}

async fn documents_load(params: &Params) -> Result<(serde_json::Value, Subscription)> {
let document_id = required_string(params, "documentId")?;
let content = required_string(params, "content")?;
let format = optional_string(params, "format")?;

DOCUMENTS
.get(&document_id)
.await?
.lock()
.await
.load(content, format)
.await?;
Ok((json!(true), Subscription::None))
}

async fn documents_dump(params: &Params) -> Result<(serde_json::Value, Subscription)> {
let document_id = required_string(params, "documentId")?;
let format = optional_string(params, "format")?;
let node_id = optional_string(params, "nodeId")?;

let content = DOCUMENTS
.get(&document_id)
.await?
.lock()
.await
.dump(format, node_id)
.await?;
Ok((json!(content), Subscription::None))
}

async fn documents_subscribe(
params: &Params,
client: &str,
Expand Down Expand Up @@ -342,7 +374,7 @@ async fn documents_patch(params: &Params) -> Result<(serde_json::Value, Subscrip
.await
.patch(patch, true, execute)
.await?;
Ok((serde_json::Value::Null, Subscription::None))
Ok((json!(true), Subscription::None))
}

async fn documents_execute(params: &Params) -> Result<(serde_json::Value, Subscription)> {
Expand All @@ -360,7 +392,7 @@ async fn documents_execute(params: &Params) -> Result<(serde_json::Value, Subscr
.await
.execute(node_id, ordering)
.await?;
Ok((serde_json::Value::Null, Subscription::None))
Ok((json!(true), Subscription::None))
}

async fn documents_cancel(params: &Params) -> Result<(serde_json::Value, Subscription)> {
Expand All @@ -378,7 +410,7 @@ async fn documents_cancel(params: &Params) -> Result<(serde_json::Value, Subscri
.await
.cancel(node_id, scope)
.await?;
Ok((serde_json::Value::Null, Subscription::None))
Ok((json!(true), Subscription::None))
}

async fn documents_restart(params: &Params) -> Result<(serde_json::Value, Subscription)> {
Expand All @@ -392,7 +424,7 @@ async fn documents_restart(params: &Params) -> Result<(serde_json::Value, Subscr
.await
.restart(kernel_id)
.await?;
Ok((serde_json::Value::Null, Subscription::None))
Ok((json!(true), Subscription::None))
}

async fn documents_kernels(params: &Params) -> Result<(serde_json::Value, Subscription)> {
Expand Down
2 changes: 1 addition & 1 deletion rust/stencila/src/server.rs
Expand Up @@ -1100,7 +1100,7 @@ async fn get_handler(
Ok(document) => {
let document = DOCUMENTS.get(&document.id).await.unwrap();
let document = document.lock().await;
let content = match document.dump(Some(format.clone())).await {
let content = match document.dump(Some(format.clone()), None).await {
Ok(content) => content,
Err(error) => {
return error_response(
Expand Down
54 changes: 53 additions & 1 deletion web/src/documents.e2e.ts
@@ -1,7 +1,16 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { article, creativeWork } from '@stencila/schema'
import { Document, DocumentEvent } from '@stencila/stencila'
import { Client, connect, disconnect } from './client'
import { close, open, subscribe, unsubscribe } from './documents'
import {
close,
create,
dump,
load,
open,
subscribe,
unsubscribe,
} from './documents'

jest.setTimeout(10000)

Expand Down Expand Up @@ -55,3 +64,46 @@ test('basic', async () => {
// Close the document
await close(client, document.id)
})

test('create', async () => {
let document = await create(client)
let json = await dump(client, document.id, 'json')
expect(json).toMatch(/^{"type":"Article"/)

document = await create(client, creativeWork({ content: ['Beep!'] }))
let md = await dump(client, document.id, 'md')
expect(md).toMatch(/^Beep!/)

document = await create(client, 'Boop!', 'md')
json = await dump(client, document.id, 'json')
expect(json).toMatch(/"content":\["Boop!"\]/)
})

test('load', async () => {
let document = await create(client)

let ok = await load(client, document.id, article({ content: [] }))
expect(ok).toBeTruthy()
let json = await dump(client, document.id, 'json')
expect(json).toMatch(/^{"type":"Article"/)

ok = await load(client, document.id, 'Hello *world*!', 'md')
expect(ok).toBeTruthy()
json = await dump(client, document.id, 'html')
expect(json).toMatch(
'<em itemtype="http://schema.stenci.la/Emphasis" itemscope><span>world</span>'
)
})

test('dump', async () => {
let document = await open(client, 'fixtures/nodes/creative-work.json')

let json = await dump(client, document.id, 'json')
expect(json).toMatch(/^{"type":"CreativeWork"/)

let md = await dump(client, document.id, 'md')
expect(md).toMatch(/^A fixture that is a creative work/)

let rpng = await dump(client, document.id, 'rpng', 'cc-1')
expect(rpng).toMatch(/^data:image\/png/)
})
100 changes: 79 additions & 21 deletions web/src/documents.ts
Expand Up @@ -66,16 +66,27 @@ export interface ContentChangeEvent extends CustomEvent {

/**
* Create a new document
*
* Optionally pass the content, in some format, for the new document.
*
* Optionally pass the content for the new document.
* If `content` is not a string and format is 'json' of undefined,
* then `content` will be stringify to JSON.
*/
export async function create(
client: Client,
content?: string,
content?: unknown,
format?: string
): Promise<Document> {
if (
content !== undefined &&
typeof content !== 'string' &&
(format === 'json' || format === undefined)
) {
content = JSON.stringify(content)
format = 'json'
}
return client.call('documents.create', {
content, format,
content,
format,
}) as Promise<Document>
}

Expand Down Expand Up @@ -106,6 +117,53 @@ export async function close(
}) as Promise<Document>
}

/**
* Load content into a document
*
* If `format` is not supplied, the content will be assumed to be the current
* format of the document e.g. `md`. If `content` is not a string and format
* is 'json' of undefined, then `content` will be stringify to JSON.
*/
export async function load(
client: Client,
documentId: DocumentId,
content: unknown,
format?: string
): Promise<Document> {
if (
typeof content !== 'string' &&
(format === 'json' || format === undefined)
) {
content = JSON.stringify(content)
format = 'json'
}
return client.call('documents.load', {
documentId,
content,
format,
}) as Promise<Document>
}

/**
* Dump all, or part, of a document to a string
*
* If `format` is not supplied, the content will be assumed to be the current
* format of the document e.g. `md`. If `nodeId` is supplied then only that
* node will be dumped.
*/
export async function dump(
client: Client,
documentId: DocumentId,
format?: string,
nodeId?: NodeId
): Promise<Document> {
return client.call('documents.dump', {
documentId,
format,
nodeId,
}) as Promise<Document>
}

/**
* Subscribe to a document topic
*/
Expand Down Expand Up @@ -440,15 +498,15 @@ async function onContentChange(
*/
export interface ValidatorChangeEvent extends CustomEvent {
detail:
| {
type: 'property'
name: string
value: string
}
| {
type: 'validator'
value: Exclude<ValidatorTypes['type'], 'Validator'>
}
| {
type: 'property'
name: string
value: string
}
| {
type: 'validator'
value: Exclude<ValidatorTypes['type'], 'Validator'>
}
}

/**
Expand All @@ -465,16 +523,16 @@ async function onValidatorChange(
const [address, value]: [Address, JsonValue] =
event.detail.type === 'property'
? // The new validator property value
[
[
// ...except for `default` which is actually a property of the parent parameter
...(event.detail.name === 'default' ? [] : ['validator']),
event.detail.name,
],
event.detail.value,
]
[
// ...except for `default` which is actually a property of the parent parameter
...(event.detail.name === 'default' ? [] : ['validator']),
event.detail.name,
],
event.detail.value,
]
: // The new validator as an object with `type`
[['validator'], { type: event.detail.value }]
[['validator'], { type: event.detail.value }]

const op: Operation = {
type: 'Replace',
Expand Down
1 change: 0 additions & 1 deletion web/src/sessions.e2e.ts
Expand Up @@ -29,7 +29,6 @@ test('basic', async () => {
expect.objectContaining({
id: expect.stringMatching(/^se-[0-9a-zA-Z]{20}/),
project: 'projectId',
snapshot: 'snapshotId',
status: 'Started',
})
)
Expand Down

0 comments on commit cacbb15

Please sign in to comment.