Civil

An opinionated Ruby framework for standardizing services, their inputs and their outputs. Build Status

Installation

Add this line to your application's Gemfile:

gem 'civil'

And then execute:

$ bundle

Or install it yourself as:

$ gem install civil

Getting started

Write your service:

class MyService
  include Civil::Service

  # Use attribute readers for input parameters
  attr_reader :foo
  attr_accessor :buffer

  service do
    do_something
    do_something_else
  end

  def do_something
    self.buffer = foo + 3
  end

  def do_something_else
    self.buffer = buffer * 2
  end
end

Invoke your service:

# Pass input parameters in hash
result = MyService.call foo: 10

result.data       # 26; result.data == the last return value from inside the service block
result.conditions # {}; add conditions to indicate non-happy path results (see below)
result.meta       # {}
result.ideal?     # true; because there are no conditions
result.deviant?   # false

Usage

Conditions

Civil distinguishes between "errors" and "conditions":

  • An error results from an unexpected behavior that has nothing to do with your business logic. For example, a user attempting to access an unauthorized resource.
  • A condition results from an expected behavior that deviates from the happy path in your business logic. For example, a user attempting to upload an avatar image that's too big.

Civil currently doesn't handle errors. To add a condition, use #add_condition:

class SaveAvatarImage
  ...

  def check_image_size
    add_condition :image_too_big, "The uploaded image must be < 1 MB in size" if image_size_too_big?
  end
end

Your condition will be available in your result:

result = SaveAvatarImage.call image: image

result.conditions # { image_too_big: [ "The uploaded image must be < 1 MB in size" ] }
result.ideal?     # false
result.deviant?   # true

Results from service calls that have no conditions are said to be "ideal" whereas those with conditions are said to be "deviant." Two helper methods, ideal? and deviant?, provide shortcuts for checking for the presence of conditions. Note that we intentionally avoid the commonly used terms "success" and "failure" as these relate to errors, not conditions.

Metadata

Metadata can be returned with any ideal or deviant result by using add_meta:

class MetadataExample
  ...

  def process_doodad
    add_meta :length, 12.5
  end
end

result = MetadataExample.call doodad: doodad

result.meta # { length: [ 12.5 ] }

Overriding the service block

You can override the steps defined within your service class' service block by passing a block to call:

class MyService
  service do
    step_one
    step_two
    result
  end

  ...
end

# The following will skip step_two
result = MyService.call { step_one; result; }

Filtering

Filter metadata and results by using where:

class BuildCars
  ...

  def 
    if make == 'Wondercar'
      add_condition :cars, { make: 'Wondercar', msg: 'no such make' }
      add_condition :cars, { make: 'Wondercar', msg: 'not a thing' }
      add_meta :cars, { make: 'Wondercar', msg: 'pretty cool name, though' }
    end
  end
end

result = BuildCars.call(makes: ['Wondercar', 'Lamecar', 'Hamilcar'])

result.conditions[:cars].where(make: 'Wondercar') # [ { make: 'Wondercar', msg: 'no such make'}, { make: 'Wondercar', msg: 'not a thing'} ]
result.conditions[:cars].where(make: 'Wondercar', msg: 'not a thing') # [ { make: 'Wondercar', msg: 'not a thing'} ]
result.meta[:cars].where(make: 'Wondercar') # [ { make: 'Wondercar', msg: 'pretty cool name, though' } ]

For filtering to work properly, you must pass a hash or an instance of Civil::Hash to add_condition/add_meta.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/earksiinni/civil. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

The gem is available as open source under the terms of the MIT License.