transformation-pipeline-0.1.0/.gitignore01006440001747000174700000000037132170534620016611 0ustar0000000000000000/target/ **/*.rs.bk Cargo.lock transformation-pipeline-0.1.0/.gitlab-ci.yml01006440001747000000000000000417132172635030017233 0ustar0000000000000000image: rust:latest stages: - test - publish test: stage: test script: - cargo test publish: only: - tags stage: publish before_script: - echo "$CARGO_IO_CREDENTIALS" > $CARGO_HOME/credentials script: - cargo package - cargo publish transformation-pipeline-0.1.0/CHANGELOG.adoc01006440001747000000000000003115132172632650016721 0ustar0000000000000000= Changelog // See https://github.com/CodeLenny/changelog-template for information about this format. // :github: :gitlab: :host: https://github.com :owner: rusty-nrf :project: transformation-pipeline :first-commit: d59589979979533f351d27ba926bc8f00d3feccb :latest-version: v0.1.0 ifdef::github[] :repo-url: https://github.com/{owner}/{project} :repo-compare: {repo-url}/compare/ :repo-changelog: {repo-url}/blob/master/CHANGELOG.adoc :compare-split: ... endif::[] ifdef::gitlab[] :repo-url: https://gitlab.com/{owner}/{project} :repo-compare: {repo-url}/compare/ :compare-split: ... endif::[] ifdef::latest-version[] == link:{repo-compare}{latest-version}{compare-split}HEAD[Unreleased] endif::[] ifndef::latest-version[] ifdef::first-commit[] == link:{repo-compare}{first-commit}{compare-split}HEAD[Unreleased] endif::[] ifndef::first-commit[] == Unreleased endif::[] endif::[] === Modified - CI: Moved `publish` job to `publish` stage :version: v0.1.0 :version-date: 2017-12-22 :previous-version: {first-commit} :version-file-url: {repo-url}/tree/{version} :version-diff-url: {repo-compare}{previous-version}{compare-split}{version} :version-log-url: {repo-changelog}#{version}---{version-date} == link:{version-file-url}[{version} - {version-date}] link:{version-file-url}[Code] (link:{version-diff-url}[Diff]) | link:{version-log-url}[Changelog] Initial Release. ### Added - Project description - List of features - Alternatives - Setup for Rust/Cargo - Basics of pipeline stages, pipeline actions, and a pipeline runner. - Testing stage for CI. - Documentation of usage and all potential actions. transformation-pipeline-0.1.0/Cargo.toml.orig01006440001747000174700000000314132172635650017515 0ustar0000000000000000[package] name = "transformation-pipeline" description = "Middleware-esque API for transforming data." version = "0.1.0" authors = ["Ryan Leonard (http://ryanleonard.us)"] license = "MIT" [dependencies] transformation-pipeline-0.1.0/Cargo.toml0000644000000013300012715 0ustar00# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g. crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] name = "transformation-pipeline" version = "0.1.0" authors = ["Ryan Leonard (http://ryanleonard.us)"] description = "Middleware-esque API for transforming data." license = "MIT" [dependencies] transformation-pipeline-0.1.0/README.md01006640001747000174700000007724132172405060016111 0ustar0000000000000000# Transformation Pipeline Middleware-esque API for transforming data. ## Features - Define a custom data type passed between stages - Simple pipeline flow exposed to stages: Abort, Finish (early), Skip - Wrapper functions to run the entire pipeline ### Potential Future Features - Sub-pipelines (e.g. Routers) ## Usage ### Installation Add `transformation-pipeline` to your `Cargo.toml` file, and add it as an external crate. ```rust extern crate transformation_pipeline; ``` ### Define Data Identify or define a datatype that will be passed between stages. This can be a built-in, such as `String`, or a custom `struct`. However, the datatype must have the `Clone` trait defined. ```rust use std::clone::Clone; struct User { name: String, } impl Clone for User { fn clone(&self) -> Self { User { name: self.name.clone(), } } } ``` ### Creating Stages Create `struct`s for each stage of the pipeline, and implement the `PipelineStage` trait for each stage. ```rust use transformation_pipeline::PipelineStage; use transformation_pipeline::StageResult; use transformation_pipeline::StageActions; struct MyPipelineStage {} impl TransformationStage for MyPipelineStage { fn run(&self, previous: User) -> StageResult { let mut name: String = "John ".to_owned(); name.push_str(&previous.name); Ok(StageActions::Next(User { name: name, })) } } ``` See [stage actions](#stage-actions) for more information about the different actions that can be returned. ### Create a Pipeline Now you can assemble a pipeline out of the created stages. Each stage must be put in a `Box`, which is a built-in type. ```rust use transformation_pipeline::TransformationPipeline; let pipeline: TransformationPipeline = TransformationPipeline::new(vec![ Box::new(MyPipelineStage {}), // Add other stages here: // Box::new(MyOtherPipelineStage {}) ]); ``` ### Using the Pipeline Now you can pass data into the pipeline: ```rust let input: User = User { name: "Doe" }; let output = pipeline.run(input).unwrap(); assert_eq!(output.name, "John Doe"); ``` ## Documentation ### Stage Actions Each stage of the pipeline must complete with some "Action". #### Next Action The standard action is "Next", which passes the given data to the next pipeline stage. If the stage is the final stage in the pipeline, the given data is returned as the pipeline result. ```rust Ok(StageActions::Next( /* data for next stage */ )) ``` #### Skip Action A stage can complete with "Skip", which starts the next pipeline stage as if the current stage never existed. This is equivalent to calling: ```rust return Ok(StageActions::Next(previous)); ``` But it can be a little more explicit to what is happening: ```rust if /* action is already completed */ { return Ok(StageActions::Skip); } /* Do action */ Ok(StageActions::Next( /* ... */ )) ``` #### Finish Action A stage can cause the pipeline to immediately complete with the "Finish" action. This returns the given data as the pipeline result, and does not run any further stages. ```rust Ok(StageActions::Finish(/* final data */ )) ``` #### Jump Action A stage can skip subsequent steps in the pipeline with the "Jump" action. This passes the given data to a stage further down the pipeline, and doesn't run any stages in between. ```rust // SAME AS Next(): return Ok(StageActions::Skip(0, /* next data */ )); // Skips 1 stage: return Ok(StageActions::Skip(1, /* data to pass to the 2nd stage */ )); ``` #### Abort Action A stage can complete with the "Abort" action, causing the entire pipeline to abort with an error. ```rust Ok(StageActions::Abort) ``` ## (Anti-)Purpose/Alternatives This package is not designed to: - Handle different data types between stages (e.g. successive maps) - Have multiple functions exposed by pipeline stages (e.g. fancy plugins) [cargo-plugin](https://github.com/Geal/cargo-plugin) may be a better alternative for general-purpose plugins. transformation-pipeline-0.1.0/src/lib.rs01006440001747000174700000000257132170652710016531 0ustar0000000000000000mod stage; mod pipeline; mod result; pub use stage::TransformationStage; pub use pipeline::TransformationPipeline; pub use result::StageActions; pub use result::StageResult; transformation-pipeline-0.1.0/src/pipeline.rs01006440001747000000000000003222132170715470017543 0ustar0000000000000000use std::io; use std::clone::Clone; use stage::TransformationStage; use result::StageActions; pub struct TransformationPipeline { stages: Vec>>, } impl TransformationPipeline { pub fn new(stages: Vec>>) -> TransformationPipeline { TransformationPipeline { stages: stages, } } pub fn run(&self, initial_data: T) -> Result { let mut skip_stages: u8 = 0; let mut current_stage: u8 = 0; let mut current_data: T = initial_data; for stage in &self.stages { if skip_stages > 0 { skip_stages = skip_stages - 1; current_stage = current_stage + 1; continue; } let stage_result: StageActions = stage.run(current_data.clone())?; match stage_result { StageActions::Abort => { return Err(io::Error::new(io::ErrorKind::Other, "Stage aborted.")); }, StageActions::Skip => { }, StageActions::Next(stage_result_data) => { current_data = stage_result_data; }, StageActions::Jump(stage_count, stage_result_data) => { skip_stages = stage_count; current_data = stage_result_data; }, StageActions::Finish(stage_result_data) => { return Ok(stage_result_data); }, } current_stage = current_stage + 1; } Ok(current_data) } } transformation-pipeline-0.1.0/src/result.rs01006440001747000000000000001006132170661040017243 0ustar0000000000000000use std::io; pub enum StageActions { // Abort the entire pipeline (returning an error). Abort, // Stop the current stage, and call the next stage with the input data. Skip, // Continue to the next pipeline stage, passing the given data. Next(T), // Jump (x) number of pipeline stages in the future, passing the given data. Jump(u8, T), // Finish the entire pipeline early with the given data. Finish(T), } pub type StageResult = Result, io::Error>; transformation-pipeline-0.1.0/src/stage.rs01006440001747000000000000000163132170667270017046 0ustar0000000000000000use result::StageResult; pub trait TransformationStage { fn run(&self, previous: T) -> StageResult; } transformation-pipeline-0.1.0/tests/basic.rs01006440001747000000000000002660132170714170017373 0ustar0000000000000000extern crate transformation_pipeline; use std::clone::Clone; use transformation_pipeline::{TransformationStage, StageActions, StageResult, TransformationPipeline}; struct User { name: String, } impl Clone for User { fn clone(&self) -> Self { User { name: self.name.clone(), } } } struct CapitializeName {} impl TransformationStage for CapitializeName { fn run(&self, previous: User) -> StageResult { let mut name: Vec = previous.name.chars().collect(); name[0] = name[0].to_uppercase().nth(0).unwrap(); Ok(StageActions::Next(User { name: name.into_iter().collect(), })) } } /* I normally don't assume that all individuals are male. This is just a test. */ struct ImposeMr {} impl TransformationStage for ImposeMr { fn run(&self, previous: User) -> StageResult { let mut name: String = "Mr. ".to_owned(); name.push_str(&previous.name); Ok(StageActions::Next(User { name: name, })) } } #[test] fn run_pipeline() { let pipeline: TransformationPipeline = TransformationPipeline::new(vec![ Box::new(CapitializeName {}), Box::new(ImposeMr {}), ]); assert_eq!(pipeline.run(User { name: "john".to_owned() }).unwrap().name, "Mr. John"); assert_eq!(pipeline.run(User { name: "John Doe".to_owned() }).unwrap().name, "Mr. John Doe"); } transformation-pipeline-0.1.0/tests/skip.rs01006440001747000000000000002145132170723760017263 0ustar0000000000000000extern crate transformation_pipeline; use transformation_pipeline::{TransformationStage, StageActions, StageResult, TransformationPipeline}; struct FirstTransformation {} impl TransformationStage for FirstTransformation { fn run(&self, _previous: String) -> StageResult { Ok(StageActions::Next("a".to_owned())) } } struct SecondTransformation {} impl TransformationStage for SecondTransformation { fn run(&self, _previous: String) -> StageResult { Ok(StageActions::Skip) } } struct ThirdTransformation {} impl TransformationStage for ThirdTransformation { fn run(&self, previous: String) -> StageResult { assert_eq!(previous, "a".to_owned()); Ok(StageActions::Next("b".to_owned())) } } #[test] fn run_pipeline() { let pipeline: TransformationPipeline = TransformationPipeline::new(vec![ Box::new(FirstTransformation {}), Box::new(SecondTransformation {}), Box::new(ThirdTransformation {}), ]); assert_eq!(pipeline.run("z".to_owned()).unwrap(), "b".to_owned()); }