|
|
|
|
@@ -23,9 +23,9 @@ pub trait CrdtNode: CrdtNodeFromValue + Hashable + Clone {
|
|
|
|
|
/// Create a new CRDT of this type
|
|
|
|
|
fn new(id: AuthorId, path: Vec<PathSegment>) -> Self;
|
|
|
|
|
/// Apply an operation to this CRDT, forwarding if necessary
|
|
|
|
|
fn apply(&mut self, op: Op<Value>) -> OpState;
|
|
|
|
|
fn apply(&mut self, op: Op<JsonValue>) -> OpState;
|
|
|
|
|
/// Get a JSON representation of the value in this node
|
|
|
|
|
fn view(&self) -> Value;
|
|
|
|
|
fn view(&self) -> JsonValue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Enum representing possible outcomes of applying an operation to a CRDT
|
|
|
|
|
@@ -60,14 +60,14 @@ pub enum OpState {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The following types can be used as a 'terminal' type in CRDTs
|
|
|
|
|
pub trait MarkPrimitive: Into<Value> + Default {}
|
|
|
|
|
pub trait MarkPrimitive: Into<JsonValue> + Default {}
|
|
|
|
|
impl MarkPrimitive for bool {}
|
|
|
|
|
impl MarkPrimitive for i32 {}
|
|
|
|
|
impl MarkPrimitive for i64 {}
|
|
|
|
|
impl MarkPrimitive for f64 {}
|
|
|
|
|
impl MarkPrimitive for char {}
|
|
|
|
|
impl MarkPrimitive for String {}
|
|
|
|
|
impl MarkPrimitive for Value {}
|
|
|
|
|
impl MarkPrimitive for JsonValue {}
|
|
|
|
|
|
|
|
|
|
/// Implement CrdtNode for non-CRDTs
|
|
|
|
|
/// This is a stub implementation so most functions don't do anything/log an error
|
|
|
|
|
@@ -75,11 +75,11 @@ impl<T> CrdtNode for T
|
|
|
|
|
where
|
|
|
|
|
T: CrdtNodeFromValue + MarkPrimitive + Hashable + Clone,
|
|
|
|
|
{
|
|
|
|
|
fn apply(&mut self, _op: Op<Value>) -> OpState {
|
|
|
|
|
fn apply(&mut self, _op: Op<JsonValue>) -> OpState {
|
|
|
|
|
OpState::ErrApplyOnPrimitive
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn view(&self) -> Value {
|
|
|
|
|
fn view(&self) -> JsonValue {
|
|
|
|
|
self.to_owned().into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -113,7 +113,7 @@ pub struct SignedOp {
|
|
|
|
|
author: AuthorId,
|
|
|
|
|
/// Signed hash using priv key of author. Effectively [`OpID`] Use this as the ID to figure out what has been delivered already
|
|
|
|
|
pub signed_digest: SignedDigest,
|
|
|
|
|
pub inner: Op<Value>,
|
|
|
|
|
pub inner: Op<JsonValue>,
|
|
|
|
|
/// List of causal dependencies
|
|
|
|
|
pub depends_on: Vec<SignedDigest>,
|
|
|
|
|
}
|
|
|
|
|
@@ -245,26 +245,26 @@ impl<T: CrdtNode + DebugView> BaseCrdt<T> {
|
|
|
|
|
|
|
|
|
|
/// An enum representing a JSON value
|
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
|
|
|
pub enum Value {
|
|
|
|
|
pub enum JsonValue {
|
|
|
|
|
Null,
|
|
|
|
|
Bool(bool),
|
|
|
|
|
Number(f64),
|
|
|
|
|
String(String),
|
|
|
|
|
Array(Vec<Value>),
|
|
|
|
|
Object(HashMap<String, Value>),
|
|
|
|
|
Array(Vec<JsonValue>),
|
|
|
|
|
Object(HashMap<String, JsonValue>),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Display for Value {
|
|
|
|
|
impl Display for JsonValue {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
write!(
|
|
|
|
|
f,
|
|
|
|
|
"{}",
|
|
|
|
|
match self {
|
|
|
|
|
Value::Null => "null".to_string(),
|
|
|
|
|
Value::Bool(b) => b.to_string(),
|
|
|
|
|
Value::Number(n) => n.to_string(),
|
|
|
|
|
Value::String(s) => format!("\"{s}\""),
|
|
|
|
|
Value::Array(arr) => {
|
|
|
|
|
JsonValue::Null => "null".to_string(),
|
|
|
|
|
JsonValue::Bool(b) => b.to_string(),
|
|
|
|
|
JsonValue::Number(n) => n.to_string(),
|
|
|
|
|
JsonValue::String(s) => format!("\"{s}\""),
|
|
|
|
|
JsonValue::Array(arr) => {
|
|
|
|
|
if arr.len() > 1 {
|
|
|
|
|
format!(
|
|
|
|
|
"[\n{}\n]",
|
|
|
|
|
@@ -283,7 +283,7 @@ impl Display for Value {
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Value::Object(obj) => format!(
|
|
|
|
|
JsonValue::Object(obj) => format!(
|
|
|
|
|
"{{ {} }}",
|
|
|
|
|
obj.iter()
|
|
|
|
|
.map(|(k, v)| format!(" \"{k}\": {v}"))
|
|
|
|
|
@@ -295,7 +295,7 @@ impl Display for Value {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for Value {
|
|
|
|
|
impl Default for JsonValue {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self::Null
|
|
|
|
|
}
|
|
|
|
|
@@ -303,17 +303,19 @@ impl Default for Value {
|
|
|
|
|
|
|
|
|
|
/// Allow easy conversion to and from serde's JSON format. This allows us to use the [`json!`]
|
|
|
|
|
/// macro
|
|
|
|
|
impl From<Value> for serde_json::Value {
|
|
|
|
|
fn from(value: Value) -> Self {
|
|
|
|
|
impl From<JsonValue> for serde_json::Value {
|
|
|
|
|
fn from(value: JsonValue) -> Self {
|
|
|
|
|
match value {
|
|
|
|
|
Value::Null => serde_json::Value::Null,
|
|
|
|
|
Value::Bool(x) => serde_json::Value::Bool(x),
|
|
|
|
|
Value::Number(x) => serde_json::Value::Number(serde_json::Number::from_f64(x).unwrap()),
|
|
|
|
|
Value::String(x) => serde_json::Value::String(x),
|
|
|
|
|
Value::Array(x) => {
|
|
|
|
|
JsonValue::Null => serde_json::Value::Null,
|
|
|
|
|
JsonValue::Bool(x) => serde_json::Value::Bool(x),
|
|
|
|
|
JsonValue::Number(x) => {
|
|
|
|
|
serde_json::Value::Number(serde_json::Number::from_f64(x).unwrap())
|
|
|
|
|
}
|
|
|
|
|
JsonValue::String(x) => serde_json::Value::String(x),
|
|
|
|
|
JsonValue::Array(x) => {
|
|
|
|
|
serde_json::Value::Array(x.iter().map(|a| a.clone().into()).collect())
|
|
|
|
|
}
|
|
|
|
|
Value::Object(x) => serde_json::Value::Object(
|
|
|
|
|
JsonValue::Object(x) => serde_json::Value::Object(
|
|
|
|
|
x.iter()
|
|
|
|
|
.map(|(k, v)| (k.clone(), v.clone().into()))
|
|
|
|
|
.collect(),
|
|
|
|
|
@@ -322,17 +324,17 @@ impl From<Value> for serde_json::Value {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<serde_json::Value> for Value {
|
|
|
|
|
impl From<serde_json::Value> for JsonValue {
|
|
|
|
|
fn from(value: serde_json::Value) -> Self {
|
|
|
|
|
match value {
|
|
|
|
|
serde_json::Value::Null => Value::Null,
|
|
|
|
|
serde_json::Value::Bool(x) => Value::Bool(x),
|
|
|
|
|
serde_json::Value::Number(x) => Value::Number(x.as_f64().unwrap()),
|
|
|
|
|
serde_json::Value::String(x) => Value::String(x),
|
|
|
|
|
serde_json::Value::Null => JsonValue::Null,
|
|
|
|
|
serde_json::Value::Bool(x) => JsonValue::Bool(x),
|
|
|
|
|
serde_json::Value::Number(x) => JsonValue::Number(x.as_f64().unwrap()),
|
|
|
|
|
serde_json::Value::String(x) => JsonValue::String(x),
|
|
|
|
|
serde_json::Value::Array(x) => {
|
|
|
|
|
Value::Array(x.iter().map(|a| a.clone().into()).collect())
|
|
|
|
|
JsonValue::Array(x.iter().map(|a| a.clone().into()).collect())
|
|
|
|
|
}
|
|
|
|
|
serde_json::Value::Object(x) => Value::Object(
|
|
|
|
|
serde_json::Value::Object(x) => JsonValue::Object(
|
|
|
|
|
x.iter()
|
|
|
|
|
.map(|(k, v)| (k.clone(), v.clone().into()))
|
|
|
|
|
.collect(),
|
|
|
|
|
@@ -341,73 +343,73 @@ impl From<serde_json::Value> for Value {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Value {
|
|
|
|
|
impl JsonValue {
|
|
|
|
|
pub fn into_json(self) -> serde_json::Value {
|
|
|
|
|
self.into()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Conversions from primitive types to [`Value`]
|
|
|
|
|
impl From<bool> for Value {
|
|
|
|
|
impl From<bool> for JsonValue {
|
|
|
|
|
fn from(val: bool) -> Self {
|
|
|
|
|
Value::Bool(val)
|
|
|
|
|
JsonValue::Bool(val)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<i64> for Value {
|
|
|
|
|
impl From<i64> for JsonValue {
|
|
|
|
|
fn from(val: i64) -> Self {
|
|
|
|
|
Value::Number(val as f64)
|
|
|
|
|
JsonValue::Number(val as f64)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<i32> for Value {
|
|
|
|
|
impl From<i32> for JsonValue {
|
|
|
|
|
fn from(val: i32) -> Self {
|
|
|
|
|
Value::Number(val as f64)
|
|
|
|
|
JsonValue::Number(val as f64)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<f64> for Value {
|
|
|
|
|
impl From<f64> for JsonValue {
|
|
|
|
|
fn from(val: f64) -> Self {
|
|
|
|
|
Value::Number(val)
|
|
|
|
|
JsonValue::Number(val)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<String> for Value {
|
|
|
|
|
impl From<String> for JsonValue {
|
|
|
|
|
fn from(val: String) -> Self {
|
|
|
|
|
Value::String(val)
|
|
|
|
|
JsonValue::String(val)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<char> for Value {
|
|
|
|
|
impl From<char> for JsonValue {
|
|
|
|
|
fn from(val: char) -> Self {
|
|
|
|
|
Value::String(val.into())
|
|
|
|
|
JsonValue::String(val.into())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T> From<Option<T>> for Value
|
|
|
|
|
impl<T> From<Option<T>> for JsonValue
|
|
|
|
|
where
|
|
|
|
|
T: CrdtNode,
|
|
|
|
|
{
|
|
|
|
|
fn from(val: Option<T>) -> Self {
|
|
|
|
|
match val {
|
|
|
|
|
Some(x) => x.view(),
|
|
|
|
|
None => Value::Null,
|
|
|
|
|
None => JsonValue::Null,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T> From<Vec<T>> for Value
|
|
|
|
|
impl<T> From<Vec<T>> for JsonValue
|
|
|
|
|
where
|
|
|
|
|
T: CrdtNode,
|
|
|
|
|
{
|
|
|
|
|
fn from(value: Vec<T>) -> Self {
|
|
|
|
|
Value::Array(value.iter().map(|x| x.view()).collect())
|
|
|
|
|
JsonValue::Array(value.iter().map(|x| x.view()).collect())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Fallibly create a CRDT Node from a JSON Value
|
|
|
|
|
pub trait CrdtNodeFromValue: Sized {
|
|
|
|
|
fn node_from(value: Value, id: AuthorId, path: Vec<PathSegment>) -> Result<Self, String>;
|
|
|
|
|
fn node_from(value: JsonValue, id: AuthorId, path: Vec<PathSegment>) -> Result<Self, String>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Fallibly cast a JSON Value into a CRDT Node
|
|
|
|
|
@@ -416,7 +418,7 @@ pub trait IntoCrdtNode<T>: Sized {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// [`CrdtNodeFromValue`] implies [`IntoCRDTNode<T>`]
|
|
|
|
|
impl<T> IntoCrdtNode<T> for Value
|
|
|
|
|
impl<T> IntoCrdtNode<T> for JsonValue
|
|
|
|
|
where
|
|
|
|
|
T: CrdtNodeFromValue,
|
|
|
|
|
{
|
|
|
|
|
@@ -426,16 +428,16 @@ where
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Trivial conversion from Value to Value as CrdtNodeFromValue
|
|
|
|
|
impl CrdtNodeFromValue for Value {
|
|
|
|
|
fn node_from(value: Value, _id: AuthorId, _path: Vec<PathSegment>) -> Result<Self, String> {
|
|
|
|
|
impl CrdtNodeFromValue for JsonValue {
|
|
|
|
|
fn node_from(value: JsonValue, _id: AuthorId, _path: Vec<PathSegment>) -> Result<Self, String> {
|
|
|
|
|
Ok(value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Conversions from primitives to CRDTs
|
|
|
|
|
impl CrdtNodeFromValue for bool {
|
|
|
|
|
fn node_from(value: Value, _id: AuthorId, _path: Vec<PathSegment>) -> Result<Self, String> {
|
|
|
|
|
if let Value::Bool(x) = value {
|
|
|
|
|
fn node_from(value: JsonValue, _id: AuthorId, _path: Vec<PathSegment>) -> Result<Self, String> {
|
|
|
|
|
if let JsonValue::Bool(x) = value {
|
|
|
|
|
Ok(x)
|
|
|
|
|
} else {
|
|
|
|
|
Err(format!("failed to convert {value:?} -> bool"))
|
|
|
|
|
@@ -444,8 +446,8 @@ impl CrdtNodeFromValue for bool {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CrdtNodeFromValue for f64 {
|
|
|
|
|
fn node_from(value: Value, _id: AuthorId, _path: Vec<PathSegment>) -> Result<Self, String> {
|
|
|
|
|
if let Value::Number(x) = value {
|
|
|
|
|
fn node_from(value: JsonValue, _id: AuthorId, _path: Vec<PathSegment>) -> Result<Self, String> {
|
|
|
|
|
if let JsonValue::Number(x) = value {
|
|
|
|
|
Ok(x)
|
|
|
|
|
} else {
|
|
|
|
|
Err(format!("failed to convert {value:?} -> f64"))
|
|
|
|
|
@@ -454,8 +456,8 @@ impl CrdtNodeFromValue for f64 {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CrdtNodeFromValue for i64 {
|
|
|
|
|
fn node_from(value: Value, _id: AuthorId, _path: Vec<PathSegment>) -> Result<Self, String> {
|
|
|
|
|
if let Value::Number(x) = value {
|
|
|
|
|
fn node_from(value: JsonValue, _id: AuthorId, _path: Vec<PathSegment>) -> Result<Self, String> {
|
|
|
|
|
if let JsonValue::Number(x) = value {
|
|
|
|
|
Ok(x as i64)
|
|
|
|
|
} else {
|
|
|
|
|
Err(format!("failed to convert {value:?} -> f64"))
|
|
|
|
|
@@ -464,8 +466,8 @@ impl CrdtNodeFromValue for i64 {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CrdtNodeFromValue for String {
|
|
|
|
|
fn node_from(value: Value, _id: AuthorId, _path: Vec<PathSegment>) -> Result<Self, String> {
|
|
|
|
|
if let Value::String(x) = value {
|
|
|
|
|
fn node_from(value: JsonValue, _id: AuthorId, _path: Vec<PathSegment>) -> Result<Self, String> {
|
|
|
|
|
if let JsonValue::String(x) = value {
|
|
|
|
|
Ok(x)
|
|
|
|
|
} else {
|
|
|
|
|
Err(format!("failed to convert {value:?} -> String"))
|
|
|
|
|
@@ -474,8 +476,8 @@ impl CrdtNodeFromValue for String {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CrdtNodeFromValue for char {
|
|
|
|
|
fn node_from(value: Value, _id: AuthorId, _path: Vec<PathSegment>) -> Result<Self, String> {
|
|
|
|
|
if let Value::String(x) = value.clone() {
|
|
|
|
|
fn node_from(value: JsonValue, _id: AuthorId, _path: Vec<PathSegment>) -> Result<Self, String> {
|
|
|
|
|
if let JsonValue::String(x) = value.clone() {
|
|
|
|
|
x.chars().next().ok_or(format!(
|
|
|
|
|
"failed to convert {value:?} -> char: found a zero-length string"
|
|
|
|
|
))
|
|
|
|
|
@@ -489,7 +491,7 @@ impl<T> CrdtNodeFromValue for LwwRegisterCrdt<T>
|
|
|
|
|
where
|
|
|
|
|
T: CrdtNode,
|
|
|
|
|
{
|
|
|
|
|
fn node_from(value: Value, id: AuthorId, path: Vec<PathSegment>) -> Result<Self, String> {
|
|
|
|
|
fn node_from(value: JsonValue, id: AuthorId, path: Vec<PathSegment>) -> Result<Self, String> {
|
|
|
|
|
let mut crdt = LwwRegisterCrdt::new(id, path);
|
|
|
|
|
crdt.set(value);
|
|
|
|
|
Ok(crdt)
|
|
|
|
|
@@ -500,8 +502,8 @@ impl<T> CrdtNodeFromValue for ListCrdt<T>
|
|
|
|
|
where
|
|
|
|
|
T: CrdtNode,
|
|
|
|
|
{
|
|
|
|
|
fn node_from(value: Value, id: AuthorId, path: Vec<PathSegment>) -> Result<Self, String> {
|
|
|
|
|
if let Value::Array(arr) = value {
|
|
|
|
|
fn node_from(value: JsonValue, id: AuthorId, path: Vec<PathSegment>) -> Result<Self, String> {
|
|
|
|
|
if let JsonValue::Array(arr) = value {
|
|
|
|
|
let mut crdt = ListCrdt::new(id, path);
|
|
|
|
|
let result: Result<(), String> =
|
|
|
|
|
arr.into_iter().enumerate().try_for_each(|(i, val)| {
|
|
|
|
|
@@ -521,7 +523,7 @@ mod test {
|
|
|
|
|
use serde_json::json;
|
|
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
|
json_crdt::{add_crdt_fields, BaseCrdt, CrdtNode, IntoCrdtNode, OpState, Value},
|
|
|
|
|
json_crdt::{add_crdt_fields, BaseCrdt, CrdtNode, IntoCrdtNode, JsonValue, OpState},
|
|
|
|
|
keypair::make_keypair,
|
|
|
|
|
list_crdt::ListCrdt,
|
|
|
|
|
lww_crdt::LwwRegisterCrdt,
|
|
|
|
|
@@ -697,7 +699,7 @@ mod test {
|
|
|
|
|
.set(3000.0)
|
|
|
|
|
.sign_with_dependencies(&kp1, vec![&_add_money]);
|
|
|
|
|
|
|
|
|
|
let sword: Value = json!({
|
|
|
|
|
let sword: JsonValue = json!({
|
|
|
|
|
"name": "Sword",
|
|
|
|
|
"soulbound": true,
|
|
|
|
|
})
|
|
|
|
|
@@ -748,8 +750,8 @@ mod test {
|
|
|
|
|
let mut base2 = BaseCrdt::<Game>::new(&kp2);
|
|
|
|
|
|
|
|
|
|
// init a 2d grid
|
|
|
|
|
let row0: Value = json!([true, false]).into();
|
|
|
|
|
let row1: Value = json!([false, true]).into();
|
|
|
|
|
let row0: JsonValue = json!([true, false]).into();
|
|
|
|
|
let row1: JsonValue = json!([false, true]).into();
|
|
|
|
|
let construct1 = base1.doc.grid.insert_idx(0, row0).sign(&kp1);
|
|
|
|
|
let construct2 = base1.doc.grid.insert_idx(1, row1).sign(&kp1);
|
|
|
|
|
|
|
|
|
|
@@ -800,13 +802,13 @@ mod test {
|
|
|
|
|
#[add_crdt_fields]
|
|
|
|
|
#[derive(Clone, CrdtNode)]
|
|
|
|
|
struct Test {
|
|
|
|
|
reg: LwwRegisterCrdt<Value>,
|
|
|
|
|
reg: LwwRegisterCrdt<JsonValue>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let kp1 = make_keypair();
|
|
|
|
|
let mut base1 = BaseCrdt::<Test>::new(&kp1);
|
|
|
|
|
|
|
|
|
|
let base_val: Value = json!({
|
|
|
|
|
let base_val: JsonValue = json!({
|
|
|
|
|
"a": true,
|
|
|
|
|
"b": "asdf",
|
|
|
|
|
"c": {
|
|
|
|
|
@@ -856,11 +858,11 @@ mod test {
|
|
|
|
|
assert_eq!(crdt.doc.reg.view(), json!(true).into());
|
|
|
|
|
|
|
|
|
|
// set nested
|
|
|
|
|
let mut list_view: Value = crdt.doc.strct.view().into();
|
|
|
|
|
let mut list_view: JsonValue = crdt.doc.strct.view().into();
|
|
|
|
|
assert_eq!(list_view, json!([]).into());
|
|
|
|
|
|
|
|
|
|
// only keeps actual numbers
|
|
|
|
|
let list: Value = json!({"list": [0, 123, -0.45, "char", []]}).into();
|
|
|
|
|
let list: JsonValue = json!({"list": [0, 123, -0.45, "char", []]}).into();
|
|
|
|
|
crdt.doc.strct.insert_idx(0, list);
|
|
|
|
|
list_view = crdt.doc.strct.view().into();
|
|
|
|
|
assert_eq!(list_view, json!([{ "list": [0, 123, -0.45]}]).into());
|
|
|
|
|
|