Baike.dev
All toolsOnline toolsAI codingTrendingOpen sourceNewsSubmit
Log in
< Back to tools
L

light-service

> 编程语言
Open source

Series of Actions with an emphasis on simplicity.

882 stars0 likes1 views
WebsiteGitHub

About

Series of Actions with an emphasis on simplicity.

LightService is a powerful and flexible service skeleton framework with an emphasis on simplicity

Table of Contents

  • Table of Contents
  • Why LightService?
  • Getting started
    • Requirements
    • Installation
    • Your first action
    • Your first organizer
  • Stopping the Series of Actions
    • Failing the Context
    • Skipping the rest of the actions
    • Skipping ALL the rest of the actions
  • Benchmarking Actions with Around Advice
  • Before and After Action Hooks
  • Expects and Promises
    • Default values for optional Expected keys
  • Key Aliases
  • Logging
  • Error Codes
  • Action Rollback
  • Localizing Messages
    • Built-in localization adapter
    • I18n localization adapter
    • Custom localization adapter
  • Orchestrating Logic in Organizers
  • ContextFactory for Faster Action Testing
  • Rails support
    • Organizer generation
    • Action generation
    • Advanced action generation
  • Other implementations
  • Contributing
  • Release Notes
  • License

Why LightService?

What do you think of this code?

…

This controller violates SRP all over. Also, imagine what it would take to test this beast. You could move the tax_percentage finders and calculations into the tax model, but then you'll make your model logic heavy.

This controller does 3 things in order:

  • Looks up the tax percentage based on order total
  • Calculates the order tax
  • Provides free shipping if the total with tax is greater than $200

The order of these tasks matters: you can't calculate the order tax without the percentage. Wouldn't it be nice to see this instead?

ruby
(
  LooksUpTaxPercentage,
  CalculatesOrderTax,
  ProvidesFreeShipping
)

This block of code should tell you the "story" of what's going on in this workflow. With the help of LightService you can write code this way. First you need an organizer object that sets up the actions in order and executes them one-by-one. Then you need to create the actions with one method (that will do only one thing).

This is how the organizer and actions interact with each other:

…

And with all that, your controller should be super simple:

ruby
class TaxController  :edit, :error => service_result.message
    else
      redirect_to checkout_shipping_path(@order), :notice => "Tax was calculated successfully"
    end

  end
end

I gave a talk at RailsConf 2013 on simple and elegant Rails code where I told the story of how LightService was extracted from the projects I had worked on.

Getting started

Requirements

This gem requires ruby 2.x. Use of generators requires Rails 5+ (tested on Rails 5.x & 6.x only. Will probably work on Rails versions as old as 3.2)

Installation

In your Gemfile:

ruby
gem 'light-service'

And then

bash
bundle install

Or install it yourself as:

bash
gem install light-service

Your first action

LightService's building blocks are actions that are normally composed within an organizer, but can be run independently. Let's make a simple greeter action. Each action can take an optional list of expected inputs and promised outputs. If these are specified and missing at action start and stop respectively, an exception will be thrown.

ruby
class GreetsPerson
  extend ::LightService::Action

  expects :name
  promises :greeting

  executed do |context|
    context.greeting = "Hey there, #{name}. You enjoying LightService so far?"
  end
end

When an action is run, you have access to its returned context, and the status of the action. You can invoke an action by calling .execute on its class with key: value arguments, and inspect its status and context like so:

ruby
outcome = GreetsPerson.execute(name: "Han")

if outcome.success?
  puts outcome.greeting # which was a promised context value
elsif outcome.failure?
  puts "Rats... I can't say hello to you"
end

You will notice that actions are set up to promote simplicity, i.e. they either succeed or fail, and they have very clear inputs and outputs. Ideally, they should do exactly one thing. This makes them as easy to test as unit tests.

Your first organizer

LightService provides a facility to compose actions using organizers. This is great when you have a business process to execute that has multiple steps. By composing actions that do exactly one thing, you can sequence simple actions together to perform complex multi-step business processes in a clear manner that is very easy to reason about.

There are advanced ways to sequence actions that can be found later in the README, but we'll keep this simple for now. First, let's add a second action that we can sequence to run after the GreetsPerson action from above:

…

And here's the organizer that ties the two together. You implement a call class method that takes some arguments and from there sends them to with in key: value format which forms the initial state of the context. From there, chain reduce to with and send it a list of action class names in sequence. The organizer will call each action, one after the other, and build up the context as it goes along.

ruby
class WelcomeAPotentiallyLuckyPerson
  extend LightService::Organizer

  def self.call(name)
    with(:name => name).reduce(GreetsPerson, RandomlyAwardsPrize)
  end
end

When an organizer is run, you have access to the context as it passed through all actions, and the overall status of the organized execution. You can invoke an organizer by calling .call on the class with the expected arguments, and inspect its status and context just like you would an action:

