Skip to content

Commit

Permalink
v0.7.0
Browse files Browse the repository at this point in the history
  • Loading branch information
nvoynov committed Mar 4, 2024
1 parent c06c5f5 commit e836751
Show file tree
Hide file tree
Showing 76 changed files with 1,301 additions and 1,313 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ title: Punch Changelog

## [Unreleased]

## [0.7.0] - 2024-03-04

A few weeks ago I finished the first commercial backend for a small domain of one actor, a dozen services, four entities, and three plugins. The domain was "punched" using Punch::DSL; bumped in a few inconveniences that were fixed by this release.

- designed new model based on Data class
- designed new DSL for Data class
- designed new decorators - no more paths calculating stuff
- designed new [entity sample](lib/punch/assets/samples/entity.rb.erb) based on Ruby 3.2 Data class
- desgined new PunchSentries service
- desgined new PunchModel service (paths calculating stuff moved here) with PunchEntity, PunchService, PunchPlugin descendants
- designed new Config based on Data
- decision to use positional or keyword arguments moved into samples/\*.rb.erb - one could replace `@model.keyword_params` for `@model.regular_params` (see [model decorator](lib/punch/decors/model.rb) for other methods); can't see the reson to use different approach in one project
- removed the ability of using nested folders for generated concepts; invoke `$ punch new service user/crate-order` will generate `lib/domain/services/user_create_order.rb`
- removed extra `lib/punch/basic/entity.rb`
- simplified plugin and test_plugin samples

## [0.6.4] - 2023-12-12

- moved to Ruby 3.2.2 (Psych and Tests)
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ gemspec

gem "rake", "~> 13.0"

gem "minitest", "~> 5.0"
gem "minitest", "~> 5.17"
5 changes: 3 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
punch (0.6.4)
punch (0.7.0)

GEM
remote: https://rubygems.org/
Expand All @@ -10,12 +10,13 @@ GEM
rake (13.0.6)

PLATFORMS
arm64-darwin-23
x64-mingw-ucrt
x64-mingw32
x86_64-linux

DEPENDENCIES
minitest (~> 5.0)
minitest (~> 5.17)
punch!
rake (~> 13.0)

Expand Down
109 changes: 49 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
---
title: Punch Readme
keywords:
- ruby
- source-code-generator
- interactor
- service
- entity
- plugin
- business-logic-layer
- the-clean-architecture
- domain-driven-design
- ruby
- source-code-generator
- interactor
- service
- entity
- plugin
- business-logic-layer
- the-clean-architecture
- domain-driven-design
...


The Punch's basic idea is to provide a clean robust frame for domain business logic and bring efficiency to the design process.

Playing last year with [The Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) I found it just a really amazing tool, but it also was a bit tiresome because of the necessity to create and require entities and services sources separately. That's why I designed Punch, which provides:
Playing last year with [The Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) I found it just a really amazing tool, but it also was a bit tiresome because of the necessity to create and require entities and services sources separately. That's why I designed Punch, which provides:

