Skip to content

Commit 0410802

Browse files
committed
added macro to support per language documentation for the BinaryOptionsToolsUni crate
1 parent 32e79c2 commit 0410802

22 files changed

Lines changed: 608 additions & 542 deletions

File tree

BinaryOptionsToolsUni/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
/target
2+
nul

BinaryOptionsToolsUni/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ keywords = ["binary-options", "uniffi", "cross-platform", "trading"]
1111
categories = ["api-bindings"]
1212
license-file = "../LICENSE"
1313

14+
[features]
15+
default = ["python", "swift", "go"]
16+
kotlin = []
17+
swift = []
18+
csharp = []
19+
go = []
20+
ruby = []
21+
python = []
22+
javascript = []
23+
1424
[[bin]]
1525
# This can be whatever name makes sense for your project, but the rest of this tutorial assumes uniffi-bindgen.
1626
name = "uniffi-bindgen"
@@ -30,6 +40,7 @@ rust_decimal = "1.37.2"
3040
futures-util = "0.3.31"
3141
uuid = { version = "1.10.0", features = ["v4", "serde", "fast-rng"] }
3242
regex = "1.12.2"
43+
bo2_macros = { version = "0.1.0", path = "bo2_macros" }
3344

3445
[build-dependencies]
3546
uniffi = { version = "0.31.0", features = ["build"] }
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "bo2_macros"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[lib]
7+
proc-macro = true
8+
9+
[dependencies]
10+
serde_json = "1.0.149"
11+
zyn = "0.5.3"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use std::collections::HashMap;
2+
3+
#[derive(zyn::Attribute)]
4+
pub struct UniffiDocArgs {
5+
name: Option<String>,
6+
path: String,
7+
}
8+
9+
#[zyn::element]
10+
pub fn uniffi_doc(#[zyn(input)]code: zyn::syn::Item, args: UniffiDocArgs) -> zyn::TokenStream {
11+
let path = std::path::Path::new(&args.path);
12+
let content = std::fs::read_to_string(path).expect("Failed to read documentation file");
13+
let data: HashMap<String, String> = match &args.name {
14+
Some(name) => {
15+
let all_data: HashMap<String, HashMap<String, String>> = serde_json::from_str(&content).expect("Failed to parse documentation JSON");
16+
all_data.get(name).cloned().expect(&format!("Documentation for '{}' not found in JSON", name))
17+
}
18+
None => serde_json::from_str(&content).expect("Failed to parse documentation JSON"),
19+
};
20+
21+
let default = data.get("default").map(|s| String::from(s) + "\n");
22+
23+
zyn::zyn! {
24+
@if (default.is_some()) {
25+
#[doc = {{ default.unwrap() }}]
26+
}
27+
@for (element in data.into_iter()) {
28+
@if (element.0 != "default") {
29+
#[cfg_attr(feature = {{ element.0 }}, doc = {{ element.1 }})]
30+
}
31+
}
32+
{{ code }}
33+
}
34+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
mod doc;
2+
3+
use doc::{UniffiDoc, UniffiDocArgs};
4+
5+
use zyn::{Args, syn::spanned::Spanned};
6+
7+
#[zyn::attribute]
8+
pub fn uniffi_doc(args: Args) -> zyn::TokenStream {
9+
let args = match UniffiDocArgs::from_args(&args) {
10+
Ok(args) => args,
11+
Err(e) => {
12+
bail!("Invalid arguments for uniffi_doc: {}", e.to_string());
13+
}
14+
};
15+
zyn::zyn! {
16+
@uniffi_doc(args = args)
17+
}
18+
}

BinaryOptionsToolsUni/docs_json/error.json

Lines changed: 57 additions & 0 deletions
Large diffs are not rendered by default.

BinaryOptionsToolsUni/docs_json/pocket_option.json

Lines changed: 75 additions & 0 deletions
Large diffs are not rendered by default.

BinaryOptionsToolsUni/docs_json/raw_handler.json

Lines changed: 48 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"SubscriptionStream": {
3+
"default": "# SubscriptionStream\n\nA poll-based stream of real-time candle data for a subscribed asset.\n\nReturned by `PocketOption.subscribe()`. Since UniFFI does not natively support async iterators or streams, this type exposes a single `next()` method that you call repeatedly in a loop to receive each new candle as it closes.\n\n## Methods\n\n- `next()` — Await the next candle from the stream\n\n## Lifecycle\n\n- The stream is active as long as the underlying subscription is alive.\n- Calling `PocketOption.unsubscribe(asset)` on the parent client will close the stream.\n- Once closed, `next()` will return an error.\n- You can have multiple active `SubscriptionStream` instances for different assets simultaneously.\n\n## Candle Timing\n\nCandles are emitted when they **close** (i.e. at the end of each period). A 5-second candle stream will emit one candle every 5 seconds. The `timestamp` field on each `Candle` reflects the **open** time of that candle.\n\n## Error Handling\n\n`next()` returns a `Result` / throws an exception in languages with exception-based error handling. Always wrap calls in appropriate error handling — the stream may be closed due to disconnection, and while the underlying client auto-reconnects, the subscription may need to be re-established.\n",
4+
"python": "## Python\n\n```python\nimport asyncio\nfrom binaryoptionstoolsuni import PocketOption\n\nasync def main():\n api = await PocketOption.init(\"YOUR_SESSION_ID\")\n\n # Subscribe to 5-second candles for EUR/USD OTC\n stream = await api.subscribe(\"EURUSD_otc\", 5)\n\n # Basic loop — runs until interrupted or stream closes\n try:\n while True:\n candle = await stream.next()\n direction = \"BULL\" if candle.close > candle.open else \"BEAR\"\n print(\n f\"[{candle.timestamp}] {candle.symbol} {direction} \"\n f\"O={candle.open:.5f} H={candle.high:.5f} \"\n f\"L={candle.low:.5f} C={candle.close:.5f}\"\n )\n except Exception as e:\n print(f\"Stream ended: {e}\")\n finally:\n await api.unsubscribe(\"EURUSD_otc\")\n await api.shutdown()\n\n # Multiple streams in parallel\n stream_eur = await api.subscribe(\"EURUSD_otc\", 5)\n stream_gbp = await api.subscribe(\"GBPUSD_otc\", 5)\n\n async def consume(stream, label):\n for _ in range(5):\n c = await stream.next()\n print(f\"[{label}] {c.close:.5f}\")\n\n await asyncio.gather(\n consume(stream_eur, \"EUR\"),\n consume(stream_gbp, \"GBP\"),\n )\n\nasyncio.run(main())\n```\n",
5+
"kotlin": "## Kotlin\n\n```kotlin\nimport uniffi.binaryoptionstoolsuni.PocketOption\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\n\nfun main() = runBlocking {\n val api = PocketOption.init(\"YOUR_SESSION_ID\")\n\n // Subscribe to 5-second candles\n val stream = api.subscribe(\"EURUSD_otc\", 5u)\n\n // Basic loop\n try {\n while (true) {\n val candle = stream.next()\n val direction = if (candle.close > candle.open) \"BULL\" else \"BEAR\"\n println(\n \"[${candle.timestamp}] ${candle.symbol} $direction \" +\n \"O=${candle.open} H=${candle.high} L=${candle.low} C=${candle.close}\"\n )\n }\n } catch (e: Exception) {\n println(\"Stream ended: $e\")\n } finally {\n api.unsubscribe(\"EURUSD_otc\")\n api.shutdown()\n }\n\n // Multiple streams in parallel\n val streamEur = api.subscribe(\"EURUSD_otc\", 5u)\n val streamGbp = api.subscribe(\"GBPUSD_otc\", 5u)\n\n val jobs = listOf(\n async {\n repeat(5) {\n val c = streamEur.next()\n println(\"[EUR] ${c.close}\")\n }\n },\n async {\n repeat(5) {\n val c = streamGbp.next()\n println(\"[GBP] ${c.close}\")\n }\n }\n )\n jobs.awaitAll()\n}\n```\n",
6+
"swift": "## Swift\n\n```swift\nimport binaryoptionstoolsuni\n\nfunc run() async throws {\n let api = try await PocketOption.init(ssid: \"YOUR_SESSION_ID\")\n\n // Subscribe to 5-second candles\n let stream = try await api.subscribe(asset: \"EURUSD_otc\", durationSecs: 5)\n\n // Basic loop\n do {\n while true {\n let candle = try await stream.next()\n let direction = candle.close > candle.open ? \"BULL\" : \"BEAR\"\n print(\n \"[\\(candle.timestamp)] \\(candle.symbol) \\(direction) \" +\n \"O=\\(candle.open) H=\\(candle.high) L=\\(candle.low) C=\\(candle.close)\"\n )\n }\n } catch {\n print(\"Stream ended: \\(error)\")\n }\n\n try await api.unsubscribe(asset: \"EURUSD_otc\")\n try await api.shutdown()\n\n // Multiple streams in parallel using async let\n let streamEur = try await api.subscribe(asset: \"EURUSD_otc\", durationSecs: 5)\n let streamGbp = try await api.subscribe(asset: \"GBPUSD_otc\", durationSecs: 5)\n\n async let eurTask: Void = {\n for _ in 0..<5 {\n let c = try await streamEur.next()\n print(\"[EUR] \\(c.close)\")\n }\n }()\n\n async let gbpTask: Void = {\n for _ in 0..<5 {\n let c = try await streamGbp.next()\n print(\"[GBP] \\(c.close)\")\n }\n }()\n\n _ = try await (eurTask, gbpTask)\n}\n```\n",
7+
"csharp": "## C#\n\n```csharp\nusing UniFFI.BinaryOptionsToolsUni;\n\nvar api = await PocketOption.Init(\"YOUR_SESSION_ID\");\n\n// Subscribe to 5-second candles\nvar stream = await api.Subscribe(\"EURUSD_otc\", 5);\n\n// Basic loop\ntry {\n while (true) {\n var candle = await stream.Next();\n var direction = candle.Close > candle.Open ? \"BULL\" : \"BEAR\";\n Console.WriteLine(\n $\"[{candle.Timestamp}] {candle.Symbol} {direction} \" +\n $\"O={candle.Open} H={candle.High} L={candle.Low} C={candle.Close}\"\n );\n }\n} catch (Exception e) {\n Console.WriteLine($\"Stream ended: {e.Message}\");\n} finally {\n await api.Unsubscribe(\"EURUSD_otc\");\n await api.Shutdown();\n}\n\n// Multiple streams in parallel\nvar streamEur = await api.Subscribe(\"EURUSD_otc\", 5);\nvar streamGbp = await api.Subscribe(\"GBPUSD_otc\", 5);\n\nvar eurTask = Task.Run(async () => {\n for (int i = 0; i < 5; i++) {\n var c = await streamEur.Next();\n Console.WriteLine($\"[EUR] {c.Close}\");\n }\n});\n\nvar gbpTask = Task.Run(async () => {\n for (int i = 0; i < 5; i++) {\n var c = await streamGbp.Next();\n Console.WriteLine($\"[GBP] {c.Close}\");\n }\n});\n\nawait Task.WhenAll(eurTask, gbpTask);\n```\n",
8+
"go": "## Go\n\n```go\npackage main\n\nimport (\n \"fmt\"\n \"sync\"\n bot \"github.com/ChipaDevTeam/BinaryOptionsTools-v2\"\n)\n\nfunc main() {\n api, err := bot.PocketOptionInit(\"YOUR_SESSION_ID\")\n if err != nil { panic(err) }\n defer api.Shutdown()\n\n // Subscribe to 5-second candles\n stream, err := api.Subscribe(\"EURUSD_otc\", 5)\n if err != nil { panic(err) }\n\n // Basic loop\n for {\n candle, err := stream.Next()\n if err != nil {\n fmt.Printf(\"Stream ended: %v\\n\", err)\n break\n }\n dir := \"BULL\"\n if candle.Close < candle.Open {\n dir = \"BEAR\"\n }\n fmt.Printf(\n \"[%d] %s %s O=%f H=%f L=%f C=%f\\n\",\n candle.Timestamp, candle.Symbol, dir,\n candle.Open, candle.High, candle.Low, candle.Close,\n )\n }\n api.Unsubscribe(\"EURUSD_otc\")\n\n // Multiple streams in parallel\n streamEur, _ := api.Subscribe(\"EURUSD_otc\", 5)\n streamGbp, _ := api.Subscribe(\"GBPUSD_otc\", 5)\n\n var wg sync.WaitGroup\n\n consume := func(s bot.SubscriptionStream, label string) {\n defer wg.Done()\n for i := 0; i < 5; i++ {\n c, err := s.Next()\n if err != nil { return }\n fmt.Printf(\"[%s] %f\\n\", label, c.Close)\n }\n }\n\n wg.Add(2)\n go consume(streamEur, \"EUR\")\n go consume(streamGbp, \"GBP\")\n wg.Wait()\n}\n```\n",
9+
"ruby": "## Ruby\n\n```ruby\nrequire 'binary_options_tools_uni'\nrequire 'async'\n\napi = BinaryOptionsToolsUni::PocketOption.init('YOUR_SESSION_ID')\n\n# Subscribe to 5-second candles\nstream = api.subscribe('EURUSD_otc', 5)\n\n# Basic loop\nbegin\n loop do\n candle = stream.next\n dir = candle.close > candle.open ? 'BULL' : 'BEAR'\n puts \"[#{candle.timestamp}] #{candle.symbol} #{dir} \" \\\n \"O=#{candle.open} H=#{candle.high} L=#{candle.low} C=#{candle.close}\"\n end\nrescue => e\n puts \"Stream ended: #{e}\"\nensure\n api.unsubscribe('EURUSD_otc')\n api.shutdown\nend\n\n# Multiple streams using threads\nstream_eur = api.subscribe('EURUSD_otc', 5)\nstream_gbp = api.subscribe('GBPUSD_otc', 5)\n\neur_thread = Thread.new do\n 5.times do\n c = stream_eur.next\n puts \"[EUR] #{c.close}\"\n end\nend\n\ngbp_thread = Thread.new do\n 5.times do\n c = stream_gbp.next\n puts \"[GBP] #{c.close}\"\n end\nend\n\neur_thread.join\ngbp_thread.join\n```\n",
10+
"javascript": "## JavaScript\n\n```javascript\nimport { PocketOption } from 'binary-options-tools-uni';\n\nasync function main() {\n const api = await PocketOption.init('YOUR_SESSION_ID');\n\n // Subscribe to 5-second candles\n const stream = await api.subscribe('EURUSD_otc', 5);\n\n // Basic loop\n try {\n while (true) {\n const candle = await stream.next();\n const direction = candle.close > candle.open ? 'BULL' : 'BEAR';\n console.log(\n `[${candle.timestamp}] ${candle.symbol} ${direction} ` +\n `O=${candle.open} H=${candle.high} L=${candle.low} C=${candle.close}`\n );\n }\n } catch (e) {\n console.log(`Stream ended: ${e}`);\n } finally {\n await api.unsubscribe('EURUSD_otc');\n await api.shutdown();\n }\n\n // Multiple streams in parallel\n const streamEur = await api.subscribe('EURUSD_otc', 5);\n const streamGbp = await api.subscribe('GBPUSD_otc', 5);\n\n const consume = async (s, label, count) => {\n for (let i = 0; i < count; i++) {\n const c = await s.next();\n console.log(`[${label}] ${c.close}`);\n }\n };\n\n await Promise.all([\n consume(streamEur, 'EUR', 5),\n consume(streamGbp, 'GBP', 5),\n ]);\n}\n\nmain();\n```\n"
11+
},
12+
"next": {
13+
"default": "## `next()`\n\nRetrieves the next candle from the subscription stream.\n\nThis call suspends/awaits until a new candle is available. Candles are emitted when their time period closes — for example, a 5-second stream emits one candle every 5 seconds.\n\n### Returns\n\nA `Candle` record containing the OHLCV data for the completed period.\n\n### Errors\n\nReturns an error (or throws) if:\n- The subscription was closed via `PocketOption.unsubscribe()`\n- The connection dropped and could not be recovered\n- An internal channel error occurred\n\n### Notes\n\n- There is no `None`/`null` sentinel value for end-of-stream. Exhaustion is communicated through the error path.\n- Each `next()` call consumes one candle from the internal buffer. If candles arrive faster than you consume them, they are buffered internally.\n- It is safe to call `next()` from multiple concurrent tasks/coroutines on the same stream instance, though in practice a single consumer loop is the typical pattern.\n",
14+
"python": "### Python\n\n```python\n# Single candle\ncandle = await stream.next()\nprint(f\"Close: {candle.close:.5f}\")\n\n# Fixed number of candles\nfor _ in range(100):\n candle = await stream.next()\n print(f\"{candle.timestamp}: {candle.close:.5f}\")\n\n# With error handling\ntry:\n candle = await stream.next()\nexcept Exception as e:\n print(f\"Could not get next candle: {e}\")\n # Re-subscribe if needed\n stream = await api.subscribe(\"EURUSD_otc\", 5)\n candle = await stream.next()\n```\n",
15+
"kotlin": "### Kotlin\n\n```kotlin\n// Single candle\nval candle = stream.next()\nprintln(\"Close: ${candle.close}\")\n\n// Fixed number of candles\nrepeat(100) {\n val c = stream.next()\n println(\"${c.timestamp}: ${c.close}\")\n}\n\n// With error handling\ntry {\n val c = stream.next()\n} catch (e: Exception) {\n println(\"Could not get next candle: $e\")\n // Re-subscribe if needed\n val newStream = api.subscribe(\"EURUSD_otc\", 5u)\n val c2 = newStream.next()\n}\n```\n",
16+
"swift": "### Swift\n\n```swift\n// Single candle\nlet candle = try await stream.next()\nprint(\"Close: \\(candle.close)\")\n\n// Fixed number of candles\nfor _ in 0..<100 {\n let c = try await stream.next()\n print(\"\\(c.timestamp): \\(c.close)\")\n}\n\n// With error handling\ndo {\n let c = try await stream.next()\n print(\"Close: \\(c.close)\")\n} catch {\n print(\"Could not get next candle: \\(error)\")\n // Re-subscribe if needed\n let newStream = try await api.subscribe(asset: \"EURUSD_otc\", durationSecs: 5)\n let c2 = try await newStream.next()\n print(\"Close: \\(c2.close)\")\n}\n```\n",
17+
"csharp": "### C#\n\n```csharp\n// Single candle\nvar candle = await stream.Next();\nConsole.WriteLine($\"Close: {candle.Close}\");\n\n// Fixed number of candles\nfor (int i = 0; i < 100; i++) {\n var c = await stream.Next();\n Console.WriteLine($\"{c.Timestamp}: {c.Close}\");\n}\n\n// With error handling\ntry {\n var c = await stream.Next();\n Console.WriteLine($\"Close: {c.Close}\");\n} catch (Exception e) {\n Console.WriteLine($\"Could not get next candle: {e.Message}\");\n // Re-subscribe if needed\n var newStream = await api.Subscribe(\"EURUSD_otc\", 5);\n var c2 = await newStream.Next();\n Console.WriteLine($\"Close: {c2.Close}\");\n}\n```\n",
18+
"go": "### Go\n\n```go\n// Single candle\ncandle, err := stream.Next()\nif err != nil { panic(err) }\nfmt.Printf(\"Close: %f\\n\", candle.Close)\n\n// Fixed number of candles\nfor i := 0; i < 100; i++ {\n c, err := stream.Next()\n if err != nil { break }\n fmt.Printf(\"%d: %f\\n\", c.Timestamp, c.Close)\n}\n\n// With error handling and re-subscribe\nfor {\n c, err := stream.Next()\n if err != nil {\n fmt.Printf(\"Stream error: %v, re-subscribing...\\n\", err)\n stream, err = api.Subscribe(\"EURUSD_otc\", 5)\n if err != nil { panic(err) }\n continue\n }\n fmt.Printf(\"%d: %f\\n\", c.Timestamp, c.Close)\n}\n```\n",
19+
"javascript": "### JavaScript\n\n```javascript\n// Single candle\nconst candle = await stream.next();\nconsole.log(`Close: ${candle.close}`);\n\n// Fixed number of candles\nfor (let i = 0; i < 100; i++) {\n const c = await stream.next();\n console.log(`${c.timestamp}: ${c.close}`);\n}\n\n// With error handling\ntry {\n const c = await stream.next();\n console.log(`Close: ${c.close}`);\n} catch (e) {\n console.log(`Could not get next candle: ${e}`);\n // Re-subscribe if needed\n const newStream = await api.subscribe('EURUSD_otc', 5);\n const c2 = await newStream.next();\n console.log(`Close: ${c2.close}`);\n}\n```\n"
20+
}
21+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"test": {
3+
"default": "Example of a JSON file for testing purposes.",
4+
"python": "This file can be used to test JSON parsing in Python.",
5+
"javascript": "It can also be used to test JSON parsing in JavaScript."
6+
}
7+
}

0 commit comments

Comments
 (0)