Gem Version .github/workflows/main.yml Maintainability

Cleon

-= Cleon v0.5.0 =- Clean Code Skeleton
home: https://github.com/nvoynov/cleon

Quickstart:
  1. gem "cleon" when your work with Bundler
  2. $ cleon

Commands:
  $ cleon CLONE
  $ cleon arguard NAME
  $ cleon service NAME [PARA1 PARA2]
  $ cleon entity NAME [PARA1 PARA2]
  $ cleon port CLEON PORT_TO

Cleon a set of basic PORO abstractions for building clean systems. If you are familiar with The Clean Architecture, you'll catch everything at a glance. If not I doubted it serves to you. You may also check User Stories.

You can check using those concepts in the Users demo gem that represents a simple but ubiquitous user management domain.

Installation

Add this line to your application's Gemfile:

gem 'cleon'

And then execute:

$ bundle install

Usage

Create the skeleton

Once you have decided to start a new ruby project for problem domain, you can start with creating a new gem and cloning Cleon skeleton to the gem. The following command exec bundle gem CLONE and then clone Cleon's code to the new generated gem; the CLONE parameter stands for the name of your new gem.

$ cleon CLONE

Hope, the output of the command is clear

$ cleon demo
Clone myself to 'demo'...
Creating gem 'demo'...
# skipped bundler output
  created lib/demo/basics
  created lib/demo/services
  created lib/demo/entities
  created test/demo
  created test/demo/services
  created test/demo/entities
  created lib/demo/arguards.rb
  created lib/demo/basics.rb
  created lib/demo.rb~
  created lib/demo.rb
  created lib/demo/entities.rb
  created lib/demo/gateway.rb
  created lib/demo/services.rb
  created lib/demo/basics/arguard.rb
  created lib/demo/basics/service.rb
  created lib/demo/basics/entity.rb
Cleon was cloned successfully

You need to fix demo.gemspec and then you can generate basic concepts. I know it's boring, but for me placing it to gem is important part.

Creating a new arguard

I was brooding over adding some more advanced type system but finished with just simple as possible argument guard that just check and return value it if meets the guard spec.

The simple example of using guards is

GuardString = ArGuard.new('string', 'must be String',
  Proc.new{|v| v.is_a?(String)})

arg = GuardString.("s")
#  => 's'
arg = GuardString.(1)
# => ArgumentError: :string must be String
arg = GuardString.(1, 'name')
# => ArgumentError: :name must be String
arg = GuardString.(1, 'name', 'should be String')
# => ArgumentError: :name should be String

To create a new guard just run the following command:

$ cleon arguard string

That will create a blank for the guard and its test. The output demonstrates a general behavior of Cleon's generators. It generates source and source_spec, backup previous version of the source when it already exit in sources tree.

$ cleon arguard string
Create arguard 'string'...
  created lib/demo/arguards.rb~
  created lib/demo/arguards.rb
  created test/demo/arguards_spec.rb
  created test/demo/arguards_spec.rb~
  created test/demo/arguards_spec.rb
ArGuard was created successfully

arguards.rb

require_relative 'basics/arguard'

module Demo
  # Place here shared argument guards for the domain
  module ArGuards

    GuardString = Demo::ArGuard.new(
      'string', 'must be String',
      Proc.new {|v|
        raise "provide spec for GuardString file: #{__FILE__} line: #{__LINE__}"})

  end
end

arguards_spec.rb

require_relative '../spec_helper'
include Demo::ArGuards

module SharedGuardSpecs
  extend Minitest::Spec::DSL

  # spec must provided the following variables:
  #   let(:guard) { GuardName }
  #   let(:valid) { ["name", :name] }
  #   let(:wrong) { [nil, 1, Object.new]}

  it 'must return value' do
    valid.each{|v| assert_equal v, guard.(v)}
  end

  it 'must raise ArgumentError' do
    wrong.each{|w| assert_raises(ArgumentError) { guard.(w) }}
  end
end

describe GuardString do
  include SharedGuardSpecs

  let(:guard) { GuardString }
  let(:valid) { [nil, -1, 0, 1, "", "str", Object.new] }
  let(:wrong) { [nil, -1, 0, 1, "", "str", Object.new] }
end

Creating a new service

Once you need a new service you can run the following generator:

$ cleon service SERVICE [PARAM1] [PRAM2]

The output will be

cleon service authenticate email secret
Create service...
  created lib/demo/services/authenticate.rb
  created lib/demo/services.rb~
  created lib/demo/services.rb
  created test/demo/services/authenticate_spec.rb

the "authenticate.rb" content

require_relative '../basics/service'

module Demo
  module Services
    class Authenticate < Service
      def initialize(email:, secret:)
        @email = email
        @secret = secret
      end

      def call
        # gateway.authenticate(@email, @secret)
      end
    end
  end
end

the "authenticate_spec.rb" source

require_relative '../../spec_helper'
include Demo::Services

describe Authenticate do

  let(:params) { {email:, secret:} }
  let(:service) { Authenticate }

  describe '#call' do
    it 'must return guarded result' do
      # @gateway = Minitest::Mock.new
      # @gateway.expect(:authenticate, nil, [String])
      #
      # Demo.stub :gateway, @gateway do
      #   result = service.(**params)
      #   assert GuardResult.(result), result
      # end
    end
  end
end

You can also enforce your service with argument guards.

$ cleon service authenticate email:email secret:secret

Then generated service source will guard the service arguments:

class Authenticate < Service
  def initialize(email:, secret:)
    @email = GuardEmail.(email)
    @secret = GuardSecret.(secret)
  end
  # skipped ..
end

Creating a new entity

Once you need a new entity you can run the following:

$ cleon entity ENTITY [PARA1] [PARA2]

The behavior is the same as for service generator

Generating whole domains

If you like everything above, maybe Dogen could be your next step. Dogen provides a model for describing domains, a DSL for creating such models, and the domain generator that creates whole domain skeleton with guars, services and entities in the Cleon's way.

Story

Once I caught myself copying and polishing basic classes from one project to another and then following DRY principle I just created the gem for this purpose. Somewhere in those time I had seen "The Foundation" series and chose the name after Cleon, because of his boring perpetuated constancy.

The first version actually jsust cloned the cleon's source files directly from this gem to another location, and changing the name of the main gem module after. But then I created Dogen and migrated here generoators.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test 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 the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/cleon.