ruby
outcome = WelcomeAPotentiallyLuckyPerson.call("Han")

if outcome.success?
  puts outcome.greeting # which was a promised context value

  if outcome.did_i_win
    puts "And you've won a prize! Lucky you. Please see the front desk for your #{outcome.prize}."
  end
else # outcome.failure? is true, and we can pull the failure message out of the context for feedback to the user.
  puts "Rats... I can't say hello to you, because #{outcome.message}."
end

Because organizers generally run through complex business logic, and every action has the potential to cause a failure, testing an organizer is functionally equivalent to an integration test.

For further examples, please visit the project's Wiki and review the "Why LightService" section above.

Stopping the Series of Actions

When nothing unexpected happens during the organizer's call, the returned context will be successful. Here is how you can check for this:

ruby
class SomeController  "Everything went OK! Thanks!"
    else
      flash[:error] = result_context.message
      render :action => "new"
    end
  end
end

However, sometimes not everything will play out as you expect it. An external API call might not be available or some complex business logic will need to stop the processing of the Series of Actions. You have two options to stop the call chain:

  1. Failing the context
  2. Skipping the rest of the actions

Failing the Context

When something goes wrong in an action and you want to halt the chain, you need to call fail! on the context object. This will push the context in a failure state (context.failure? # will evalute to true). The context's fail! method can take an optional message argument, this message might help describing what went wrong. In case you need to return immediately from the point of failure, you have to do that by calling next context.

In case you want to fail the context and stop the execution of the executed block, use the fail_and_return!('something went wrong') method. This will immediately leave the block, you don't need to call next context to return from the block.

Here is an example:

ruby
class SubmitsOrderAction
  extend LightService::Action
  expects :order, :mailer

  executed do |context|
    unless context.order.submit_order_successful?
      context.fail_and_return!("Failed to submit the order")
    end

    # This won't be executed
    context.mailer.send_order_notification!
  end
end

In the example above the organizer called 4 actions. The first 2 actions got executed successfully. The 3rd had a failure, that pushed the context into a failure state and the 4th action was skipped.

Skipping the rest of the actions

You can skip the rest of the actions by calling context.skip_remaining!. This behaves very similarly to the above-mentioned fail! mechanism, except this will not push the context into a failure state. A good use case for this is executing the first couple of action and based on a check you might not need to execute the rest. Here is an example of how you do it:

ruby
class ChecksOrderStatusAction
  extend LightService::Action
  expects :order

  executed do |context|
    if context.order.send_notification?
      context.skip_remaining!("Everything is good, no need to execute the rest of the actions")
    end
  end
end

In the example above the organizer called 4 actions. The first 2 actions got executed successfully. The 3rd decided to skip the rest, the 4th action was not invoked. The context was successful.

Skipping ALL the rest of the actions

While context.skip_remaining! skips actions in the current scope (e.g. inside a reduce_if or iterate block), sometimes you want to halt the entire execution of the organizer and any parent organizers. You can do this by calling context.skip_all_remaining!.

Consider this example:

ruby
def self.actions
  [
    reduce_if(->(ctx) { ctx.should_execute_conditional_actions }, [
      ConditionallyRunAction1, # calls ctx.skip_all_remaining!
      ConditionallyRunAction2, # skipped
    ]),
    AlwaysRunAction, # also skipped with skip_all_remaining!
  ]
end

In this case, AlwaysRunAction will be skipped if skip_all_remaining! was called inside the reduce_if block. If skip_remaining! had been used instead, AlwaysRunAction would still execute because the "skip" state is normally reset when exiting a scoped block.

Benchmarking Actions with Around Advice

Benchmarking your action is needed when you profile the series of actions. You could add benchmarking logic to each and every action, however, that would blur the business logic you have in your actions.

Take advantage of the organizer's around_each method, which wraps the action calls as its reducing them in order.

Check out this example:

ruby
class LogDuration
  def self.call(context)
    start_time = Time.now
    result = yield
    duration = Time.now - start_time
    LightService::Configuration.logger.info(
      :action   => context.current_action,
      :duration => duration
    )

    result
  end
end

class CalculatesTax
  extend LightService::Organizer

  def self.call(order)
    with(:order => order).around_each(LogDuration).reduce(
        LooksUpTaxPercentageAction,
        Calcula

Issues· 0 open

View all issuesOpen on GitHub

No open issues yet, or sync has not completed.

> Tags

Rubyaoprubyworkflow

No comments yet. Be the first to share.

> Details

PublishedAug 1, 2026
UpdatedSep 17, 2026
Category编程语言
PricingOpen source

> Related tools

T
TypeScript
JavaScript 的超集,为前端与全栈提供静态类型
P
Python
通用编程语言,广泛用于 Web、数据与 AI
G
Go
Google 推出的简洁高效系统语言