2020-09-23 00:04:00 +00:00
|
|
|
use std::fmt::Write;
|
|
|
|
|
2022-03-22 03:09:31 +00:00
|
|
|
use crate::registry::{Deprecation, MetaField, MetaInputValue, MetaType, Registry};
|
2020-10-15 06:38:10 +00:00
|
|
|
|
2022-05-05 12:09:35 +00:00
|
|
|
const SYSTEM_SCALARS: &[&str] = &["Int", "Float", "String", "Boolean", "ID"];
|
|
|
|
const FEDERATION_SCALARS: &[&str] = &["Any"];
|
|
|
|
|
|
|
|
/// Options for SDL export
|
|
|
|
#[derive(Debug, Copy, Clone, Default)]
|
|
|
|
pub struct SDLExportOptions {
|
|
|
|
sorted_fields: bool,
|
|
|
|
sorted_arguments: bool,
|
|
|
|
sorted_enum_values: bool,
|
|
|
|
federation: bool,
|
2022-06-25 02:21:00 +00:00
|
|
|
prefer_single_line_descriptions: bool,
|
2022-05-05 12:09:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl SDLExportOptions {
|
|
|
|
/// Create a `SDLExportOptions`
|
|
|
|
#[inline]
|
|
|
|
pub fn new() -> Self {
|
|
|
|
Default::default()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Export sorted fields
|
|
|
|
#[inline]
|
|
|
|
#[must_use]
|
|
|
|
pub fn sorted_fields(self) -> Self {
|
|
|
|
Self {
|
|
|
|
sorted_fields: true,
|
|
|
|
..self
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Export sorted field arguments
|
|
|
|
#[inline]
|
|
|
|
#[must_use]
|
|
|
|
pub fn sorted_arguments(self) -> Self {
|
|
|
|
Self {
|
|
|
|
sorted_arguments: true,
|
|
|
|
..self
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Export sorted enum items
|
|
|
|
#[inline]
|
|
|
|
#[must_use]
|
|
|
|
pub fn sorted_enum_items(self) -> Self {
|
|
|
|
Self {
|
|
|
|
sorted_enum_values: true,
|
|
|
|
..self
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Export as Federation SDL(Schema Definition Language)
|
|
|
|
#[inline]
|
|
|
|
#[must_use]
|
|
|
|
pub fn federation(self) -> Self {
|
|
|
|
Self {
|
|
|
|
federation: true,
|
|
|
|
..self
|
|
|
|
}
|
|
|
|
}
|
2022-06-25 02:21:00 +00:00
|
|
|
|
|
|
|
/// When possible, write one-line instead of three-line descriptions
|
|
|
|
#[inline]
|
|
|
|
#[must_use]
|
|
|
|
pub fn prefer_single_line_descriptions(self) -> Self {
|
|
|
|
Self {
|
|
|
|
prefer_single_line_descriptions: true,
|
|
|
|
..self
|
|
|
|
}
|
|
|
|
}
|
2022-05-05 12:09:35 +00:00
|
|
|
}
|
|
|
|
|
2020-09-23 00:04:00 +00:00
|
|
|
impl Registry {
|
2022-05-05 12:09:35 +00:00
|
|
|
pub(crate) fn export_sdl(&self, options: SDLExportOptions) -> String {
|
2020-09-23 00:04:00 +00:00
|
|
|
let mut sdl = String::new();
|
|
|
|
|
2022-05-07 08:16:01 +00:00
|
|
|
let has_oneof = self
|
|
|
|
.types
|
|
|
|
.values()
|
|
|
|
.any(|ty| matches!(ty, MetaType::InputObject { oneof: true, .. }));
|
2022-03-05 01:06:59 +00:00
|
|
|
|
|
|
|
if has_oneof {
|
2022-05-07 08:16:01 +00:00
|
|
|
sdl.write_str("directive @oneOf on INPUT_OBJECT\n\n").ok();
|
2022-03-05 01:06:59 +00:00
|
|
|
}
|
|
|
|
|
2020-09-23 00:04:00 +00:00
|
|
|
for ty in self.types.values() {
|
|
|
|
if ty.name().starts_with("__") {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-05-05 12:09:35 +00:00
|
|
|
if options.federation {
|
2020-09-23 00:04:00 +00:00
|
|
|
const FEDERATION_TYPES: &[&str] = &["_Any", "_Entity", "_Service"];
|
|
|
|
if FEDERATION_TYPES.contains(&ty.name()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-05 12:09:35 +00:00
|
|
|
self.export_type(ty, &mut sdl, &options);
|
|
|
|
writeln!(sdl).ok();
|
2020-09-23 00:04:00 +00:00
|
|
|
}
|
|
|
|
|
2022-05-05 12:09:35 +00:00
|
|
|
if !options.federation {
|
2020-09-23 00:04:00 +00:00
|
|
|
writeln!(sdl, "schema {{").ok();
|
|
|
|
writeln!(sdl, "\tquery: {}", self.query_type).ok();
|
|
|
|
if let Some(mutation_type) = self.mutation_type.as_deref() {
|
|
|
|
writeln!(sdl, "\tmutation: {}", mutation_type).ok();
|
|
|
|
}
|
|
|
|
if let Some(subscription_type) = self.subscription_type.as_deref() {
|
|
|
|
writeln!(sdl, "\tsubscription: {}", subscription_type).ok();
|
|
|
|
}
|
|
|
|
writeln!(sdl, "}}").ok();
|
|
|
|
}
|
|
|
|
|
|
|
|
sdl
|
|
|
|
}
|
|
|
|
|
|
|
|
fn export_fields<'a, I: Iterator<Item = &'a MetaField>>(
|
|
|
|
sdl: &mut String,
|
|
|
|
it: I,
|
2022-05-05 12:09:35 +00:00
|
|
|
options: &SDLExportOptions,
|
2020-09-23 00:04:00 +00:00
|
|
|
) {
|
2022-05-05 12:09:35 +00:00
|
|
|
let mut fields = it.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
if options.sorted_fields {
|
|
|
|
fields.sort_by(|a, b| a.name.cmp(&b.name));
|
|
|
|
}
|
|
|
|
|
|
|
|
for field in fields {
|
2020-09-23 00:04:00 +00:00
|
|
|
if field.name.starts_with("__")
|
2022-05-05 12:09:35 +00:00
|
|
|
|| (options.federation && matches!(&*field.name, "_service" | "_entities"))
|
2020-09-23 00:04:00 +00:00
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-06-25 02:21:00 +00:00
|
|
|
if let Some(description) = field.description {
|
|
|
|
export_description(sdl, options, false, description);
|
2020-09-23 13:36:40 +00:00
|
|
|
}
|
2022-05-05 12:09:35 +00:00
|
|
|
|
2020-09-23 00:04:00 +00:00
|
|
|
if !field.args.is_empty() {
|
2020-10-16 05:37:48 +00:00
|
|
|
write!(sdl, "\t{}(", field.name).ok();
|
2022-05-05 12:09:35 +00:00
|
|
|
|
|
|
|
let mut args = field.args.values().collect::<Vec<_>>();
|
|
|
|
if options.sorted_arguments {
|
|
|
|
args.sort_by(|a, b| a.name.cmp(b.name));
|
|
|
|
}
|
|
|
|
|
|
|
|
for (i, arg) in args.into_iter().enumerate() {
|
2020-10-16 05:37:48 +00:00
|
|
|
if i != 0 {
|
|
|
|
sdl.push_str(", ");
|
|
|
|
}
|
|
|
|
sdl.push_str(&export_input_value(arg));
|
|
|
|
}
|
|
|
|
write!(sdl, "): {}", field.ty).ok();
|
2020-09-23 00:04:00 +00:00
|
|
|
} else {
|
|
|
|
write!(sdl, "\t{}: {}", field.name, field.ty).ok();
|
|
|
|
}
|
|
|
|
|
2022-03-22 03:09:31 +00:00
|
|
|
write_deprecated(sdl, &field.deprecation);
|
|
|
|
|
2022-05-05 12:09:35 +00:00
|
|
|
if options.federation {
|
2020-09-23 00:04:00 +00:00
|
|
|
if field.external {
|
|
|
|
write!(sdl, " @external").ok();
|
|
|
|
}
|
|
|
|
if let Some(requires) = field.requires {
|
|
|
|
write!(sdl, " @requires(fields: \"{}\")", requires).ok();
|
|
|
|
}
|
|
|
|
if let Some(provides) = field.provides {
|
|
|
|
write!(sdl, " @provides(fields: \"{}\")", provides).ok();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
writeln!(sdl).ok();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-05 12:09:35 +00:00
|
|
|
fn export_type(&self, ty: &MetaType, sdl: &mut String, options: &SDLExportOptions) {
|
2020-09-23 00:04:00 +00:00
|
|
|
match ty {
|
2020-09-23 13:36:40 +00:00
|
|
|
MetaType::Scalar {
|
2020-09-23 13:54:33 +00:00
|
|
|
name, description, ..
|
2020-09-23 13:36:40 +00:00
|
|
|
} => {
|
2020-09-23 00:04:00 +00:00
|
|
|
let mut export_scalar = !SYSTEM_SCALARS.contains(&name.as_str());
|
2022-05-05 12:09:35 +00:00
|
|
|
if options.federation && FEDERATION_SCALARS.contains(&name.as_str()) {
|
2020-09-23 00:04:00 +00:00
|
|
|
export_scalar = false;
|
|
|
|
}
|
|
|
|
if export_scalar {
|
2022-06-25 02:21:00 +00:00
|
|
|
if let Some(description) = description {
|
|
|
|
export_description(sdl, options, true, description);
|
2020-09-23 13:36:40 +00:00
|
|
|
}
|
2020-09-23 00:04:00 +00:00
|
|
|
writeln!(sdl, "scalar {}", name).ok();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
MetaType::Object {
|
|
|
|
name,
|
|
|
|
fields,
|
|
|
|
extends,
|
|
|
|
keys,
|
2020-09-23 13:36:40 +00:00
|
|
|
description,
|
2020-09-23 00:04:00 +00:00
|
|
|
..
|
|
|
|
} => {
|
2021-04-01 12:56:14 +00:00
|
|
|
if Some(name.as_str()) == self.subscription_type.as_deref()
|
2022-05-05 12:09:35 +00:00
|
|
|
&& options.federation
|
2021-04-01 12:56:14 +00:00
|
|
|
&& !self.federation_subscription
|
|
|
|
{
|
2020-09-27 11:29:23 +00:00
|
|
|
return;
|
2020-09-23 00:04:00 +00:00
|
|
|
}
|
|
|
|
|
2022-05-05 12:09:35 +00:00
|
|
|
if name.as_str() == self.query_type && options.federation {
|
2021-04-12 07:46:49 +00:00
|
|
|
let mut field_count = 0;
|
|
|
|
for field in fields.values() {
|
|
|
|
if field.name.starts_with("__")
|
2022-05-05 12:09:35 +00:00
|
|
|
|| (options.federation
|
|
|
|
&& matches!(&*field.name, "_service" | "_entities"))
|
2021-04-12 07:46:49 +00:00
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
field_count += 1;
|
|
|
|
}
|
|
|
|
if field_count == 0 {
|
2022-05-05 12:09:35 +00:00
|
|
|
// is empty query root type
|
2021-04-12 07:46:49 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-25 02:21:00 +00:00
|
|
|
if let Some(description) = description {
|
|
|
|
export_description(sdl, options, true, description);
|
2020-09-23 13:36:40 +00:00
|
|
|
}
|
2022-05-05 12:09:35 +00:00
|
|
|
|
|
|
|
if options.federation && *extends {
|
2020-09-23 00:04:00 +00:00
|
|
|
write!(sdl, "extend ").ok();
|
|
|
|
}
|
2022-05-05 12:09:35 +00:00
|
|
|
|
2020-09-23 00:04:00 +00:00
|
|
|
write!(sdl, "type {} ", name).ok();
|
2020-11-07 00:49:08 +00:00
|
|
|
self.write_implements(sdl, name);
|
2020-09-23 00:04:00 +00:00
|
|
|
|
2022-05-05 12:09:35 +00:00
|
|
|
if options.federation {
|
2020-09-23 00:04:00 +00:00
|
|
|
if let Some(keys) = keys {
|
|
|
|
for key in keys {
|
|
|
|
write!(sdl, "@key(fields: \"{}\") ", key).ok();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
writeln!(sdl, "{{").ok();
|
2022-05-05 12:09:35 +00:00
|
|
|
Self::export_fields(sdl, fields.values(), options);
|
2020-09-23 00:04:00 +00:00
|
|
|
writeln!(sdl, "}}").ok();
|
|
|
|
}
|
|
|
|
MetaType::Interface {
|
|
|
|
name,
|
|
|
|
fields,
|
|
|
|
extends,
|
|
|
|
keys,
|
2020-09-23 13:36:40 +00:00
|
|
|
description,
|
2020-09-23 00:04:00 +00:00
|
|
|
..
|
|
|
|
} => {
|
2022-06-25 02:21:00 +00:00
|
|
|
if let Some(description) = description {
|
|
|
|
export_description(sdl, options, true, description);
|
2020-09-23 13:36:40 +00:00
|
|
|
}
|
2022-05-05 12:09:35 +00:00
|
|
|
|
|
|
|
if options.federation && *extends {
|
2020-09-23 00:04:00 +00:00
|
|
|
write!(sdl, "extend ").ok();
|
|
|
|
}
|
|
|
|
write!(sdl, "interface {} ", name).ok();
|
2022-05-05 12:09:35 +00:00
|
|
|
|
|
|
|
if options.federation {
|
2020-09-23 00:04:00 +00:00
|
|
|
if let Some(keys) = keys {
|
|
|
|
for key in keys {
|
|
|
|
write!(sdl, "@key(fields: \"{}\") ", key).ok();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-11-07 00:49:08 +00:00
|
|
|
self.write_implements(sdl, name);
|
|
|
|
|
2020-09-23 00:04:00 +00:00
|
|
|
writeln!(sdl, "{{").ok();
|
2022-05-05 12:09:35 +00:00
|
|
|
Self::export_fields(sdl, fields.values(), options);
|
2020-09-23 00:04:00 +00:00
|
|
|
writeln!(sdl, "}}").ok();
|
|
|
|
}
|
|
|
|
MetaType::Enum {
|
2020-09-23 13:54:33 +00:00
|
|
|
name,
|
|
|
|
enum_values,
|
2020-09-23 13:36:40 +00:00
|
|
|
description,
|
|
|
|
..
|
2020-09-23 00:04:00 +00:00
|
|
|
} => {
|
2022-06-25 02:21:00 +00:00
|
|
|
if let Some(description) = description {
|
|
|
|
export_description(sdl, options, true, description);
|
2020-09-23 13:36:40 +00:00
|
|
|
}
|
2022-05-05 12:09:35 +00:00
|
|
|
|
2020-09-23 00:04:00 +00:00
|
|
|
write!(sdl, "enum {} ", name).ok();
|
|
|
|
writeln!(sdl, "{{").ok();
|
2022-05-05 12:09:35 +00:00
|
|
|
|
|
|
|
let mut values = enum_values.values().collect::<Vec<_>>();
|
|
|
|
if options.sorted_enum_values {
|
|
|
|
values.sort_by(|a, b| a.name.cmp(&b.name));
|
|
|
|
}
|
|
|
|
|
|
|
|
for value in values {
|
2022-03-22 03:09:31 +00:00
|
|
|
write!(sdl, "\t{}", value.name).ok();
|
|
|
|
write_deprecated(sdl, &value.deprecation);
|
|
|
|
writeln!(sdl).ok();
|
2020-09-23 00:04:00 +00:00
|
|
|
}
|
2022-05-05 12:09:35 +00:00
|
|
|
|
2020-09-23 00:04:00 +00:00
|
|
|
writeln!(sdl, "}}").ok();
|
|
|
|
}
|
|
|
|
MetaType::InputObject {
|
2020-09-23 13:36:40 +00:00
|
|
|
name,
|
|
|
|
input_fields,
|
|
|
|
description,
|
2022-02-17 08:55:32 +00:00
|
|
|
oneof,
|
2020-09-23 13:36:40 +00:00
|
|
|
..
|
2020-09-23 00:04:00 +00:00
|
|
|
} => {
|
2022-06-25 02:21:00 +00:00
|
|
|
if let Some(description) = description {
|
|
|
|
export_description(sdl, options, true, description);
|
2020-09-23 13:36:40 +00:00
|
|
|
}
|
2022-05-05 12:09:35 +00:00
|
|
|
|
2020-09-23 00:04:00 +00:00
|
|
|
write!(sdl, "input {} ", name).ok();
|
2022-05-05 12:09:35 +00:00
|
|
|
|
2022-02-17 08:55:32 +00:00
|
|
|
if *oneof {
|
|
|
|
write!(sdl, "@oneof ").ok();
|
|
|
|
}
|
2020-09-23 00:04:00 +00:00
|
|
|
writeln!(sdl, "{{").ok();
|
2022-05-05 12:09:35 +00:00
|
|
|
|
|
|
|
let mut fields = input_fields.values().collect::<Vec<_>>();
|
|
|
|
if options.sorted_fields {
|
|
|
|
fields.sort_by(|a, b| a.name.cmp(b.name));
|
|
|
|
}
|
|
|
|
|
|
|
|
for field in fields {
|
2020-09-23 13:36:40 +00:00
|
|
|
if let Some(description) = field.description {
|
2022-06-25 02:21:00 +00:00
|
|
|
export_description(sdl, options, false, description);
|
2020-09-23 13:36:40 +00:00
|
|
|
}
|
2021-03-07 06:35:47 +00:00
|
|
|
writeln!(sdl, "\t{}", export_input_value(&field)).ok();
|
2020-09-23 00:04:00 +00:00
|
|
|
}
|
2022-05-05 12:09:35 +00:00
|
|
|
|
2020-09-23 00:04:00 +00:00
|
|
|
writeln!(sdl, "}}").ok();
|
|
|
|
}
|
|
|
|
MetaType::Union {
|
|
|
|
name,
|
|
|
|
possible_types,
|
2020-09-23 13:36:40 +00:00
|
|
|
description,
|
2020-09-23 00:04:00 +00:00
|
|
|
..
|
|
|
|
} => {
|
2022-06-25 02:21:00 +00:00
|
|
|
if let Some(description) = description {
|
|
|
|
export_description(sdl, options, true, description);
|
2020-09-23 13:36:40 +00:00
|
|
|
}
|
2022-05-05 12:09:35 +00:00
|
|
|
|
2020-10-16 05:37:48 +00:00
|
|
|
write!(sdl, "union {} =", name).ok();
|
2022-06-21 05:58:00 +00:00
|
|
|
for (idx, ty) in possible_types.iter().enumerate() {
|
|
|
|
if idx == 0 {
|
|
|
|
write!(sdl, " {}", ty).ok();
|
|
|
|
} else {
|
|
|
|
write!(sdl, " | {}", ty).ok();
|
|
|
|
}
|
2020-10-16 05:37:48 +00:00
|
|
|
}
|
|
|
|
writeln!(sdl).ok();
|
2020-09-23 00:04:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-11-07 00:49:08 +00:00
|
|
|
|
|
|
|
fn write_implements(&self, sdl: &mut String, name: &str) {
|
|
|
|
if let Some(implements) = self.implements.get(name) {
|
|
|
|
if !implements.is_empty() {
|
|
|
|
write!(
|
|
|
|
sdl,
|
|
|
|
"implements {} ",
|
|
|
|
implements
|
|
|
|
.iter()
|
|
|
|
.map(AsRef::as_ref)
|
|
|
|
.collect::<Vec<&str>>()
|
|
|
|
.join(" & ")
|
|
|
|
)
|
|
|
|
.ok();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-09-23 00:04:00 +00:00
|
|
|
}
|
|
|
|
|
2022-06-25 02:21:00 +00:00
|
|
|
fn export_description(
|
|
|
|
sdl: &mut String,
|
|
|
|
options: &SDLExportOptions,
|
|
|
|
top_level: bool,
|
|
|
|
description: &str,
|
|
|
|
) {
|
|
|
|
if options.prefer_single_line_descriptions && !description.contains('\n') {
|
|
|
|
let tab = if top_level { "" } else { "\t" };
|
|
|
|
let description = description.replace('"', r#"\""#);
|
|
|
|
writeln!(sdl, "{}\"{}\"", tab, description).ok();
|
2022-06-25 02:23:48 +00:00
|
|
|
} else if top_level {
|
|
|
|
writeln!(sdl, "\"\"\"\n{}\n\"\"\"", description).ok();
|
2022-06-25 02:21:00 +00:00
|
|
|
} else {
|
2022-06-25 02:23:48 +00:00
|
|
|
let description = description.replace('\n', "\n\t");
|
|
|
|
writeln!(sdl, "\t\"\"\"\n\t{}\n\t\"\"\"", description).ok();
|
2022-06-25 02:21:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-23 00:04:00 +00:00
|
|
|
fn export_input_value(input_value: &MetaInputValue) -> String {
|
|
|
|
if let Some(default_value) = &input_value.default_value {
|
|
|
|
format!(
|
|
|
|
"{}: {} = {}",
|
|
|
|
input_value.name, input_value.ty, default_value
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
format!("{}: {}", input_value.name, input_value.ty)
|
|
|
|
}
|
|
|
|
}
|
2022-03-22 03:09:31 +00:00
|
|
|
|
|
|
|
fn write_deprecated(sdl: &mut String, deprecation: &Deprecation) {
|
|
|
|
if let Deprecation::Deprecated { reason } = deprecation {
|
|
|
|
let _ = match reason {
|
|
|
|
Some(reason) => write!(sdl, " @deprecated(reason: \"{}\")", escape_string(reason)).ok(),
|
|
|
|
None => write!(sdl, " @deprecated").ok(),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn escape_string(s: &str) -> String {
|
|
|
|
let mut res = String::new();
|
|
|
|
|
|
|
|
for c in s.chars() {
|
|
|
|
let ec = match c {
|
|
|
|
'\\' => Some("\\\\"),
|
|
|
|
'\x08' => Some("\\b"),
|
|
|
|
'\x0c' => Some("\\f"),
|
|
|
|
'\n' => Some("\\n"),
|
|
|
|
'\r' => Some("\\r"),
|
|
|
|
'\t' => Some("\\t"),
|
|
|
|
_ => None,
|
|
|
|
};
|
|
|
|
match ec {
|
|
|
|
Some(ec) => {
|
|
|
|
res.write_str(ec).ok();
|
|
|
|
}
|
|
|
|
None => {
|
|
|
|
res.write_char(c).ok();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
res
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_escape_string() {
|
|
|
|
assert_eq!(
|
|
|
|
escape_string("1\\\x08d\x0c3\n4\r5\t6"),
|
|
|
|
"1\\\\\\bd\\f3\\n4\\r5\\t6"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|