Coder Social home page Coder Social logo

trailblazer-activity's Introduction

Trailblazer

Battle-tested Ruby framework to help structuring your business logic.

Gem Version

What's Trailblazer?

Trailblazer introduces new abstraction layers into Ruby applications to help you structure your business logic.

It ships with our canonical "service object" implementation called operation, many conventions, gems for testing, Rails support, optional form objects and much more.

Should I use Trailblazer?

Give us a chance if you say "yes" to this!

  • You hate messy controller code but don't know where to put it?
  • Moving business code into the "fat model" gives you nightmares?
  • "Service objects" are great?
  • Anyhow, you're tired of 12 different "service object" implementations throughout your app?
  • You keep asking for additional layers such as forms, policies, decorators?

Yes? Then we got a well-seasoned framework for you: Trailblazer.

Here are the main concepts.

Operation

The operation encapsulates business logic and is the heart of the Trailblazer architecture.

An operation is not just a monolithic replacement for your business code. It's a simple orchestrator between the form objects, models, your business code and all other layers needed to get the job done.

# app/concepts/song/operation/create.rb
module Song::Operation
  class Create < Trailblazer::Operation
    step :create_model
    step :validate
    left :handle_errors
    step :notify

    def create_model(ctx, **)
      # do whatever you feel like.
      ctx[:model] = Song.new
    end

    def validate(ctx, params:, **)
      # ..
    end
    # ...
  end
end

The step DSL takes away the pain of flow control and error handling. You focus on what happens: creating models, validating data, sending out notifications.

Control flow

The operation takes care when things happen: the flow control. Internally, this works as depicted in this beautiful diagram.

Flow diagram of a typical operation.

The best part: the only way to invoke this operation is Operation.call. The single entry-point saves programmers from shenanigans with instances and internal state - it's proven to be an almost bullet-proof concept in the past 10 years.

result = Song::Operation::Create.(params: {title: "Hear Us Out", band: "Rancid"})

result.success? #=> true
result[:model]  #=> #<Song title="Hear Us Out" ...>

Data, computed values, statuses or models from within the operation run are exposed through the result object.

Operations can be nested, use composition and inheritance patterns, provide variable mapping around each step, support dependency injection, and save you from reinventing the wheel - over and over, again.

Leveraging those functional mechanics, operations encourage a high degree of encapsulation while giving you all the conventions and tools for free (except for a bit of a learning curve).

Tracing

In the past years, we learnt from some old mistakes and improved developer experience. As a starter, check out our built-in tracing!

result = Song::Operation::Create.wtf?(params: {title: "", band: "Rancid"})

Tracing the internal flow of an operation.

Within a second you know which step failed - a thing that might seem trivial, but when things grow and a deeply nested step in an iteration fails, you will start loving #wtf?! It has saved us days of debugging.

We even provide a visual debugger to inspect traces on the webs.

There's a lot more

All our abstraction layers such as operations, form objects, view components, test gems and much more are used in hundreds of OSS projects and commercial applications in the Ruby world.

We provide a visual debugger, a BPMN editor for long-running business processes, thorough documentation and a growing list of onboarding videos (TRAILBLAZER TALES).

Trailblazer is both used for refactoring legacy apps (we support Ruby 2.5+) and helps big teams organizing, structuring and debugging modern, growing (Rails) applications.

Documentation

Make sure to check out the new beginner's guide to learning Trailblazer. The new book discusses all aspects in a step-wise approach you need to understand Trailblazer's mechanics and design ideas.

The new begginer's guide.

trailblazer-activity's People

Contributors

apotonick avatar bookwyrm avatar emaglio avatar kamilmilewski avatar petergoldstein avatar scharrels avatar seuros avatar sveredyuk avatar vessi avatar yogeshjain999 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

trailblazer-activity's Issues

Cannot move from fail step to pass step

step :find_by_auth
pass :assign_email, pass_fast: true
fail :find_by_email, Output(:success) => :assign_authentication
pass :assign_authentication, pass_fast: true
fail :create

I use it for user authentication by omniauth. When user isn’t found by authentication then it moves to find_by_email and then I see error message:

