use schemars::Schema;
use semver::{BuildMetadata, Prerelease, Version};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use url::Url;
pub use resolver::{resolve_within, BrokenReference};
pub mod resolved;
mod resolver;
pub const OPEN_RPC_SPECIFICATION_VERSION: Version = Version {
major: 1,
minor: 3,
patch: 2,
pre: Prerelease::EMPTY,
build: BuildMetadata::EMPTY,
};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenRPC {
pub openrpc: Version,
pub info: Info,
#[serde(skip_serializing_if = "Option::is_none")]
pub servers: Option<Vec<Server>>,
pub methods: Vec<ReferenceOr<Method>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub components: Option<Components>,
#[serde(skip_serializing_if = "Option::is_none")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(flatten)]
pub extensions: SpecificationExtensions,
}
impl Default for OpenRPC {
fn default() -> Self {
Self {
openrpc: OPEN_RPC_SPECIFICATION_VERSION,
info: Default::default(),
servers: Default::default(),
methods: Default::default(),
components: Default::default(),
external_docs: Default::default(),
extensions: Default::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct Info {
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub terms_of_service: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contact: Option<Contact>,
#[serde(skip_serializing_if = "Option::is_none")]
pub license: Option<License>,
pub version: String,
#[serde(flatten)]
pub extensions: SpecificationExtensions,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct Contact {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(flatten)]
pub extensions: SpecificationExtensions,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct License {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<Url>,
#[serde(flatten)]
pub extensions: SpecificationExtensions,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct Server {
pub name: String,
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variables: Option<BTreeMap<String, ServerVariable>>,
#[serde(flatten)]
pub extensions: SpecificationExtensions,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct ServerVariable {
#[serde(skip_serializing_if = "Option::is_none")]
pub r#enum: Option<Vec<String>>,
pub default: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(flatten)]
pub extensions: SpecificationExtensions,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct Method {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<ReferenceOr<Tag>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub external_docs: Option<ExternalDocumentation>,
pub params: Vec<ReferenceOr<ContentDescriptor>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<ReferenceOr<ContentDescriptor>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub servers: Option<Vec<Server>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub errors: Option<Vec<ReferenceOr<Error>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub param_structure: Option<ParamStructure>,
#[serde(skip_serializing_if = "Option::is_none")]
pub examples: Option<Vec<ReferenceOr<ExamplePairing>>>,
#[serde(flatten)]
pub extensions: SpecificationExtensions,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContentDescriptor {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
pub schema: Schema,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<bool>,
#[serde(flatten)]
pub extensions: SpecificationExtensions,
}
impl Default for ContentDescriptor {
fn default() -> Self {
Self {
name: Default::default(),
summary: Default::default(),
description: Default::default(),
required: Default::default(),
schema: schemars::json_schema!(false),
deprecated: Default::default(),
extensions: Default::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct ExamplePairing {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
pub params: Vec<ReferenceOr<Example>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<ReferenceOr<Example>>,
#[serde(flatten)]
pub extensions: SpecificationExtensions,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Example {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(flatten)]
pub value: ExampleValue,
#[serde(flatten)]
pub extensions: SpecificationExtensions,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub enum ExampleValue {
#[serde(rename = "externalValue")]
External(String),
#[serde(rename = "value")]
Embedded(Value),
}
#[test]
fn example() {
let json = serde_json::json!({
"name": "foo",
"value": { "foo": "bar" },
"x-tension": "extension"
});
let actual = serde_json::from_value::<Example>(json.clone()).unwrap();
assert_eq!(serde_json::to_value(actual).unwrap(), json);
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Error {
pub code: i64,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct Components {
#[serde(skip_serializing_if = "Option::is_none")]
pub content_descriptors: Option<BTreeMap<String, ContentDescriptor>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub schemas: Option<BTreeMap<String, Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub examples: Option<BTreeMap<String, Example>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub errors: Option<BTreeMap<String, Error>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example_pairing_objects: Option<BTreeMap<String, ExamplePairing>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<BTreeMap<String, Tag>>,
#[serde(flatten)]
pub extensions: SpecificationExtensions,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct Tag {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(flatten)]
pub extensions: SpecificationExtensions,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExternalDocumentation {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub url: Url,
#[serde(flatten)]
pub extensions: SpecificationExtensions,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ReferenceOr<T> {
Reference(String),
Item(T),
}
#[derive(Debug, Clone, PartialEq, Serialize, Default)]
#[serde(transparent)]
pub struct SpecificationExtensions(pub BTreeMap<String, Value>);
impl<'de> Deserialize<'de> for SpecificationExtensions {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = BTreeMap<String, Value>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a map with string keys starting with `x-`")
}
fn visit_map<A: serde::de::MapAccess<'de>>(
self,
mut map: A,
) -> Result<Self::Value, A::Error> {
let mut ret = Self::Value::default();
loop {
match map.next_key::<String>() {
Err(_) => (),
Ok(None) => break,
Ok(Some(key)) if key.starts_with("x-") => {
let _ = ret.insert(key, map.next_value()?);
}
Ok(Some(_)) => {
let _ = map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
Ok(ret)
}
}
deserializer.deserialize_any(Visitor).map(Self)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub enum ParamStructure {
ByName,
ByPosition,
#[default]
Either,
}
#[derive(Serialize, Deserialize)]
#[serde(untagged, expecting = "a reference or an inline item")]
enum _ReferenceOr<T> {
Reference {
#[serde(rename = "$ref")]
reference: String,
},
Item(T),
}
impl<T> ReferenceOr<T> {
pub fn map_item<U>(self, f: impl FnOnce(T) -> U) -> ReferenceOr<U> {
match self {
ReferenceOr::Reference(it) => ReferenceOr::Reference(it),
ReferenceOr::Item(it) => ReferenceOr::Item(f(it)),
}
}
}
impl<T: Serialize> Serialize for ReferenceOr<T> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
ReferenceOr::Reference(it) => _ReferenceOr::Reference {
reference: it.clone(),
},
ReferenceOr::Item(it) => _ReferenceOr::Item(it),
}
.serialize(serializer)
}
}
impl<'de, T: Deserialize<'de>> Deserialize<'de> for ReferenceOr<T> {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
_ReferenceOr::deserialize(deserializer).map(|it| match it {
_ReferenceOr::Reference { reference } => ReferenceOr::Reference(reference),
_ReferenceOr::Item(it) => ReferenceOr::Item(it),
})
}
}
impl Default for resolved::OpenRPC {
fn default() -> Self {
Self {
openrpc: OPEN_RPC_SPECIFICATION_VERSION,
info: Default::default(),
servers: Default::default(),
methods: Default::default(),
components: Default::default(),
external_docs: Default::default(),
extensions: Default::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use regex::Regex;
use std::borrow::Cow;
use syn::{spanned::Spanned, Item};
#[test]
fn generate_resolved() {
let lib = syn::parse_file(include_str!("lib.rs")).unwrap();
let regex = Regex::new("ReferenceOr<(?<item>[[:alnum:]]+?)>").unwrap();
let mut rewritten = String::from(
"\
// This file is @generated by library tests
//! Parallel types where [`ReferenceOr<T>`] is replaced by item `T`.
use crate::*;
use semver::Version;
use serde::{Deserialize, Serialize};
",
);
for item in lib.items {
if let Item::Struct(strukt) = item {
let source = strukt.span().source_text().unwrap();
match regex.replace_all(&source, "$item") {
Cow::Borrowed(_) => {}
Cow::Owned(replaced) => {
rewritten.push_str(&replaced);
rewritten.push('\n');
}
}
}
}
expect_test::expect_file!["./resolved.rs"].assert_eq(&rewritten);
}
#[test]
fn test_default_content_desc_ser() {
let desc = ContentDescriptor::default();
println!("{}", serde_json::to_string_pretty(&desc).unwrap());
assert_eq!(
serde_json::to_value(&desc).unwrap(),
serde_json::json!({
"name": "",
"schema": false
})
);
}
}