#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum Platform {
#[default]
Cloudflare,
Node,
Browser,
}
impl Platform {
pub fn bynk_binding_filename(self) -> &'static str {
match self {
Platform::Cloudflare => "bynk-cloudflare.ts",
Platform::Node => "bynk-node.ts",
Platform::Browser => "bynk-browser.ts",
}
}
pub fn bynk_binding_source(self) -> &'static str {
match self {
Platform::Cloudflare => BYNK_CLOUDFLARE_BINDING,
Platform::Node => BYNK_NODE_BINDING,
Platform::Browser => BYNK_BROWSER_BINDING,
}
}
pub fn as_str(self) -> &'static str {
match self {
Platform::Cloudflare => "cloudflare",
Platform::Node => "node",
Platform::Browser => "browser",
}
}
}
pub const BYNK_UNIT: &str = "bynk";
pub const BYNK_SURFACE_CAPABILITIES: &[&str] = &[
"Clock",
"Random",
"Logger",
"Fetch",
"Secrets",
"Locale",
"Idempotency",
"Events",
];
pub const CLOUDFLARE_UNIT: &str = "bynk.cloudflare";
pub const KV_BINDING_NAME: &str = "KV";
pub fn provider_takes_env(unit: &str, provider: &str) -> bool {
self::unit(unit).is_some_and(|u| u.env_providers.contains(&provider))
}
pub fn platform_of(unit: &str) -> Option<Platform> {
self::unit(unit).and_then(|u| u.platform)
}
pub const LIST_UNIT: &str = "bynk.list";
pub const MAP_UNIT: &str = "bynk.map";
pub const BYNK_LIST_SRC: &str = include_str!("../lib/core/bynk.list.bynk");
pub const BYNK_MAP_SRC: &str = include_str!("../lib/core/bynk.map.bynk");
pub const STRING_UNIT: &str = "bynk.string";
pub const BYNK_STRING_SRC: &str = include_str!("../lib/core/bynk.string.bynk");
pub const LOCALE_UNIT: &str = "bynk.locale";
pub const BYNK_LOCALE_SRC: &str = include_str!("../lib/core/bynk.locale.bynk");
pub const LOCALE_TYPES_UNIT: &str = "bynk.locale.types";
pub const BYNK_LOCALE_TYPES_SRC: &str = include_str!("../lib/core/bynk.locale.types.bynk");
pub const BYNK_ADAPTER_SRC: &str = include_str!("../lib/core/bynk.bynk");
const BYNK_CLOUDFLARE_BINDING: &str = include_str!("../lib/core/bindings/bynk-cloudflare.ts");
const BYNK_NODE_BINDING: &str = include_str!("../lib/core/bindings/bynk-node.ts");
const BYNK_BROWSER_BINDING: &str = include_str!("../lib/core/bindings/bynk-browser.ts");
pub const CLOUDFLARE_ADAPTER_SRC: &str = include_str!("../lib/cloudflare/bynk.cloudflare.bynk");
pub const FIRSTPARTY_SOURCES: &[(&str, &str)] = &SOURCES;
const SOURCES: [(&str, &str); UNITS.len()] = {
let mut out = [("", ""); UNITS.len()];
let mut i = 0;
while i < UNITS.len() {
out[i] = (UNITS[i].name, UNITS[i].source);
i += 1;
}
out
};
pub const CLOUDFLARE_BINDING_FILENAME: &str = "bynk/cloudflare.binding.ts";
const CLOUDFLARE_BINDING: &str = include_str!("../lib/cloudflare/cloudflare.binding.ts");
pub fn cloudflare_binding_source() -> &'static str {
CLOUDFLARE_BINDING
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnitKind {
Adapter,
Commons,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Binding {
None,
PerPlatform,
Fixed {
filename: &'static str,
source: &'static str,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Unit {
pub name: &'static str,
pub kind: UnitKind,
pub identity_path: &'static str,
pub source: &'static str,
pub binding: Binding,
pub id_block: u32,
pub platform: Option<Platform>,
pub env_providers: &'static [&'static str],
pub stability: Stability,
pub experimental: &'static [&'static str],
pub extensible: &'static [&'static str],
pub instantiable: &'static [Instantiable],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Instantiable {
pub capability: &'static str,
pub binding_prefix: &'static str,
pub options: &'static [&'static str],
pub required: &'static [&'static str],
pub bare: bool,
}
pub const SERVICE_BINDING_PREFIX: &str = "SERVICE";
pub const SERVICE_WORKER_OPTION: &str = "worker";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Stability {
Stable,
Experimental,
}
impl Unit {
pub fn binding_for(&self, platform: Platform) -> Option<(&'static str, &'static str)> {
match self.binding {
Binding::None => None,
Binding::PerPlatform => Some((
platform.bynk_binding_filename(),
platform.bynk_binding_source(),
)),
Binding::Fixed { filename, source } => Some((filename, source)),
}
}
}
pub const UNITS: &[Unit] = &[
Unit {
name: BYNK_UNIT,
kind: UnitKind::Adapter,
identity_path: "bynk.bynk",
source: BYNK_ADAPTER_SRC,
binding: Binding::PerPlatform,
id_block: 0,
platform: None,
env_providers: &["SecretsProvider"],
stability: Stability::Stable,
experimental: &[],
extensible: &["FetchError", "Method"],
instantiable: &[],
},
Unit {
name: CLOUDFLARE_UNIT,
kind: UnitKind::Adapter,
identity_path: "bynk/cloudflare.bynk",
source: CLOUDFLARE_ADAPTER_SRC,
binding: Binding::Fixed {
filename: CLOUDFLARE_BINDING_FILENAME,
source: CLOUDFLARE_BINDING,
},
id_block: 1,
platform: Some(Platform::Cloudflare),
env_providers: &["WorkersKv", "WorkersService"],
stability: Stability::Stable,
experimental: &["Service"],
extensible: &[],
instantiable: &[
Instantiable {
capability: "Kv",
binding_prefix: KV_BINDING_NAME,
options: &[],
required: &[],
bare: true,
},
Instantiable {
capability: "Service",
binding_prefix: SERVICE_BINDING_PREFIX,
options: &[SERVICE_WORKER_OPTION],
required: &[SERVICE_WORKER_OPTION],
bare: false,
},
],
},
Unit {
name: LIST_UNIT,
kind: UnitKind::Commons,
identity_path: "bynk/list.bynk",
source: BYNK_LIST_SRC,
binding: Binding::None,
id_block: 3,
platform: None,
env_providers: &[],
stability: Stability::Stable,
experimental: &[],
extensible: &[],
instantiable: &[],
},
Unit {
name: MAP_UNIT,
kind: UnitKind::Commons,
identity_path: "bynk/map.bynk",
source: BYNK_MAP_SRC,
binding: Binding::None,
id_block: 2,
platform: None,
env_providers: &[],
stability: Stability::Stable,
experimental: &[],
extensible: &[],
instantiable: &[],
},
Unit {
name: STRING_UNIT,
kind: UnitKind::Commons,
identity_path: "bynk/string.bynk",
source: BYNK_STRING_SRC,
binding: Binding::None,
id_block: 4,
platform: None,
env_providers: &[],
stability: Stability::Stable,
experimental: &[],
extensible: &[],
instantiable: &[],
},
Unit {
name: LOCALE_UNIT,
kind: UnitKind::Commons,
identity_path: "bynk/locale.bynk",
source: BYNK_LOCALE_SRC,
binding: Binding::None,
id_block: 6,
platform: None,
env_providers: &[],
stability: Stability::Stable,
experimental: &[],
extensible: &[],
instantiable: &[],
},
Unit {
name: LOCALE_TYPES_UNIT,
kind: UnitKind::Commons,
identity_path: "bynk/locale/types.bynk",
source: BYNK_LOCALE_TYPES_SRC,
binding: Binding::None,
id_block: 5,
platform: None,
env_providers: &[],
stability: Stability::Stable,
experimental: &[],
extensible: &[],
instantiable: &[],
},
];
pub fn unit(name: &str) -> Option<&'static Unit> {
UNITS.iter().find(|u| u.name == name)
}
pub fn split_item_path(path: &str) -> Option<(&'static Unit, &str)> {
split_item_path_in(UNITS, path)
}
pub fn split_item_path_in<'u, 'p>(units: &'u [Unit], path: &'p str) -> Option<(&'u Unit, &'p str)> {
units
.iter()
.filter_map(|u| {
if path == u.name {
Some((u, ""))
} else {
path.strip_prefix(u.name)?
.strip_prefix('.')
.map(|rest| (u, rest))
}
})
.max_by_key(|(u, _)| u.name.len())
}
pub fn covers(prefix: &str, path: &str) -> bool {
path == prefix
|| path
.strip_prefix(prefix)
.is_some_and(|rest| rest.starts_with('.'))
}
pub fn stability_of(path: &str) -> Option<Stability> {
stability_of_in(UNITS, path)
}
pub fn stability_of_in(units: &[Unit], path: &str) -> Option<Stability> {
let (unit, rest) = split_item_path_in(units, path)?;
if unit.stability == Stability::Experimental
|| unit.experimental.iter().any(|item| covers(item, rest))
{
Some(Stability::Experimental)
} else {
Some(Stability::Stable)
}
}
pub fn experimental_items() -> Vec<String> {
experimental_items_in(UNITS)
}
pub fn experimental_items_in(units: &[Unit]) -> Vec<String> {
let mut out = Vec::new();
for u in units {
if u.stability == Stability::Experimental {
out.push(u.name.to_string());
}
out.extend(
u.experimental
.iter()
.map(|item| format!("{}.{item}", u.name)),
);
}
out
}
pub fn allows(allow: &[String], path: &str) -> bool {
allow.iter().any(|entry| covers(entry, path))
}
pub fn allow_entry_is_live(experimental: &[String], entry: &str) -> bool {
experimental
.iter()
.any(|item| covers(item, entry) || covers(entry, item))
}
pub fn instantiable(unit: &str, capability: &str) -> Option<&'static Instantiable> {
self::unit(unit)?
.instantiable
.iter()
.find(|i| i.capability == capability)
}
pub fn is_extensible(unit: &str, name: &str) -> bool {
is_extensible_in(UNITS, unit, name)
}
pub fn is_extensible_in(units: &[Unit], unit: &str, name: &str) -> bool {
units
.iter()
.any(|u| u.name == unit && u.extensible.contains(&name))
}
pub fn exported_names(unit: &Unit) -> Vec<&'static str> {
let mut out = Vec::new();
for line in unit.source.lines() {
let trimmed = line.trim_start();
if let Some(rest) = trimmed.strip_prefix("exports ")
&& let Some((_, list)) = rest.split_once('{')
&& let Some((names, _)) = list.split_once('}')
{
out.extend(names.split(',').map(str::trim).filter(|n| !n.is_empty()));
} else if unit.kind == UnitKind::Commons
&& (line == trimmed || line.strip_prefix('\t') == Some(trimmed))
{
for keyword in ["fn ", "type "] {
if let Some(rest) = trimmed.strip_prefix(keyword) {
let end = rest
.find(|c: char| !(c.is_alphanumeric() || c == '_'))
.unwrap_or(rest.len());
if end > 0 {
out.push(&rest[..end]);
}
}
}
}
}
out
}
pub fn names_first_party_item(path: &str) -> bool {
names_first_party_item_in(UNITS, path)
}
pub fn names_first_party_item_in(units: &[Unit], path: &str) -> bool {
match split_item_path_in(units, path) {
None => false,
Some((_, "")) => true,
Some((unit, rest)) => {
let item = rest.split('.').next().unwrap_or(rest);
exported_names(unit).contains(&item)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const SYNTH: &[Unit] = &[
Unit {
name: "bynk",
kind: UnitKind::Adapter,
identity_path: "bynk.bynk",
source: "adapter bynk {\n\texports capability { Clock, Fetch }\n\texports transparent { FetchError }\n}\n",
binding: Binding::PerPlatform,
id_block: 0,
platform: None,
env_providers: &[],
stability: Stability::Stable,
experimental: &["Fetch.stream"],
extensible: &["FetchError"],
instantiable: &[],
},
Unit {
name: "bynk.cloudflare",
kind: UnitKind::Adapter,
identity_path: "bynk/cloudflare.bynk",
source: "adapter bynk.cloudflare {\n\texports capability { Kv, Ai }\n}\n",
binding: Binding::None,
id_block: 1,
platform: Some(Platform::Cloudflare),
env_providers: &[],
stability: Stability::Stable,
experimental: &["Ai"],
extensible: &[],
instantiable: &[],
},
Unit {
name: "bynk.cloudflare.d1",
kind: UnitKind::Adapter,
identity_path: "bynk/cloudflare/d1.bynk",
source: "adapter bynk.cloudflare.d1 {\n\texports capability { D1 }\n}\n",
binding: Binding::None,
id_block: 2,
platform: Some(Platform::Cloudflare),
env_providers: &[],
stability: Stability::Experimental,
experimental: &[],
extensible: &[],
instantiable: &[],
},
Unit {
name: "bynk.text",
kind: UnitKind::Commons,
identity_path: "bynk/text.bynk",
source: "commons bynk.text\n\ntype Slug = String\n\nfn shout(s: String) -> String {\n\ts\n}\n",
binding: Binding::None,
id_block: 3,
platform: None,
env_providers: &[],
stability: Stability::Stable,
experimental: &[],
extensible: &[],
instantiable: &[],
},
];
#[test]
fn item_paths_resolve_to_the_longest_unit() {
let unit_of = |p| split_item_path_in(SYNTH, p).map(|(u, rest)| (u.name, rest));
assert_eq!(unit_of("bynk"), Some(("bynk", "")));
assert_eq!(unit_of("bynk.Fetch.send"), Some(("bynk", "Fetch.send")));
assert_eq!(
unit_of("bynk.cloudflare.Kv"),
Some(("bynk.cloudflare", "Kv"))
);
assert_eq!(
unit_of("bynk.cloudflare.d1.D1"),
Some(("bynk.cloudflare.d1", "D1"))
);
assert_eq!(unit_of("bynkx"), None, "a whole-segment prefix only");
assert_eq!(unit_of("user.unit"), None);
}
#[test]
fn stability_follows_unit_item_and_operation() {
let st = |p| stability_of_in(SYNTH, p);
use Stability::*;
assert_eq!(st("bynk.Clock"), Some(Stable));
assert_eq!(st("bynk.Fetch.send"), Some(Stable));
assert_eq!(
st("bynk.Fetch.stream"),
Some(Experimental),
"an experimental operation"
);
assert_eq!(
st("bynk.Fetch"),
Some(Stable),
"its capability stays stable"
);
assert_eq!(
st("bynk.cloudflare.Ai"),
Some(Experimental),
"an experimental item"
);
assert_eq!(
st("bynk.cloudflare.Ai.run"),
Some(Experimental),
"its operations too"
);
assert_eq!(st("bynk.cloudflare.Kv"), Some(Stable));
assert_eq!(
st("bynk.cloudflare.d1"),
Some(Experimental),
"an experimental unit"
);
assert_eq!(st("bynk.cloudflare.d1.D1.query"), Some(Experimental));
assert_eq!(st("user.unit"), None);
}
#[test]
fn allow_entries_cover_by_whole_segment_prefix() {
let allow = vec![
"bynk.cloudflare.Ai".to_string(),
"bynk.cloudflare.d1".to_string(),
];
assert!(allows(&allow, "bynk.cloudflare.Ai"));
assert!(allows(&allow, "bynk.cloudflare.Ai.run"));
assert!(allows(&allow, "bynk.cloudflare.d1.D1.query"));
assert!(
!allows(&allow, "bynk.cloudflare.AiX"),
"not a segment prefix"
);
assert!(!allows(&allow, "bynk.Fetch.stream"));
assert!(!allows(&[], "bynk.cloudflare.Ai"));
}
#[test]
fn an_allow_entry_is_live_while_it_reaches_something_experimental() {
let synth = experimental_items_in(SYNTH);
let live = |e| allow_entry_is_live(&synth, e);
assert!(live("bynk.cloudflare.Ai"));
assert!(live("bynk.cloudflare.Ai.run"), "under an experimental item");
assert!(live("bynk.cloudflare"), "contains an experimental item");
assert!(live("bynk.Fetch.stream"));
assert!(live("bynk"), "contains an experimental operation");
assert!(!live("bynk.cloudflare.Kv"), "stable: a stale entry");
assert!(!live("bynk.text"));
assert_eq!(
experimental_items_in(SYNTH),
[
"bynk.Fetch.stream",
"bynk.cloudflare.Ai",
"bynk.cloudflare.d1"
]
);
}
#[test]
fn exported_names_and_item_existence_come_from_the_sources() {
assert_eq!(exported_names(&SYNTH[0]), ["Clock", "Fetch", "FetchError"]);
assert_eq!(exported_names(&SYNTH[3]), ["Slug", "shout"]);
let names = |p| names_first_party_item_in(SYNTH, p);
assert!(names("bynk.cloudflare"));
assert!(names("bynk.cloudflare.Ai"));
assert!(
names("bynk.cloudflare.Ai.run"),
"an operation of an existing item"
);
assert!(names("bynk.text.shout"));
assert!(!names("bynk.cloudflare.Aii"), "a typo");
assert!(!names("user.unit"));
}
#[test]
fn extensibility_is_by_unit_and_name() {
assert!(is_extensible_in(SYNTH, "bynk", "FetchError"));
assert!(
!is_extensible_in(SYNTH, "user.app", "FetchError"),
"a user's own FetchError"
);
assert!(!is_extensible_in(SYNTH, "bynk", "Method"));
}
#[test]
fn the_shipped_instantiable_kinds_are_pinned() {
let kinds: Vec<(&str, &str, &str)> = UNITS
.iter()
.flat_map(|u| {
u.instantiable
.iter()
.map(move |i| (u.name, i.capability, i.binding_prefix))
})
.collect();
assert_eq!(
kinds,
[
("bynk.cloudflare", "Kv", "KV"),
("bynk.cloudflare", "Service", "SERVICE")
]
);
let kv = instantiable("bynk.cloudflare", "Kv").expect("Kv is instantiable");
assert!(kv.options.is_empty() && kv.required.is_empty() && kv.bare);
let service = instantiable("bynk.cloudflare", "Service").expect("Service is instantiable");
assert_eq!(service.options, ["worker"]);
assert_eq!(service.required, ["worker"]);
assert!(!service.bare);
for u in UNITS {
for i in u.instantiable {
assert!(
i.required.iter().all(|r| i.options.contains(r)),
"{}.{}: a required option must be an option",
u.name,
i.capability
);
}
}
assert!(exported_names(unit("bynk.cloudflare").unwrap()).contains(&"Kv"));
assert!(exported_names(unit("bynk.cloudflare").unwrap()).contains(&"Service"));
assert_eq!(instantiable("bynk", "Clock"), None);
assert_eq!(instantiable("bynk.cloudflare", "Nope"), None);
}
#[test]
fn the_shipped_stability_is_pinned() {
for u in UNITS {
assert_eq!(u.stability, Stability::Stable, "{}", u.name);
}
assert_eq!(experimental_items(), ["bynk.cloudflare.Service"]);
let extensible: Vec<(&str, &str)> = UNITS
.iter()
.flat_map(|u| u.extensible.iter().map(move |e| (u.name, *e)))
.collect();
assert_eq!(extensible, [("bynk", "FetchError"), ("bynk", "Method")]);
for (unit, name) in extensible {
assert!(
exported_names(self::unit(unit).unwrap()).contains(&name),
"{unit} lists {name} as extensible but does not export it"
);
}
assert!(names_first_party_item("bynk.cloudflare.Kv.list"));
assert!(names_first_party_item("bynk.list.reverse"));
assert!(!names_first_party_item("bynk.cloudflare.Kvv"));
}
#[test]
fn the_cloudflare_binding_reads_kv_binding_name() {
let ident = |c: char| c.is_ascii_alphanumeric() || c == '_' || c == '$';
let src = CLOUDFLARE_BINDING;
let uses: Vec<(&str, &str)> = src
.match_indices(KV_BINDING_NAME)
.map(|(at, name)| (&src[..at], &src[at + name.len()..]))
.filter(|(before, after)| !before.ends_with(ident) && !after.starts_with(ident))
.collect();
assert!(
uses.iter().any(|(b, a)| {
b.trim_end().ends_with("binding: string = \"") && a.starts_with('"')
}),
"the constructor's `binding` no longer defaults to \"{KV_BINDING_NAME}\""
);
assert!(
src.contains("?.[this.binding]"),
"the namespace is no longer read at `env[binding]`"
);
}
#[test]
fn id_blocks_are_pinned_and_distinct() {
let blocks: Vec<(&str, u32)> = UNITS.iter().map(|u| (u.name, u.id_block)).collect();
assert_eq!(
blocks,
[
("bynk", 0),
("bynk.cloudflare", 1),
("bynk.list", 3),
("bynk.map", 2),
("bynk.string", 4),
("bynk.locale", 6),
("bynk.locale.types", 5),
]
);
let mut sorted: Vec<u32> = UNITS.iter().map(|u| u.id_block).collect();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(sorted.len(), UNITS.len(), "two units share an id block");
}
#[test]
fn firstparty_sources_keep_their_order() {
let names: Vec<&str> = FIRSTPARTY_SOURCES.iter().map(|(n, _)| *n).collect();
assert_eq!(
names,
[
"bynk",
"bynk.cloudflare",
"bynk.list",
"bynk.map",
"bynk.string",
"bynk.locale",
"bynk.locale.types",
]
);
for (unit, (name, source)) in UNITS.iter().zip(FIRSTPARTY_SOURCES) {
assert_eq!((unit.name, unit.source), (*name, *source));
}
}
#[test]
fn metadata_views_match_the_previous_answers() {
assert_eq!(platform_of("bynk.cloudflare"), Some(Platform::Cloudflare));
for name in [
"bynk",
"bynk.list",
"bynk.map",
"bynk.string",
"bynk.locale",
"user.unit",
] {
assert_eq!(platform_of(name), None, "{name}");
}
assert!(provider_takes_env("bynk", "SecretsProvider"));
assert!(provider_takes_env("bynk.cloudflare", "WorkersKv"));
assert!(!provider_takes_env("bynk", "WorkersKv"));
assert!(!provider_takes_env("bynk.cloudflare", "SecretsProvider"));
assert!(!provider_takes_env("bynk", "ClockProvider"));
assert!(!provider_takes_env("user.unit", "SecretsProvider"));
}
#[test]
fn entries_are_well_formed() {
for u in UNITS {
assert!(
u.name == "bynk" || u.name.starts_with("bynk."),
"{}",
u.name
);
assert_eq!(
UNITS.iter().filter(|o| o.name == u.name).count(),
1,
"{}",
u.name
);
assert_eq!(
UNITS
.iter()
.filter(|o| o.identity_path == u.identity_path)
.count(),
1,
"{}",
u.identity_path
);
match u.kind {
UnitKind::Adapter => assert_ne!(u.binding, Binding::None, "{}", u.name),
UnitKind::Commons => {
assert_eq!(u.binding, Binding::None, "{}", u.name);
assert!(u.env_providers.is_empty(), "{}", u.name);
}
}
}
assert_eq!(
unit("bynk.cloudflare").and_then(|u| u.binding_for(Platform::Node)),
Some((CLOUDFLARE_BINDING_FILENAME, CLOUDFLARE_BINDING))
);
assert_eq!(
unit("bynk").and_then(|u| u.binding_for(Platform::Browser)),
Some(("bynk-browser.ts", BYNK_BROWSER_BINDING))
);
assert_eq!(
unit("bynk.list").and_then(|u| u.binding_for(Platform::Node)),
None
);
}
#[test]
fn bynk_surface_capabilities_match_the_adapter() {
let src = BYNK_ADAPTER_SRC;
let line = src
.lines()
.find(|l| l.trim_start().starts_with("exports capability {"))
.expect("adapter declares `exports capability`");
let inner = line
.split_once('{')
.and_then(|(_, r)| r.split_once('}'))
.map(|(names, _)| names)
.expect("braced capability list");
let declared: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
assert_eq!(
declared, BYNK_SURFACE_CAPABILITIES,
"BYNK_SURFACE_CAPABILITIES is out of sync with bynk.bynk"
);
}
}