Trailblazer::Circuit::IllegalOutputSignalError: [#<Trailblazer::Activity::TaskBuilder::Task user_proc=find_by_email>][ Trailblazer::Activity::Right ]

I have tried replace Output(:sucess) with Output(Trailblazer::Activity::Right, :success) but error still appears.

Subprocess doesn't create unique task

You need to clone constants to make Activity think those are two different tasks.

step Subprocess(Merge::Scalar),
step Subprocess(Merge::Scalar.clone)

Replace Hirb gem

We use the hirb gem for outputting the stack tree.

`-- #<Trailblazer::Activity: {top}>
    |-- Start.default
    |-- #<Trailblazer::Activity: {}>
    |   |-- Start.default
    |   |-- read_a_field
    |   |-- read_b_field_2
    |   |-- overwrite_a_with_b
    |   `-- End.success
    |-- #<Trailblazer::Activity: {}>
    |   |-- Start.default
    |   |-- read_a_field
    |   |-- read_b_field_2
    |   |-- write_a
    |   `-- End.failure
    |-- #<Trailblazer::Activity: {}>
    |   |-- Start.default
    |   |-- #<Method: #<Trailblazer::Activity: {Trailblazer::Disposable::Merge::Property::Nested}>.read_a_field>
    |   |-- read_b_field_2
    |   |-- #<Trailblazer::Activity: {}>
    |   |   |-- Start.default
    |   |   |-- #<Trailblazer::Activity: {c}>
    |   |   |   |-- Start.default

This is very very simple and we probably don't need a gem for that.

Step/Task with same id should raise an error

This:

|-- #<Trailblazer::Activity::Start semantic=:default>
|-- find_model
|-- Wrap/DocsWrapTest::HandleUnsafeProcess
|   |-- #<Trailblazer::Activity::Start semantic=:default>
|   |-- update
|   |-- rehash
|   `-- #<Trailblazer::Operation::Railway::End::Success semantic=:success>
|-- Wrap/DocsWrapTest::HandleUnsafeProcess
|   |-- #<Trailblazer::Activity::Start semantic=:default>
|   |-- update
|   |-- rehash
|   `-- #<Trailblazer::Operation::Railway::End::Success semantic=:success>
|-- notify
`-- #<Trailblazer::Operation::Railway::End::Success semantic=:success>

should raise an error

Graph#outputs

Would it be more consistent to have Graph(MyActivity).outputs instead of MyActivity.to_h[:outputs]?

Simpler DSL for output wiring

module Myactivity
  step a
  step Nested(Tyrant::Magicstep), success => d
  step c
  step Rescue(d), exception => myhandler
  step e

Implement syntax `step :method_name` in Activity

It would be great if we could have:

class MyActivity < Trabilblazer::Activity::Railway
  step :my_method
  
  def my_method(ctx, kinda_loyke_it:,  **)
    ctx[:yeah_nah] = my_helper_method(kinda_loyke_it)
  end

  private

  def my_helper_method(nah)
    { yeah: nah }
  end
end

Current syntax:

class MyActivity < Trabilblazer::Activity::Railway
  def self.my_helper_method(nah)
    { yeah: nah }
  end
  
  def self.my_method(ctx, kinda_loyke_it:,  **)
    ctx[:yeah_nah] = my_helper_method(kinda_loyke_it)
  end

  step method(:my_method)
end

Better exception

I don't know how it could be done, yet, but the wiring error exception with anonymous classes is very hard to debug

Unrecognized Signal `#<Trailblazer::Activity::End semantic=:skip>` returned from #<Class:0x00005559038d3b30>. Registered signals are, 
#<Trailblazer::Activity::End semantic=:failure>
#<Trailblazer::Activity::End semantic=:success>
#<Trailblazer::Activity::End semantic=:key_not_found>

Maybe the activity could maintain a "name hint" and add this to the exception. Then it would be much more obvious this exception is coming from, say "song.deserialize.nested".

Idea: Performance better with while loop

In Circuit#call, a while loop that also doesn't call termini anymore improves from 1.23 to 1.55.

def call(args, start_task: @start_task, runner: Runner, **circuit_options)
        circuit_options = circuit_options.merge(runner: runner) # TODO: set the :runner option via arguments_for_call to save the merge?
        task            = start_task
        last_signal = nil

        while ! @stop_events.include?(task) do
          last_signal, args, _discarded_circuit_options = runner.(
            task,
            args,
            **circuit_options
          )

          # Stop execution of the circuit when we hit a stop event (< End). This could be an task's End or Suspend.
           # if @stop_events.include?(task)

          if (next_task = next_for(task, last_signal))
            task = next_task
          else
            raise IllegalSignalError.new(
              task,
              signal: last_signal,
              outputs: @map[task],
              exec_context: circuit_options[:exec_context], # passed at run-time from DSL
            )
          end
        end

        return [ last_signal, args ]
      end

When using task wrap you have no way to access the ID of the step

When using custom macros that return a Proc/Lambda, you have no access to the id when applying a task wrap. Because of this, the only way you can try and derive a name/id of that step is to inspect the Proc, which is neither convenient nor produces a good name. In contrast, the custom macro would usually know a name to begin with. As such, it would be more convenient if the wrap context also contained the id so that you could use it. I want this feature so that I can have a name to log for each set for instrumentation purposes.

Thank you for your time in reading and considering this.

Specify Output => End wiring in Macro

What we do currently

step :validate, Output(:failure) => End(:invalid_params)
step :find_user, Output(:failure) => End(:not_found)

it works, but is repeated in every CRUD Operation

ideally, I would like to see my custom Macros that do Output => End wiring inside macro
and I can decide how to interpret failed op result based on it semantics (very useful for granular error reporting in CRUD API)

step MyValidateMacro( MyDryValidationSchema ) # if fails op result to have `sematic: :invalid_params`
step MyModelMacro( UserRepository, :find_by_email ) # if fails op result to have `sematic: :not_found`

Not sure it is the right repo for issues, but you asked to add this issue :)
https://gitter.im/trailblazer/chat?at=5ad4d05d270d7d3708d2704c (initial request in Gitter)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.