- three basic blocks - entity, service, and plugin
- source code templates for those blocks
Expand All @@ -27,11 +26,13 @@ You can find an example of a "punched" domain in [punch_users](https://github.co
- 85% of source files were "punched" and 15% were created manually;
- 50% of Ruby LOC were "punched" and the other 50% were created manually.

Location Total "Punched" SLOC Blank Comments Net Ruby LOC
---------- ------- --------- ---------- -------- --------- ------------
lib 23 (17) 13 (13) 657 (329) 102 (53) 175 (93) 380 (183)
test 17 (17) 15 (16) 363 (335) 46 (38) 45 (150) 272 (147)
lib + test 40 (34) 28 (29) 1020 (664) 148 (91) 220 (243) 652 (330)
Location Total "Punched" SLOC Blank Comments Net Ruby LOC

---

lib 23 (17) 13 (13) 657 (329) 102 (53) 175 (93) 380 (183)
test 17 (17) 15 (16) 363 (335) 46 (38) 45 (150) 272 (147)
lib + test 40 (34) 28 (29) 1020 (664) 148 (91) 220 (243) 652 (330)

## User Stories

Expand Down Expand Up @@ -75,16 +76,9 @@ When you change `domain` settings as `domain: domain`, code will be placed to `l

- `lib/domain/services/sing_in.rb`
- `lib/domain/services.rb~` (require "services/sign_in")
- `test/domain/services/test_sing_in.rb`
- `test/domain/services/test_sing_in.rb`
- `Domain::Services` will be the service namespace

You can go even further and run `$ punch service user/sign_in email password` and it this case if will affect output as:

- `lib/domain/services/user/sing_in.rb`
- `lib/domain/services/user.rb~` (require "user/sign_in")
- `test/domain/services/user/test_sing_in.rb`
- `Domain::Services::User` will be the service namespace

### Logging

Punch logs all commands and errors in `punch.log` file.
Expand All @@ -107,7 +101,7 @@ module Services
class SignUp < Service
def initialize(login, password)
def initialize(login:, password:)
@login = login
@password = password
end
Expand All @@ -118,58 +112,58 @@ module Services
end
```

The `login` and `password` parameters there could be passed in a few ways and the chosen way will affect for code generation output.

1. Finishing parameters with `:` will tell the Punch to generate keyword arguments, so `sign_up login: password:` will generate constructor declaration as `def initialize(login:, password:)`
2. Providing default values for parameters like `sign_up "login \"user\"" "password \"$ecret\""` will generate `def initialize(login = "user", password = "$ecret")`
3. In the same manner you can provided default values for keyword arguments - `sign_up "login: \"user\"" "password: \"$ecret\""` will generate `def initialize(login: "user", password: "$ecret")`
4. Positional and keyword arguments can be mixed together. Punch is advanced enough to place it into right order - positional first, positional with default values next, and keywords at the end.
By default templates all parameters will be generated as keyword arguments. You can change the behaviour by customizing templates.

And finally we can meet the [Sentry](#punchsentry). For keyword arguments you can point a sentry, and being pointed for an argument, constructor will validate the argument with the provided sentry.

Passing parameters as `sign_up login:login password:password` tell Punch to generate `login` and `password` sentries first, and then validate `login` and `secret` parameters inside constructor.
The `login` and `password` parameters there could be passed in a few ways and the chosen way will affect for code generation output.

In `sentries.rb` will be placed two new sentries unless they do not exist.
[punch service sing-up "login \"user\"" secret]{.underline}

```ruby
MustbeLogin = Sentry.new(:login, ...
MustbePassword = Sentry.new(:secret, ...
class SignUp < Service
def initialize(login: 'user', password:)
```

The constructor became
[punch service sing-up user_id:UUID "secret \"$ecret\""]{.underline}

```ruby
def initialize(login:, password:)
@login = MustbeLogin.(login)
@secret = MustbePassword.(password)
end
class SignUp < Service
def initialize(user_id:, secret: '$ecret')
@user_id = MustbeUUID.(user_id, :user_id)
```

Your new service being just "punched" is not ready to run because you need basic Service first (`class SignUp < Service`). The ["Punch Basics"](#punch-basics) section will explain the situation with basic concepts.
Look into the [Sentry](#punchsentry) section for details.

**NOTE** Your "punched" services will not work because of lacking basic Service class. The ["Punch Basics"](#punch-basics) section will explain the situation with basic concepts.

### "punch entity"

The `$punch entity NAME [PARAM] [PARAM]..` command follows the same principles, but its default template also generates `attr_reader` for parameters. An example follows
The `$punch entity NAME [PARAM] [PARAM]..` command follows the same principles but uses anoher template based on Ruby 3.2. Data class. Basically it will look like Data.define

[$ punch entity user login secret]{.underline}

```ruby
module Entities
class User < Data.define(:login, :secret)
def initialize(login:, secret:)
super
end
end
```

class User < Entity
attr_reader :login
attr_reader :secret
[$ punch entity user login:email "secret:string \"$secret\""]{.underline}

def initialize(id:, login:, password:)
super(id)
@login = MustbeLogin.(login)
@secret = MustbePassword.(password)
end
```ruby
class User < Data.define(:login, :secret)
def initialize(login:, secret: '$ecret')
MustbeEmail.(login, :login)
MustbeString.(secret, :secret)
super
end
end
```

### "punch plugin"

The `punch plugin NAME [PARAM] [PARAM]..` command will "punch" Plugin, the command behavior is similar to `punch service/entity`. In addition it will create `config.rb` file with PluginHolder placed there.
The `punch plugin NAME` command will "punch" Plugin, but it will be easier just to preview its result running `$ punch preview plugin store`

### "punch preview"

Expand All @@ -189,7 +183,6 @@ Punch can provide you with its own basic concept it itself designed upon. The `$

```
lib/punch/basics/sentry.rb
lib/punch/basics/entity.rb
lib/punch/basics/service.rb
lib/punch/basics/plugin.rb
lib/punch/basics.rb
Expand Down Expand Up @@ -236,10 +229,6 @@ class SingIn < Service
end
```

#### Punch::Entity

[Entity](https://github.com/nvoynov/punch/blob/master/lib/punch/basics/entity.rb) is just minimal entity class with `id` attribute that passed nil becomes `SecureRandom.uuid`

#### Punch::Plugin

[Plugin](https://github.com/nvoynov/punch/blob/master/lib/punch/basics/plugin.rb) is a simple mixin for your plugin interfaces - logic external to the domain. The mixin provides `plugin` method that creates a new module that serves for plugin holder
Expand Down Expand Up @@ -298,7 +287,7 @@ README.md

Looking through [sample.rb](https://github.com/nvoynov/punch/blob/master/lib/assets/domain/sample.rb), you can express your domain and then generate it with `dogen.rb` script.

__Besides the domain code generation__, you can use domain metadata for "punching" whatever you want to generate from entities, services, plugins, and actors. For example, I going to generate my next SRS skeleton for [Marko](https://github.com/nvoynov/marko) with actors, use cases, and entities.
**Besides the domain code generation**, you can use domain metadata for "punching" whatever you want to generate from entities, services, plugins, and actors. For example, I going to generate my next SRS skeleton for [Marko](https://github.com/nvoynov/marko) with actors, use cases, and entities.

### App design

Expand Down
2 changes: 1 addition & 1 deletion exe/punch
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require "punch/cli"
require "punch/plugins"
include Punch

LoggerPlug.object = Logger.new('punch.log',
LoggerHolder.object = Logger.new('punch.log',
datetime_format: '%Y-%m-%d %H:%M:%S',
formatter: proc{|severity, datetime, progname, msg|
"[#{datetime}] #{severity.ljust(5)}: #{msg}\n"
Expand Down
2 changes: 1 addition & 1 deletion lib/assets/domain/sample.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
include Punch

def build_sample_domain
DSL::Builder.build do
DSL::Domain.() do
sentry :email, 'must be valid email address'
sentry :password, 'at least 8 symbols with digits'

Expand Down
40 changes: 25 additions & 15 deletions lib/assets/samples/entity.rb.erb
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
<% @spacer = ' ' * (@model.namespace.split(/::/).size - 1) -%>
# frozen_string_literal: true
require_relative "entity"

<%= @model.open_namespace %>
<%= @spacer %> # <%= @model.desc %>
<%= @spacer %> class <%= @model.const %> < Entity
<%= @model.properties.lines.map{"#{@spacer} #{_1}"}.join %>
<%= @spacer %> def initialize(id: nil, <%= @model.parameters %>)
<%= @spacer %> super(id)
<%= @model.assignment.lines.map{"#{@spacer} #{_1}"}.join %>
<%= @spacer %> end
<%= @spacer %> end

<%= @model.close_namespace %>
# frozen_string_literal: true
require 'securerandom'

<%= @model.open_namespace %>

%
% indent = @model.indentation
% membrs = @model.params.map(&:name)
% .unshift('id').map{|e| ?: + e}.join(', ')
%
% params = 'id: SecureRanodm.uuid, ' + @model.keyword_params
%
<%= indent %># <%= @model.const %> <%= @model.desc %>
<%= indent %># @!parse class <%= @model.const %> < Data
<%= indent %>class <%= @model.const %> < Data.define(<%= membrs %>)
<%= indent %> <%= @model.params_yarpro.lines.join(' ' + indent) %>
<%= indent %>
<%= indent %> <%= @model.params_yardoc.lines.join(' ' + indent) %>
<%= indent %> def initialize(<%= params %>)
<%= indent %> <%= @model.params_guard.lines.join(' ' + indent) %>
<%= indent %> super
<%= indent %> end
<%= indent %>end

<%= @model.close_namespace %>
28 changes: 12 additions & 16 deletions lib/assets/samples/plugin.rb.erb
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
<% @spacer = ' ' * (@model.namespace.split(/::/).size - 1) -%>
# frozen_string_literal: true
require_relative "../basics"

<%= @model.open_namespace %>
<%= @spacer %> # <%= @model.desc %>
<%= @spacer %> class <%= @model.const %>
<%= @spacer %> extend Plugin

<%= @spacer %> Failure = Class.new(StandardError)

<%= @spacer %> def self.inherited(klass)
<%= @spacer %> klass.const_set(:Failure, Class.new(klass::Failure))
<%= @spacer %> super
<%= @spacer %> end

<%= @spacer %> def initialize(<%= @model.parameters %>)
<%= @model.assignment.lines.map{"#{@spacer} #{_1}"}.join %>
<%= @spacer %> end
<%= @spacer %> end
% indentation = @model.indentation
<%= indentation %># <%= @model.const %> <%= @model.desc %>
<%= indentation %>class <%= @model.const %>
<%= indentation %> extend Plugin
<%= indentation %>
<%= indentation %> Failure = Class.new(StandardError)
<%= indentation %>
<%= indentation %> def self.inherited(klass)
<%= indentation %> klass.const_set(:Failure, Class.new(klass::Failure))
<%= indentation %> super
<%= indentation %> end
<%= indentation %>end

<%= @model.close_namespace %>
4 changes: 2 additions & 2 deletions lib/assets/samples/sentry.rb.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# <%= @model.desc %>
<%= @model.const %> = Sentry.new(:key, "<%= @model.desc %>"
) {|v| <%= @model.block %>}
<%= @model.const %> = Sentry.new(:key, "must bee <%= @model.name %>"
) {|v| <%= @model.proc %> }
31 changes: 17 additions & 14 deletions lib/assets/samples/service.rb.erb
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
<% @spacer = ' ' * (@model.namespace.split(/::/).size - 1) -%>
# frozen_string_literal: true
require_relative "service"
require_relative 'service'

<%= @model.open_namespace %>
<%= @spacer %> # <%= @model.desc %>
<%= @spacer %> class <%= @model.const %> < Service

<%= @model.yardoc.lines.map{"#{@spacer} #{_1}"}.join %>
<%= @spacer %> def initialize(<%= @model.parameters %>)
<%= @model.assignment.lines.map{"#{@spacer} #{_1}"}.join %>
<%= @spacer %> end

<%= @spacer %> def call
<%= @spacer %> # user = storage.find(User, email: @email)
<%= @spacer %> fail "#{self.class}#call UNDER CONSTRUCTION"
<%= @spacer %> end
<%= @spacer %> end
% indent = @model.indentation
<%= indent %># <%= @model.const %> <%= @model.desc %>
<%= indent %>class <%= @model.const %> < Service
<%= indent %> <%= @model.params_yardoc.lines.join(' ' + indent) %>
<%= indent %> def initialize(<%= @model.keyword_params %>)
<%= indent %> <%= @model.params_guard.lines.join(' ' + indent) %>
<%= indent %> super
<%= indent %> end
<%= indent %>
<%= indent %> # @return [] what?
<%= indent %> def call
<%= indent %> # user = store.get(User, id: @user_id)
<%= indent %> # fail "unkown user #{@user_id}" unless user
<%= indent %> fail "#{self.class}#call must be overridden"
<%= indent %> end
<%= indent %>end

<%= @model.close_namespace %>
Loading

0 comments on commit e836751

Please sign in to comment.