MiniCamel

MiniCamel allows you to perform simple data transformations and combine these transformations to reusable business processes. It is inspired by his big brother Apache Camel.

Basic Usage

The idea behind MiniCamel is to define a route (or multiple) to transform an input presentation to an output presentation.

# Checkout the 'examples' folder for the full example
class HelloWorldRoutes < MiniCamel::RouteBuilder
  def configure
    from(:hello_world).
      desc("Transforms 'Hello' to 'Hello World'").
      to(:write_text).
      transform(:input, to: :output, with_class: TransformHello).
      extract_result(from: :output)

    # reusable route
    from(:write_text).
      desc("Writes some text to stdout").
      process(:input, with_class: WriteInput)
  end
end

In each route you can define custom interactor classes that will transform or process specific parameters.

# transform(:input, to: :output, with_class: TransformHello)

class TransformHello < MiniCamel::Interactor

  attribute :input, String
  validates :input, presence: true

  def run
    {text: "#{input} World"}
  end

end

This interactor will process the input string and transform it to a Hash. The return value of the interactor will be written into output.

After you have defined all your routes you can generate your MiniCamel environment.

environment = MiniCamel::Environment.new
environment.register_route_builder(HelloWorldRoutes.new)
environment.finalize

And dispatch one of the routes.

exchange = environment.dispatch_route(:hello_world, with_data: {input: "Hello"})

The result of the dispatch_route call will be a MiniCamel::Exchange. An exchange is an object that is transfered between each processor of a route. It exposes two callbacks

exchange.on(:success) do |result|
  puts result
end.on(:failure) do |error|
  puts error
end

The on(:success) callback will yield the exchange result if the exchange was processed successfully otherwise it will yield an error in the on(:failure) callback.

The internals

Context

The context is a part of the exchange and holds the data of the exchange.

Processors

Each route can call a number of processors. A processor is a simple logical unit that changes the context or uses its data to trigger specific business processes.

ExposeField

The ExposeField processor exposes a field from an object or a hash to the context so the value can be used by other processors.

# context: {test: {hello: "world"}}
expose_field(:hello, from: :test)
# new context: {test: {hello: "world"}, hello: "world"}

Further more you can change the name of the exposed field.

# context: {test: {hello: "world"}}
expose_field(:hello, from: :test, as: :bye)
# new context: {bye: "world", test: {hello: "world"}}

ExposeFields

The ExposeFields processor works like ExposeField but it allows to expose multiple fields at once. Unlike ExposeField it does not allow to change the name of the exposed fields.

# context: {test: {hello: "world", foo: "bar"}}
expose_fields(:hello, :foo, from: :test)
# new context: {hello: "world", foo: "bar", test: {hello: "world"}}

ExtractResult

The ExtractResult processor extracts an object from the context. This object will be used as the result of the exchange. Please be aware that the result object has to be of the type Hash or MiniCamel::Dto.

from(:extract_result_example).
  desc("Extracts a result from ':test'.").
  extract_result(from: :test)

exchange = env.dispatch_route(:extract_result_example, with_data: {test: {hello: "world"})
exchange.on(:success) do |result|
  puts result # -> {hello: "world"}
end

Mutate

The Mutate processor transforms a value with an interactor and writes the return value back to the original field. If you want to transform multiple values than you should use the TransformProcessor.

# context: {test: {hello: "world"}}
mutate(:test, with_class: MutateTestInteractor)
# new context: {test: {foo: "bar"}} (the MutateTestInteractor class returns {foo: "bar"})

MutateEach

Like the Mutate processor the MutateEach processor transforms a value with an interactor. But instead of a single value it transforms the array under the given field name. You may also provide additional_fields for each step.

# context: {values: [1, 2, 3]}
mutate_each(:value, in: :values, with_class: MultiplyBy2Interactor)
# new context: {values: [2, 4, 6]}

To

To allows you to route the current exchange to a sub route and to reuse predifined processes.

from(:hello_world).
  desc("Prints 'Hello'").
  to(:print_text)

from(:foo_bar).
  desc("Prints 'Foo'").
  to(:print_text)

from(:print_text).
  desc("Prints some text to stdout").
  process(:input, with_class: WriteInput)

env.dispatch_route(:hello_world, with_data: {input: 'Hello'})
# -> "Hello"

env.dispatch_route(:foo_bar, with_data: {input: 'Foo'})
# -> "Foo"

Pipeline

Pipeline allows you to call multiple sub routes in one call.

from(:ask_question).
  desc("Prints some user input").
  pipeline(:ask_for_input, :print_text)

from(:print_text).
  desc("Prints some text to stdout").
  process(:input, with_class: WriteInput)

from(:ask_for_input).
  desc("Asks the user for input").
  produce(:input, with_class: AskUserForInputInteractor)

env.dispatch_route(:ask_question, with_data: {})
# -> depends on user input

Process

Process allows you to execute some business logic. Process does not modify the data you pass into it.

from(:print_text).
  desc("Prints some text to stdout").
  process(:input, with_class: WriteInput)

env.dispatch_route(:print_text, with_data: {input: "foobar"})
# -> "foobar"

ProcessEach

ProcessEach allows you to process each value in an array. Just like Process it does not modify the array values you pass into it. You may also provide additional_fields for each step.

from(:greet).
  desc("Prints each value of an array").
  process(:input, in: :inputs, with_class: WriteInput)

env.dispatch_route(:greet, with_data: {inputs: ["hello", "world"]})
# -> "hello"
# -> "world"

Transform

The Transform processor allows you to transform multiple values into one result value with an interactor.

from(:concat_foobar).
  desc("Concats foo bar to foobar").
  transform(:foo, :bar, to: :foobar, with_class: ConcatFoobarInteractor).
  process(:foobar, with_class: PrintInput)

env.dispatch_route(:concat_foobar, with_data: {foo: "foo", bar: "bar"})
# -> "foobar"

TransformEach

The TransformEach processor allows you to transform array values and put them into a new array with an interactor. You may also provide additional_fields for each step.

from(:multiply_by_two).
  desc("Multiplies each array entry by two and puts it into a new numbers_multiplied_by_two array.").
  transform(:number, in: :numbers, to: :numbers_multiplied_by_two, with_class: MultiplyByTwoInteractor).
  process(:numbers_multiplied_by_two, with_class: PrintInput)

env.dispatch_route(:multiply_by_two, with_data: {numbers: [1,2,3]})
# -> [2,4,6]

Validate

The Validate processor will validate a field you pass into it. The value of the field has to respond to invalid?.

class InvalidValue

  def invalid?
    true
  end

end

class InvalidValueError < StandardError; end

from(:check_value).
  desc("Raises a InvalidValueError").
  validate(:value, raise_error: InvalidValueError)

env.dispatch_route(:check_value, with_data: {value: InvalidValue.new})
# -> raises InvalidValueError

TODO

  • Improve README
  • Check for unknown routes at route finalization
  • Create a plugin system to easily add new processors
  • Improve exception handling

License

Copyright (c) 2016 Spas Poptchev

MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.