Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start generating documentation #6

Merged
merged 1 commit into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ members = ["krpc-build"]

[package]
name = "krpc-client"
version = "0.4.0-0.5.1-1.12.5"
version = "0.4.1-0.5.1-1.12.5"
authors = ["Kyle Ladd <[email protected]>"]
edition = "2021"

[features]
docs = ["krpc-build/docs"]

[dependencies]
thiserror = "1.0"
protobuf = { version = "3.2.0", features = ["bytes"] }
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Rust client for [kRPC](https://github.com/krpc/krpc) (Remote Procedure Calls for

Work in progress. Bug-reports and contributions welcome. All procedures seem to work, but more testing is needed. Streams work, but Events are still on the way.

```toml
krpc-client = { git = "https://github.com/kladd/krpc-client" }
```

### Examples

Greet the crew with standard procedure calls.
Expand Down Expand Up @@ -49,6 +53,9 @@ for _ in 0..10 {
}
```

### Features
* `docs`: Include documentation with generated RPC types/procedures. Only services are documented right now.

### Hacking

* `krpc-client/client.rs` contains basic connection, request, and response handling.
Expand Down
6 changes: 5 additions & 1 deletion krpc-build/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
[package]
name = "krpc-build"
version = "0.4.0-0.5.1-1.12.5"
version = "0.4.1-0.5.1-1.12.5"
edition = "2021"

[features]
docs = ["dep:xml-rs"]

[dependencies]
serde = "1.0"
serde_json = "1.0"
xml-rs = { version = "0.8.4", optional = true }
convert_case = "0.6.0"
codegen = "0.2.0"
203 changes: 203 additions & 0 deletions krpc-build/src/doc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
use xml::{
attribute::OwnedAttribute, name::OwnedName, reader::XmlEvent, EventReader,
};

struct DocContext {
stack: Vec<DocType>,
section: DocSection,
}

#[derive(Debug, PartialEq)]
enum DocSection {
None,
Parameters,
Returns,
Remarks,
}

impl DocContext {
fn open_element(&mut self, doc: DocType) {
match doc {
DocType::Parameter { .. } => {
self.push_section_maybe(DocSection::Parameters)
}
DocType::Returns(_) => self.push_section_maybe(DocSection::Returns),
DocType::Remarks(_) => self.push_section_maybe(DocSection::Remarks),
_ => {}
};
self.stack.push(doc);
}

fn close_element(&mut self) -> Option<String> {
let end = self.stack.pop().expect("context already closed");
if let Some(parent) = self.stack.last_mut() {
parent.push_str(&end.to_string());
}

// Consume pseudo elements.
while let Some(DocType::Section(_)) = self.stack.last() {
self.close_element();
}

// If we've popped back to the root, we're done. Shovel it.
if self.stack.is_empty() {
Some(end.to_string())
} else {
None
}
}

fn push_str(&mut self, str: &str) {
self.stack
.last_mut()
.expect("writing to an empty context")
.push_str(str);
}

fn push_section_maybe(&mut self, section: DocSection) {
if section != self.section {
self.stack
.push(DocType::Section(format!("# {:?}\n", section)));
self.section = section;
}
}
}

enum DocType {
Code(String),
Doc(String),
Link { href: String, label: String },
Math(String),
ParamRef(String),
Parameter { name: String, label: String },
Remarks(String),
Returns(String),
Section(String),
See { cref: String },
Summary(String),
}

impl DocType {
fn from_event(name: OwnedName, attrs: Vec<OwnedAttribute>) -> Self {
match name.to_string().as_ref() {
"summary" => Self::Summary(String::new()),
"a" => Self::Link {
href: Self::find_attr("href", attrs)
.expect("a tag with no href"),
label: String::new(),
},
"c" => Self::Code(String::new()),
"doc" => Self::Doc(String::new()),
"param" => Self::Parameter {
name: Self::find_attr("name", attrs)
.expect("param tag with no name"),
label: String::new(),
},
"paramref" => Self::ParamRef(
Self::find_attr("name", attrs)
.expect("paramref tag with no name"),
),
"returns" => Self::Returns(String::new()),
"remarks" => Self::Remarks(String::new()),
"see" => Self::See {
cref: Self::find_attr("cref", attrs).expect("see with no cref"),
},
"math" => Self::Math(String::new()),
_ => panic!("Unrecognized doc element: {}", name),
}
}

fn push_str(&mut self, str: &str) {
match self {
Self::Summary(s)
| Self::Doc(s)
| Self::Returns(s)
| Self::ParamRef(s)
| Self::Remarks(s)
| Self::Code(s)
| Self::Math(s)
| Self::Section(s) => s.push_str(str),
Self::Link { label, .. } | Self::Parameter { label, .. } => {
label.push_str(str)
}
Self::See { cref } => cref.push_str(str),
};
}

fn find_attr(key: &str, attrs: Vec<OwnedAttribute>) -> Option<String> {
attrs
.iter()
.find(|attr| attr.name.to_string() == key)
.map(|attr| attr.value.to_string())
}
}

impl ToString for DocType {
fn to_string(&self) -> String {
match self {
Self::Summary(s)
| Self::Doc(s)
| Self::Returns(s)
| Self::Remarks(s)
// TODO: Format math.
| Self::Math(s)
| Self::Section(s) => s.to_owned(),
Self::Link { href, label } => format!("[{label}]({href})"),
Self::Parameter { name, label } => format!(" - `{name}`: {label}"),
Self::ParamRef(s) | Self::Code(s) => format!("`{s}`"),
// TODO: Actually reference the generated procedure definition.
Self::See { cref } => format!("`{}`", cref.replace("M:", "")),
}
}
}

pub fn parse_doc(xml: &str) -> String {
let parser = EventReader::new(xml.as_bytes());

let mut ctx = DocContext {
stack: Vec::new(),
section: DocSection::None,
};

for event in parser {
match event {
Ok(XmlEvent::StartElement {
name, attributes, ..
}) => ctx.open_element(DocType::from_event(name, attributes)),
Ok(XmlEvent::Whitespace(s)) | Ok(XmlEvent::Characters(s)) => {
ctx.push_str(&s);
}
Ok(XmlEvent::EndElement { .. }) => {
if let Some(done) = ctx.close_element() {
return done;
}
}
_ => continue,
};
}

unreachable!();
}

#[cfg(test)]
mod tests {
use crate::doc::parse_doc;

#[test]
fn test_parse_doc() {
let sample = "<doc>\n<summary>\nThis service provides functionality to interact with\n<a href=\"https://forum.kerbalspaceprogram.com/index.php?/topic/184787-infernal-robotics-next/\">Infernal Robotics</a>.\n</summary>\n</doc>";
dbg!(parse_doc(sample));
}

#[test]
fn test_parse_params() {
let sample = "<doc>\n<summary>\nConstruct a tuple.\n</summary>\n<returns>The tuple.</returns>\n<param name=\"elements\">The elements.</param>\n</doc>";
dbg!(parse_doc(sample));
}

#[test]
fn test_parse_paramref_code() {
let sample = "<doc>\n<summary>\nReturns the servo group in the given <paramref name=\"vessel\" /> with the given <paramref name=\"name\" />,\nor <c>null</c> if none exists. If multiple servo groups have the same name, only one of them is returned.\n</summary>\n<param name=\"vessel\">Vessel to check.</param>\n<param name=\"name\">Name of servo group to find.</param>\n</doc>";
dbg!(parse_doc(sample));
}
}
34 changes: 28 additions & 6 deletions krpc-build/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#[cfg(feature = "docs")]
mod doc;

use std::{fs, io::Error, path::Path};

use codegen::Function;
Expand All @@ -10,12 +13,12 @@ use serde_json::{Map, Value};
/// type and function definitions for that service.
///
/// # Examples
/// ```
/// ```json
/// {
/// "SpaceCenter": {
/// "procedures": {
/// "get_ActiveVessel": {
/// "paramters": [],
/// "parameters": [],
/// "return_type": {
/// "code": "CLASS",
/// "service": "SpaceCenter",
Expand All @@ -27,7 +30,7 @@ use serde_json::{Map, Value};
/// }
/// ```
/// becomes
/// ```
/// ```rust
/// use std::sync::Arc;
///
/// use crate::{client::Client, error::RpcError, schema::rpc_object};
Expand All @@ -54,7 +57,7 @@ pub fn build<O: std::io::Write>(
let json: serde_json::Value = serde_json::from_reader(def_file)?;

for (name, props_json) in json.as_object().unwrap().into_iter() {
let mut service = RpcService::new(&mut scope, name);
let mut service = RpcService::new(&mut scope, name, props_json);

let props = props_json.as_object().unwrap();

Expand Down Expand Up @@ -101,7 +104,11 @@ impl<'a> RpcService<'a> {
("crate::error", "RpcError"),
];

fn new(scope: &'a mut codegen::Scope, service_name: &str) -> Self {
fn new(
scope: &'a mut codegen::Scope,
service_name: &str,
_props_json: &Value,
) -> Self {
let module = scope
.new_module(&service_name.to_case(Case::Snake))
.attr("allow(clippy::type_complexity)")
Expand All @@ -111,12 +118,17 @@ impl<'a> RpcService<'a> {
module.import(path, type_name);
}

module
let _struct_def = module
.new_struct(service_name)
.vis("pub")
.field("pub client", "::std::sync::Arc<crate::client::Client>")
.allow("dead_code");

#[cfg(feature = "docs")]
if let Some(xml) = get_documentation(_props_json) {
_struct_def.doc(&doc::parse_doc(&xml));
}

// TODO: Remove new? Or derive it.
module
.new_impl(service_name)
Expand Down Expand Up @@ -285,6 +297,16 @@ fn fn_set_args(fn_block: &mut Function, definition: &Value) -> RpcArgs {
}
}

#[cfg(feature = "docs")]
fn get_documentation(definition: &Value) -> Option<String> {
definition
.as_object()
.unwrap()
.get("documentation")
.and_then(Value::as_str)
.map(String::from)
}

fn get_return_type(definition: &Value) -> String {
let mut ret = String::from("()");
if let Some(return_value) = definition.get("return_type") {
Expand Down