diff --git a/README.md b/README.md index a6222644..e200357f 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,7 @@ The built-in blocks provided by Protoflow are listed below: | [`EncodeJSON`] | Encodes messages into JSON format. | | [`Hash`] | Computes the cryptographic hash of a byte stream. | | [`Random`] | Generates and sends a random value. | +| [`RandomInt`] | Generates and sends a random number. | | [`ReadDir`] | Reads file names from a file system directory. | | [`ReadEnv`] | Reads the value of an environment variable. | | [`ReadFile`] | Reads bytes from the contents of a file. | @@ -503,6 +504,27 @@ block-beta protoflow execute Random seed=42 ``` +#### [`RandomInt`] + +A block for generating and sending a random number. + +```mermaid +block-beta + columns 4 + RandomInt space:2 Sink + RandomInt-- "output" -->Sink + + classDef block height:48px,padding:8px; + classDef hidden visibility:none; + class RandomInt block + class Sink hidden +``` + +```bash +protoflow execute RandomInt min=0 max=100 +protoflow execute RandomInt seed=42 min=0 max=100 +``` + #### [`ReadDir`] A block that reads file names from a file system directory. @@ -810,6 +832,7 @@ To add a new block type implementation, make sure to examine and amend: [`EncodeJSON`]: https://docs.rs/protoflow-blocks/latest/protoflow_blocks/struct.EncodeJson.html [`Hash`]: https://docs.rs/protoflow-blocks/latest/protoflow_blocks/struct.Hash.html [`Random`]: https://docs.rs/protoflow-blocks/latest/protoflow_blocks/struct.Random.html +[`RandomInt`]: https://docs.rs/protoflow-blocks/latest/protoflow_blocks/struct.RandomInt.html [`ReadDir`]: https://docs.rs/protoflow-blocks/latest/protoflow_blocks/struct.ReadDir.html [`ReadEnv`]: https://docs.rs/protoflow-blocks/latest/protoflow_blocks/struct.ReadEnv.html [`ReadFile`]: https://docs.rs/protoflow-blocks/latest/protoflow_blocks/struct.ReadFile.html diff --git a/lib/protoflow-blocks/Cargo.toml b/lib/protoflow-blocks/Cargo.toml index 52c54740..78a7bb84 100644 --- a/lib/protoflow-blocks/Cargo.toml +++ b/lib/protoflow-blocks/Cargo.toml @@ -67,6 +67,7 @@ struson = "0.5" sysml-model = { version = "=0.2.3", default-features = false, optional = true } ubyte = { version = "0.10", default-features = false } csv = "1.3.1" +rand = "0.9.0-alpha.2" [dev-dependencies] bytes = "1.8.0" diff --git a/lib/protoflow-blocks/doc/core/random_int.mmd b/lib/protoflow-blocks/doc/core/random_int.mmd new file mode 100644 index 00000000..299f92f1 --- /dev/null +++ b/lib/protoflow-blocks/doc/core/random_int.mmd @@ -0,0 +1,9 @@ +block-beta + columns 4 + RandomInt space:2 Sink + RandomInt-- "output" -->Sink + + classDef block height:48px,padding:8px; + classDef hidden visibility:none; + class RandomInt block + class Sink hidden diff --git a/lib/protoflow-blocks/doc/core/random_int.seq.mmd b/lib/protoflow-blocks/doc/core/random_int.seq.mmd new file mode 100644 index 00000000..e9f611c7 --- /dev/null +++ b/lib/protoflow-blocks/doc/core/random_int.seq.mmd @@ -0,0 +1,12 @@ +sequenceDiagram + autonumber + participant RandomInt as RandomInt block + participant RandomInt.output as RandomInt.output port + participant BlockA as Another block + + RandomInt-->>BlockA: Connect + + RandomInt->>BlockA: Random number + + RandomInt-->>RandomInt.output: Close + RandomInt-->>BlockA: Disconnect diff --git a/lib/protoflow-blocks/src/block_config.rs b/lib/protoflow-blocks/src/block_config.rs index 8e32c0f0..568ebda6 100644 --- a/lib/protoflow-blocks/src/block_config.rs +++ b/lib/protoflow-blocks/src/block_config.rs @@ -57,7 +57,7 @@ impl<'de> serde::Deserialize<'de> for BlockConfig { tag, value: Value::Mapping(_mapping), } => Ok(match tag.string.as_str() { - "Buffer" | "Const" | "Count" | "Delay" | "Drop" | "Random" => { + "Buffer" | "Const" | "Count" | "Delay" | "Drop" | "Random" | "RandomInt" => { CoreBlockConfig::deserialize(value.clone()) .map(BlockConfig::Core) .unwrap() diff --git a/lib/protoflow-blocks/src/block_tag.rs b/lib/protoflow-blocks/src/block_tag.rs index 8b97ebe7..0beef9aa 100644 --- a/lib/protoflow-blocks/src/block_tag.rs +++ b/lib/protoflow-blocks/src/block_tag.rs @@ -17,6 +17,7 @@ pub enum BlockTag { Delay, Drop, Random, + RandomInt, // FlowBlocks // HashBlocks #[cfg(any( @@ -78,6 +79,7 @@ impl BlockTag { Delay => "Delay", Drop => "Drop", Random => "Random", + RandomInt => "RandomInt", #[cfg(any( feature = "hash-blake3", feature = "hash-md5", @@ -129,6 +131,7 @@ impl FromStr for BlockTag { "Delay" => Delay, "Drop" => Drop, "Random" => Random, + "RandomInt" => RandomInt, #[cfg(any( feature = "hash-blake3", feature = "hash-md5", @@ -191,6 +194,7 @@ impl BlockInstantiation for BlockTag { Delay => Box::new(super::Delay::::with_system(system, None)), Drop => Box::new(super::Drop::::with_system(system)), Random => Box::new(super::Random::::with_system(system, None)), + RandomInt => Box::new(super::RandomInt::with_system(system, None, None, None)), #[cfg(any( feature = "hash-blake3", feature = "hash-md5", diff --git a/lib/protoflow-blocks/src/blocks/core.rs b/lib/protoflow-blocks/src/blocks/core.rs index 15557ba0..a1351072 100644 --- a/lib/protoflow-blocks/src/blocks/core.rs +++ b/lib/protoflow-blocks/src/blocks/core.rs @@ -35,6 +35,15 @@ pub mod core { fn random(&mut self) -> Random; fn random_seeded(&mut self, seed: Option) -> Random; + + fn random_int(&mut self) -> RandomInt; + + fn random_int_with_params( + &mut self, + seed: Option, + min: Option, + max: Option, + ) -> RandomInt; } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -46,6 +55,7 @@ pub mod core { Delay, Drop, Random, + RandomInt, } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -80,6 +90,12 @@ pub mod core { output: OutputPortName, seed: Option, }, + RandomInt { + output: OutputPortName, + seed: Option, + min: Option, + max: Option, + }, } impl Named for CoreBlockConfig { @@ -92,6 +108,7 @@ pub mod core { Delay { .. } => "Delay", Drop { .. } => "Drop", Random { .. } => "Random", + RandomInt { .. } => "RandomInt", }) } } @@ -108,6 +125,7 @@ pub mod core { Delay { output, .. } => vec![("output", Some(output.clone()))], Drop { .. } => vec![], Random { output, .. } => vec![("output", Some(output.clone()))], + RandomInt { output, .. } => vec![("output", Some(output.clone()))], } } } @@ -137,6 +155,9 @@ pub mod core { Box::new(super::Random::with_params(system.output::(), *seed)) // TODO: Random::with_system(system, *seed)) } + RandomInt { seed, min, max, .. } => { + Box::new(super::RandomInt::with_system(system, *seed, *min, *max)) + } } } } @@ -158,6 +179,9 @@ pub mod core { mod random; pub use random::*; + + mod random_int; + pub use random_int::*; } pub use core::*; diff --git a/lib/protoflow-blocks/src/blocks/core/random_int.rs b/lib/protoflow-blocks/src/blocks/core/random_int.rs new file mode 100644 index 00000000..2f1d11cc --- /dev/null +++ b/lib/protoflow-blocks/src/blocks/core/random_int.rs @@ -0,0 +1,160 @@ +// This is free and unencumbered software released into the public domain. + +use crate::{ + prelude::{format, vec}, + StdioConfig, StdioError, StdioSystem, System, +}; +use protoflow_core::{Block, BlockError, BlockResult, BlockRuntime, OutputPort}; +use protoflow_derive::Block; +use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; +use simple_mermaid::mermaid; + +/// A block for generating and sending a random number. +/// +/// # Block Diagram +#[doc = mermaid!("../../../doc/core/random_int.mmd")] +/// +/// # Sequence Diagram +#[doc = mermaid!("../../../doc/core/random_int.seq.mmd" framed)] +/// +/// # Examples +/// +/// ## Using the block in a system +/// +/// ```rust +/// # use protoflow_blocks::*; +/// # fn main() { +/// System::build(|s| { +/// let config = StdioConfig { +/// encoding: Default::default(), +/// params: Default::default(), +/// }; +/// let random_int = s.random_int(); +/// let number_encoder = s.encode_with::(config.encoding); +/// let stdout = s.write_stdout(); +/// s.connect(&random_int.output, &number_encoder.input); +/// s.connect(&number_encoder.output, &stdout.input); +/// }); +/// # } +/// ``` +/// +/// ## Running the block via the CLI +/// +/// ```console +/// $ protoflow execute RandomInt +/// ``` +/// +/// ```console +/// $ protoflow execute RandomInt seed=42 min=0 max=100 +/// ``` +/// +#[derive(Block, Clone)] +pub struct RandomInt { + /// The port to send the value on. + #[output] + pub output: OutputPort, + /// A parameter for the random seed to use. + #[parameter] + pub seed: Option, + /// A parameter for the random min to use. + #[parameter] + pub min: Option, + /// A parameter for the random max to use. + #[parameter] + pub max: Option, +} + +impl RandomInt { + pub fn new(output: OutputPort) -> Self { + Self::with_params(output, None, None, None) + } + + pub fn with_params( + output: OutputPort, + seed: Option, + min: Option, + max: Option, + ) -> Self { + Self { + output, + seed, + min, + max, + } + } + + pub fn with_system( + system: &System, + seed: Option, + min: Option, + max: Option, + ) -> Self { + use crate::SystemBuilding; + Self::with_params(system.output(), seed, min, max) + } +} + +impl Block for RandomInt { + fn execute(&mut self, runtime: &dyn BlockRuntime) -> BlockResult { + runtime.wait_for(&self.output)?; + + let mut rng = if let Some(seed) = self.seed { + StdRng::seed_from_u64(seed) + } else { + let mut thread_rng = rand::rng(); + StdRng::from_rng(&mut thread_rng) + }; + + let min = self.min.unwrap_or(i64::MIN); + let max = self.max.unwrap_or(i64::MAX); + + if min >= max { + return Err(BlockError::Other(format!( + "Invalid range: min ({}) must be less than max ({})", + min, max + ))); + } + + let random_value = rng.random_range(min..max); + + self.output.send(&random_value)?; + + Ok(()) + } +} + +#[cfg(feature = "std")] +impl StdioSystem for RandomInt { + fn build_system(config: StdioConfig) -> Result { + use crate::{CoreBlocks, IoBlocks, SystemBuilding}; + + config.allow_only(vec!["seed", "min", "max"])?; + let seed = config.get_opt::("seed")?; + let min = config.get_opt::("min")?; + let max = config.get_opt::("max")?; + + Ok(System::build(|s| { + let random_int = s.random_int_with_params(seed, min, max); + let number_encoder = s.encode_with::(config.encoding); + let stdout = config.write_stdout(s); + s.connect(&random_int.output, &number_encoder.input); + s.connect(&number_encoder.output, &stdout.input); + })) + } +} + +#[cfg(test)] +mod tests { + + use super::RandomInt; + use crate::{System, SystemBuilding}; + + #[test] + fn instantiate_block() { + // Check that the block is constructible: + let _ = System::build(|s| { + let _ = s.block(RandomInt::new(s.output())); + }); + } +} diff --git a/lib/protoflow-blocks/src/lib.rs b/lib/protoflow-blocks/src/lib.rs index 30050a0b..7625984f 100644 --- a/lib/protoflow-blocks/src/lib.rs +++ b/lib/protoflow-blocks/src/lib.rs @@ -59,6 +59,7 @@ pub fn build_stdio_system( "Delay" => Delay::::build_system(config)?, "Drop" => Drop::::build_system(config)?, "Random" => Random::::build_system(config)?, + "RandomInt" => RandomInt::build_system(config)?, // FlowBlocks // HashBlocks #[cfg(any( diff --git a/lib/protoflow-blocks/src/system.rs b/lib/protoflow-blocks/src/system.rs index 695ccfba..f607cabd 100644 --- a/lib/protoflow-blocks/src/system.rs +++ b/lib/protoflow-blocks/src/system.rs @@ -7,8 +7,8 @@ use crate::{ types::{DelayType, Encoding}, AllBlocks, Buffer, ConcatStrings, Const, CoreBlocks, Count, Decode, DecodeCsv, DecodeHex, DecodeJson, Delay, Drop, Encode, EncodeCsv, EncodeHex, EncodeJson, FlowBlocks, HashBlocks, - IoBlocks, MathBlocks, Random, ReadDir, ReadEnv, ReadFile, ReadSocket, ReadStdin, SplitString, - SysBlocks, TextBlocks, WriteFile, WriteSocket, WriteStderr, WriteStdout, + IoBlocks, MathBlocks, Random, RandomInt, ReadDir, ReadEnv, ReadFile, ReadSocket, ReadStdin, + SplitString, SysBlocks, TextBlocks, WriteFile, WriteSocket, WriteStderr, WriteStdout, }; use protoflow_core::{ Block, BlockID, BlockResult, BoxedBlockType, InputPort, Message, OutputPort, PortID, @@ -161,6 +161,19 @@ impl CoreBlocks for System { fn random_seeded(&mut self, seed: Option) -> Random { self.0.block(Random::::with_system(self, seed)) } + + fn random_int(&mut self) -> RandomInt { + self.0.block(RandomInt::with_system(self, None, None, None)) + } + + fn random_int_with_params( + &mut self, + seed: Option, + min: Option, + max: Option, + ) -> RandomInt { + self.0.block(RandomInt::with_system(self, seed, min, max)) + } } impl FlowBlocks for System {}