diff --git a/CHANGELOG.md b/CHANGELOG.md index f765dd7..0ad794d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/Gemfile b/Gemfile index 6b1025a..8d339bb 100644 --- a/Gemfile +++ b/Gemfile @@ -7,4 +7,4 @@ gemspec gem "rake", "~> 13.0" -gem "minitest", "~> 5.0" +gem "minitest", "~> 5.17" diff --git a/Gemfile.lock b/Gemfile.lock index 21fd6ee..99f3ce4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - punch (0.6.4) + punch (0.7.0) GEM remote: https://rubygems.org/ @@ -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) diff --git a/README.md b/README.md index 305253d..0374ace 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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. @@ -107,7 +101,7 @@ module Services class SignUp < Service - def initialize(login, password) + def initialize(login:, password:) @login = login @password = password end @@ -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" @@ -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 @@ -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 @@ -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 diff --git a/exe/punch b/exe/punch old mode 100644 new mode 100755 index 3da4dd8..507c78f --- a/exe/punch +++ b/exe/punch @@ -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" diff --git a/lib/assets/domain/sample.rb b/lib/assets/domain/sample.rb index b1ae6f8..b949d28 100644 --- a/lib/assets/domain/sample.rb +++ b/lib/assets/domain/sample.rb @@ -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' diff --git a/lib/assets/samples/entity.rb.erb b/lib/assets/samples/entity.rb.erb index 18fd9f3..88dbe5a 100644 --- a/lib/assets/samples/entity.rb.erb +++ b/lib/assets/samples/entity.rb.erb @@ -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 %> diff --git a/lib/assets/samples/plugin.rb.erb b/lib/assets/samples/plugin.rb.erb index 2cfa3d7..bf28409 100644 --- a/lib/assets/samples/plugin.rb.erb +++ b/lib/assets/samples/plugin.rb.erb @@ -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 %> diff --git a/lib/assets/samples/sentry.rb.erb b/lib/assets/samples/sentry.rb.erb index a0c7ac9..a6c2e8b 100644 --- a/lib/assets/samples/sentry.rb.erb +++ b/lib/assets/samples/sentry.rb.erb @@ -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 %> } diff --git a/lib/assets/samples/service.rb.erb b/lib/assets/samples/service.rb.erb index dfc7c1e..ae6e287 100644 --- a/lib/assets/samples/service.rb.erb +++ b/lib/assets/samples/service.rb.erb @@ -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 %> diff --git a/lib/assets/samples/test_entity.rb.erb b/lib/assets/samples/test_entity.rb.erb index a7c1c42..f81bbc1 100644 --- a/lib/assets/samples/test_entity.rb.erb +++ b/lib/assets/samples/test_entity.rb.erb @@ -1,11 +1,10 @@ -require_relative "<%= @model.test_helper %>" -include <%= @model.namespace %> - -describe <%= @model.const %> do - # let(:entity) { <%= @model.const %>.new(<%= @model.parameters %>) } - - it { - # assert entity - skip "UNDER CONSTRUCTION" - } -end +require_relative "<%= @model.test_helper %>" +include <%= @model.namespace %> + +describe <%= @model.const %> do + # let(:entity) { <%= @model.const %>.new(<%= @model.keyword_params %>) } + + it { + skip "design test for <%= @model.const %>" + } +end diff --git a/lib/assets/samples/test_plugin.rb.erb b/lib/assets/samples/test_plugin.rb.erb index 4e7c55d..6e5dda5 100644 --- a/lib/assets/samples/test_plugin.rb.erb +++ b/lib/assets/samples/test_plugin.rb.erb @@ -1,13 +1,33 @@ require_relative "<%= @model.test_helper %>" include <%= @model.namespace %> +# Use the following pattern when the <%= @model.const %> is just an interface to be implemented in App +# module <%= @model.const %>Tests +# def plugin +# fail "override it in plugin implementation test" +# end +# +# def test_one +# plugin.one +# end +# +# def test_two +# plugin.two +# end +# end +# +# class Test<%= @model.const %>Implementation < Minitest::Test +# include <%= @model.const %>Tests +# def plugin +# <%= @model.const %>Implementation.new +# end +# end + class Test<%= @model.const %> < Minitest::Test def plugin - <%= @model.const %>.new(<%= @model.arguments %>) + <%= @model.const %>.new end - def test_some_method - skip "UNDER CONSTRUCTION" + def test_one end - end diff --git a/lib/assets/samples/test_sentry.rb.erb b/lib/assets/samples/test_sentry.rb.erb index aae5b99..0cb340e 100644 --- a/lib/assets/samples/test_sentry.rb.erb +++ b/lib/assets/samples/test_sentry.rb.erb @@ -1,13 +1,14 @@ -require_relative "<%= @model.test_helper %>" +require_relative '<%= @model.test_helper %>' describe <%= @model.const %> do let(:subject) { <%= @model.const %> } - # @todo provide valid and wrong samples here - let(:proper) { [nil, -1, 0, 1, "", "string", Object.new] } - let(:faulty) { [nil, -1, 0, 1, "", "string", Object.new] } + + # @todo <%= @model.const %> :proper, :faulty samples + let(:proper) { [nil, false, 42, "", Object.new] } + let(:faulty) { [nil, false, 42, "", Object.new] } it { - skip "UNDER CONSTRUCTION" + skip "design test for <%= @model.const %>" proper.each{|arg| assert_equal arg, subject.(arg) } faulty.each{|arg| assert_raises(ArgumentError) { subject.(arg) }} } diff --git a/lib/assets/samples/test_service.rb.erb b/lib/assets/samples/test_service.rb.erb index fa0cb2d..979660a 100644 --- a/lib/assets/samples/test_service.rb.erb +++ b/lib/assets/samples/test_service.rb.erb @@ -3,13 +3,13 @@ include <%= @model.namespace %> describe <%= @model.const %> do let(:service) { <%= @model.const %> } - # let(:payload) { {<%= @model.arguments %>} } + # let(:payload) { {<%= @model.keyword_params %>} } it { - skip "UNDER CONSTRUCTION" - service.() - # there are no plugins implemented at the moment - # so its behaviour should be stubbded and mocked + skip "design test for <%= @model.const %>" + assert_raises(ArgumentError) { service.() } + # + # plugin methods should be stubbded or mocked # # PluginHolder.object.stub :get, dummy do # assert_equal dummy, service.(**payload) diff --git a/lib/assets/starter/.dockerignore b/lib/assets/starter/.dockerignore index 8eb1b8a..5933ec8 100644 --- a/lib/assets/starter/.dockerignore +++ b/lib/assets/starter/.dockerignore @@ -8,5 +8,5 @@ # !Gemfile # !Gemfile.lock -# Ignore inside sources +# Ignore backups **/*~ diff --git a/lib/assets/starter/.gitignore b/lib/assets/starter/.gitignore new file mode 100644 index 0000000..6e3d70d --- /dev/null +++ b/lib/assets/starter/.gitignore @@ -0,0 +1,9 @@ +.gitingore +**/*~ +*.log + +.punch +punch.* + +sandbox.rb +sandbox.md diff --git a/lib/assets/starter/CHANGELOG.md b/lib/assets/starter/CHANGELOG.md new file mode 100644 index 0000000..3e1aadc --- /dev/null +++ b/lib/assets/starter/CHANGELOG.md @@ -0,0 +1,7 @@ +% CHANGELOG + +## [Unreleased] + +## 2024-01-01 + +Project punched diff --git a/lib/assets/starter/Dockerfile b/lib/assets/starter/Dockerfile index 2a4e437..6e94ec3 100644 --- a/lib/assets/starter/Dockerfile +++ b/lib/assets/starter/Dockerfile @@ -1,7 +1,12 @@ -FROM ruby:3.2.2 +FROM ruby:3.2.2 as build WORKDIR /app # COPY Gemfile Gemfile.lock . # ENV BUNDLER_WITHOUT development test # RUN gem update --system && bundle install ADD . . CMD ["ruby", "/app/exe/service.rb"] + + +FROM ruby:3.2.2 +COPY --from=build /app /app +CMD ["/bin/hello"] diff --git a/lib/assets/starter/Gemfile b/lib/assets/starter/Gemfile index a09fccc..1a2990e 100644 --- a/lib/assets/starter/Gemfile +++ b/lib/assets/starter/Gemfile @@ -8,7 +8,5 @@ source "https://rubygems.org" group :development do gem "rake" gem "minitest" - # @todo try to use local repository instead of remote - # [Bundler Config. Local Git Repos](https://bundler.io/v2.4/man/bundle-config.1.html) gem "punch", git: "https://github.com/nvoynov/punch.git" end diff --git a/lib/punch.rb b/lib/punch.rb index 77f9975..28a49d3 100644 --- a/lib/punch.rb +++ b/lib/punch.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true require_relative "punch/version" +require_relative "punch/config" require_relative "punch/basics" -require_relative "punch/model" -require_relative "punch/dsl" # model/dsl? -require_relative "punch/decors" # model/decor? +require_relative "punch/models" +require_relative "punch/decors" +require_relative "punch/dsl" require_relative "punch/services" require_relative "punch/plugins" require_relative "punch/cli" diff --git a/lib/punch/basics.rb b/lib/punch/basics.rb index 8c917b4..30b6ba0 100644 --- a/lib/punch/basics.rb +++ b/lib/punch/basics.rb @@ -1,4 +1,3 @@ require_relative "basics/plugin" require_relative "basics/sentry" require_relative "basics/service" -# require_relative "basics/entity" # not used but serves for copying diff --git a/lib/punch/basics/entity.rb b/lib/punch/basics/entity.rb deleted file mode 100644 index 29f0b96..0000000 --- a/lib/punch/basics/entity.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require "securerandom" -require_relative "sentry" - -module Punch - - MustbeUUID = Sentry.new(:id, 'must be UUIDv4') {|v| - v.is_a?(String) && v =~ /\h{8}-(\h{4}-){3}\h{12}/ - } - - # Simple entity class with id - # - # @example - # class User < Entity - # attr_reader :login - # attr_reader :secret - # def initialize(id: nil, login:, secret:) - # super(id) - # @login = login - # @secret = secret - # end - # end - # - class Entity - # @return [UUIDv4] - attr_reader :id - - def initialize(id) - @id = !!id ? MustbeUUID.(id) : SecureRandom.uuid - end - end - -end diff --git a/lib/punch/cli.rb b/lib/punch/cli.rb index 23a9721..cd775a5 100644 --- a/lib/punch/cli.rb +++ b/lib/punch/cli.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "model" +require_relative "models" require_relative "plugins" require_relative "services" require_relative "version" @@ -11,8 +11,8 @@ module Punch module CLI extend self extend Forwardable - def_delegator :PlayboxPlug, :object, :playbox - def_delegator :LoggerPlug, :object, :logger + def_delegator :PlayboxHolder, :object, :playbox + def_delegator :LoggerHolder, :object, :logger # create new Punch project # @param dir [String] project directory name @@ -27,8 +27,6 @@ def folder(dir) # @param *args [Array] where the first item # must be model klass :entity or :service, # the rest items are klass parameters - # - # I can't see any sense to punch just sentry there def source(*args) return unless punch_home? @@ -40,9 +38,15 @@ def source(*args) secure_call { logger.info { "punch #{klass} #{args.join(?\s)}" } - model = ModelBuilder.(*args) - service = klass == :plugin ? PunchPlugin : PunchSource - service.(klass, model) + case klass + when :plugin + model = Punch::Models::Plugin.new(name: args.shift) + PunchPlugin.(model) + when :entity + PunchEntity.(BuildModel.(*args)) + when :service + PunchService.(BuildModel.(*args)) + end } end @@ -73,7 +77,7 @@ def samples # preview the result of the punch def preview(*args) return unless punch_home? - PlayboxPlug.plugin Preview + PlayboxHolder.plugin Preview logger.info { "preview #{args.join(?\s)}" } source(*args) end @@ -157,18 +161,17 @@ def secure_call Usage: bundle add punch --git https://github.com/nvoynov/punch.git - punch new DIRECTORY Create a new Punch project + punch new DIRECTORY punch new Project - punch entity NAME [PARAM..] Punch new entity concept + punch entity NAME [PARAM..] punch new Entity PARAM: name[:][sentry][default] zero or more parameters - punch service NAME [PARAM..] Punch new service concept + punch service NAME [PARAM..] punch new Service PARAM: name[:][sentry][default] zero or more parameters - punch plugin NAME [PARAM..] Punch new plugin concept - PARAM: name[:][sentry][default] zero or more parameters + punch plugin NAME punch new Plugin - punch preview COMMAND Preview generation result + punch preview COMMAND preview Punch result service NAME [PARAM..] command to preview... entity NAME [PARAM..] plugin NAME [PARAM..] diff --git a/lib/punch/config.rb b/lib/punch/config.rb index 7ebab97..97daec4 100644 --- a/lib/punch/config.rb +++ b/lib/punch/config.rb @@ -1,52 +1,56 @@ -# frozen_string_literal: true - -require "psych" - -module Punch - - class << self - - def config - conf = Config.new('lib', 'test', '', 'sentries', 'entities', 'services', 'plugins') - conf.freeze - text = Psych.dump(conf) - head = text.lines.first - body = text.lines.drop(1).join - unless File.exist?(CONFIG) - File.write(CONFIG, body) - return conf - end - body = File.read(CONFIG) - obj = Psych.load([head, body].join, permitted_classes: [Config, Symbol], freeze: true) - obj.is_a?(Config) ? obj : conf # test for faulty load result - end - - def root - dir = File.dirname(__dir__) - File.expand_path("..", dir) - end - - def assets - File.join(root, 'lib', 'assets') - end - - def starter - File.join(assets, 'starter') - end - - def samples - File.join(assets, 'samples') - end - - def domain - File.join(assets, 'domain') - end - - def basics - File.join(root, 'lib', 'punch', 'basics') - end - end - - Config = Struct.new(:lib, :test, :domain, :sentries, :entities, :services, :plugins) - CONFIG = 'punch.yml'.freeze -end +require 'psych' +require_relative 'basics' + +module Punch + def root + dir = File.dirname(__dir__) + File.expand_path("..", dir) + end + + def assets + File.join(root, 'lib', 'assets') + end + + def starter + File.join(assets, 'starter') + end + + def samples + File.join(assets, 'samples') + end + + def domain + File.join(assets, 'domain') + end + + def basics + File.join(root, 'lib', 'punch', 'basics') + end + + class Config < Data.define(:lib, :test, :domain, :sentries, :entities, :services, :plugins) + def self.read + return new.tap{|o| File.write(CONF, Psych.dump(o.to_h.transform_keys(&:to_s)))} \ + unless File.exist?(CONF) + new(**Psych.load(File.read(CONF)).transform_keys(&:to_sym)) + rescue => e + puts 'Punch: reading config error; default config used', e.full_message + new + end + + def initialize( + lib: 'lib', test: 'test', domain: '', + sentries: 'sentries', + entities: 'entities', + services: 'services', + plugins: 'plugins' + ) + super + end + end + + CONF = 'punch.yml'.freeze + + Config.extend(Plugin) + ConfigHolder = Config.plugin + +end diff --git a/lib/punch/decors.rb b/lib/punch/decors.rb index 332a144..57d06fb 100644 --- a/lib/punch/decors.rb +++ b/lib/punch/decors.rb @@ -1,181 +1,4 @@ -require "delegate" -require_relative "model" -require_relative "config" - -module Punch - - class Factory - def self.decorate(klass, model) - conf = Punch.config - base = [conf.lib, conf.test, conf.domain] - case klass.to_sym - when :sentry - SentryDecor.new(*base.concat([conf.sentries, model])) - when :entity - SourceDecor.new(*base.concat([conf.entities, model])) - when :service - SourceDecor.new(*base.concat([conf.services, model])) - when :plugin - SourceDecor.new(*base.concat([conf.plugins, model])) - else - fail "Unknown klass for decoration" - end - end - end - - # Ruby source code decorator for Model - class SourceDecor < SimpleDelegator - - def initialize(home, test, domain, base, model) - @home = home - @test = test - @base = base - @domain = domain - super(model) - end - - # @todo name from command-line can be complex users/create_order - def name - sanitize(super) - end - - def const - constanize(name.split(SEPARATOR).last) - end - - def source - File.join(path, name + '.rb') - end - - def test - name.split(SEPARATOR) - .unshift([@test, @domain, @base].reject(&:empty?)) - .then{|ary| ary.push("test_#{ary.pop}.rb") } - .then{|ary| File.join(ary) } - end - - def test_helper - level = namespace.split(/::/).size - test_level = @test.split(SEPARATOR).size - 1 - level += test_level if test_level > 0 - "#{'../' * level}test_helper" - end - - def require - # it must be relative to source lib/entities/user.rb - source.split(SEPARATOR) - .then{|ary| ary.pop; ary } - .then{|ary| ary.push(ary.pop + '.rb') } - .then{|ary| File.join(ary) } - end - - def require_string - source.split(SEPARATOR) - .then{|ary| ary.drop(ary.size - 2) } - .then{|ary| ary.push(File.basename(ary.pop, '.*')) } - .then{|ary| File.join(ary) } - end - - # @todo see by examples, it should cut .rb - # services/user/create_order.rb -> Services::User - def namespace - File.join([@domain, @base, name].reject(&:empty?)) - .split(SEPARATOR) - .then{|ary| ary.pop; ary } - .map{ constanize(_1) } - .join('::') - end - - def open_namespace - fn = proc{|memo, item| memo << ' ' * memo.size + "module #{item}"} - namespace.split(/::/) - .inject([], &fn) - .join(?\n) - end - - def close_namespace - fn = proc{|memo, _| memo << ' ' * memo.size + "end"} - namespace.split(/::/) - .inject([], &fn) - .reverse - .join(?\n) - end - - def properties - params.map{|param| - param_type = param.sentry? ? param.sentry.capitalize : 'Object' - <<~EOF - # @return [#{param_type}] #{param.desc} - attr_reader :#{param.name} - EOF - }.join(?\n) - end - - def yardoc - params.map{|param| - param_type = param.sentry? ? param.sentry.capitalize : 'Object' - "# @param #{param.name} [#{param_type}] #{param.desc}" - }.push("# @return [@todo] what?").join(?\n) - end - - def parameters - params.map{|param| - ary = [param.name] - ary << ':' if param.keyword? - ary << ' =' if param.default? && param.positional? - ary << ?\s + param.default if param.default? - ary.join - }.join(', ') - end - - def assignment - params.map{|param| - source = param.name - source = "Mustbe#{constanize(param.sentry)}.(#{param.name})" if param.sentry? - "@#{param.name} = #{source}" - }.join(?\n) - end - - def arguments - fn = proc{|pa| pa.keyword? ? "#{pa.name}: @#{pa.name}" : "@#{pa.name}"} - params.map(&fn).join(', ') - end - - SEPARATOR = %r{[\\\/]}.freeze - - protected - - def path - File.join([@home, @domain, @base].reject(&:empty?)) - end - - def relative_path - File.join([@domain, @base, "#{name}.rb"].reject(&:empty?)) - end - - def sanitize(str) - # @todo maybe Basic#name should return sanitized string? - str = str.to_s unless str.is_a?(String) - str.downcase.strip.gsub(/\s{1,}/, '_') - end - - def constanize(str) - sanitize(str).split(?_).map(&:capitalize).join - end - - end - - class SentryDecor < SourceDecor - def const - # constanize("#{PREFIX}#{name}") - PREFIX + super - end - - def source - path + '.rb' - end - - PREFIX = 'Mustbe'.freeze - end - -end +require_relative 'decors/decor' +require_relative 'decors/param' +require_relative 'decors/model' +require_relative 'decors/sentry' diff --git a/lib/punch/decors/decor.rb b/lib/punch/decors/decor.rb new file mode 100644 index 0000000..c174ac3 --- /dev/null +++ b/lib/punch/decors/decor.rb @@ -0,0 +1,32 @@ +require 'delegate' +require 'forwardable' + +module Punch + module Decors + + class Decor < SimpleDelegator + extend Forwardable + def_delegator :ConfigHolder, :object, :conf + + def initialize(model, context = nil) + super(model) + @context = context + end + + def name + sanitize(super) + end + + protected + + attr_reader :context + + def sanitize(arg) + String.new( arg.is_a?(String) ? arg : arg.to_s) + .gsub(/[^\w\s]/, ?\s) + .gsub(/\s{1,}/, '_') + end + end + + end +end diff --git a/lib/punch/decors/model.rb b/lib/punch/decors/model.rb new file mode 100644 index 0000000..247647e --- /dev/null +++ b/lib/punch/decors/model.rb @@ -0,0 +1,75 @@ +require_relative 'decor' +require_relative 'param' + +module Punch + module Decors + + class Model < Decor + + def const + name.split(?_).map(&:capitalize).join + end + + def namespace + [conf.domain, context].reject(&:empty?) + .then{|a| File.join(a)} + .split(PATH_SEPARATOR) + .map(&:capitalize) + .join('::') + end + + def indentation + ?\s * 2 * namespace.split(/::/).size + end + + def open_namespace + fu = proc{|memo, e| memo << "#{?\s * 2 * memo.size}module #{e}"} + namespace.split(/::/).inject([], &fu).join(?\n) + end + + def close_namespace + fu = proc{|memo, e| memo << "#{?\s * 2 * memo.size}end" } + namespace.split(/::/).inject([], &fu).reverse.join(?\n) + end + + def regular_params + # @todo this does not work with decorated stuff, but why? + # Array.new(params) + # .sort_by.with_index{|x, idx| [x, idx]} + # .map(&:regular).join(', ') + fu = proc{|arg| arg.name + (arg.default? ? "= #{arg.default}" : '')} + __getobj__.params + .sort_by.with_index{|x, idx| [x, idx]} + .map(&fu).join(', ') + end + + def keyword_params + params.map(&:keyword).join(', ') + end + + def params_yardoc + params.map(&:yardoc).join(?\n) + end + + def params_yarpro + params.map(&:yarpro).join(?\n) + end + + def params_guard + params.map(&:guard).reject(&:empty?).join(?\n) + end + + def test_helper + '../' * namespace.split(/::/).size + 'test_helper' + end + + def params + super.map{|e| Param.new(e) } + end + + PATH_SEPARATOR = %r{[\\\/]}.freeze + + end + + end +end diff --git a/lib/punch/decors/param.rb b/lib/punch/decors/param.rb new file mode 100644 index 0000000..e696396 --- /dev/null +++ b/lib/punch/decors/param.rb @@ -0,0 +1,47 @@ +require_relative 'decor' + +module Punch + module Decors + + class Param < Decor + + def sentry + return '' unless sentry? + super == super.upcase ? super : sanitize(super).capitalize + end + + def regular + name + (default? ? " = #{default}" : '') + end + + def keyword + name + ?: + (default? ? " #{default}" : '') + end + + # @return [String] Yard param comment + def yardoc + "# @param #{name} [Object] #{description}" + end + + # @return [String] Yard property macro + def yarpro + <<~EOF.strip + # !attribute [r] #{name} + # @return [Object] #{description} + EOF + end + + def guard + return '' unless sentry? + "#{name} = Mustbe#{sentry}.(#{name}, :#{name})" + end + + protected + + def description + [sentry, desc].reject(&:empty?).join(', ') + end + end + + end +end diff --git a/lib/punch/decors/sentry.rb b/lib/punch/decors/sentry.rb new file mode 100644 index 0000000..b44497c --- /dev/null +++ b/lib/punch/decors/sentry.rb @@ -0,0 +1,30 @@ +require_relative 'decor' + +module Punch + module Decors + + class Sentry < Decor + + def const + PREFIX + name.split(?_).map(&:capitalize).join + end + + def namespace + [conf.domain, conf.sentries].reject(&:empty?) + .then{|a| File.join(a)} + .split(PATH_SEPARATOR) + .map(&:capitalize) + .join('::') + end + + def test_helper + '../' * namespace.split(/::/).size + 'test_helper' + end + + PREFIX = 'Mustbe' + PATH_SEPARATOR = %r{[\\\/]}.freeze + + end + + end +end diff --git a/lib/punch/dsl.rb b/lib/punch/dsl.rb index 085f9ff..93a836b 100644 --- a/lib/punch/dsl.rb +++ b/lib/punch/dsl.rb @@ -1,2 +1,4 @@ +require_relative "dsl/dsl" +require_relative "dsl/model" +require_relative "dsl/actor" require_relative "dsl/domain" -require_relative "dsl/builder" diff --git a/lib/punch/dsl/actor.rb b/lib/punch/dsl/actor.rb new file mode 100644 index 0000000..b62ac31 --- /dev/null +++ b/lib/punch/dsl/actor.rb @@ -0,0 +1,21 @@ +require_relative 'dsl' +require_relative 'model' + +module Punch + module DSL + + Actor = DSL.define(Models::Actor) do + def initialize(name, desc = '') + super() + @store[:name] = name + @store[:desc] = desc + end + + def service(name, desc = '', &block) + @store[:services] ||= [] + @store[:services] << Model.(name, desc, &block) + end + end + + end +end diff --git a/lib/punch/dsl/builder.rb b/lib/punch/dsl/builder.rb deleted file mode 100644 index 5ddb6ba..0000000 --- a/lib/punch/dsl/builder.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -require_relative "domain" - -module Punch - module DSL - - class Builder - attr_reader :domain - - def self.build(&block) - bldr = new - bldr.instance_eval(&block) - bldr.domain - end - - def initialize - @domain = Domain.new(:domain) - end - private_class_method :new - - def sentry(name, desc = '', block: '') - @domain.add_sentry( - Sentry.new(name, desc, block: block) - ) - end - - def entity(name, desc = '', &block) - entity = Entity.new(name, desc) - entity.instance_eval(&block) if block - @domain.add_entity(entity) - end - - def service(name, desc = '', &block) - service = Service.new(name, desc) - service.instance_eval(&block) if block - @domain.add_service(service) - end - - def actor(name, desc = '', &block) - actor = Actor.new(name, desc) - actor.instance_eval(&block) - @domain.add_actor(actor) - end - - def plugin(name, desc = '', &block) - plugin = Plugin.new(name, desc) - plugin.instance_eval(&block) if block - @domain.add_plugin(plugin) - end - end - - end -end diff --git a/lib/punch/dsl/domain.rb b/lib/punch/dsl/domain.rb index f4750d6..98c5a3b 100644 --- a/lib/punch/dsl/domain.rb +++ b/lib/punch/dsl/domain.rb @@ -1,115 +1,42 @@ -# frozen_string_literal: true - -require_relative "../model" - -module Punch - module DSL - - class Sentry < Punch::SentryModel - end - - module ParamMixin - def param(name, desc = '', keyword: true, default: nil, sentry: '') - para = Param.new(name, desc, - keyword: keyword, - default: default, - sentry: sentry) - self.<<(para) - end - end - - class Entity < Punch::Model - include ParamMixin - end - - class Service < Punch::Model - include ParamMixin - end - - class Plugin < Punch::Model - include ParamMixin - end - - class Actor < Punch::Basic - def initialize(name, desc = '') - super(name, desc) - @services = {} - end - - def <<(service) - fail ArgumentError, "Service required" unless service.is_a? Service - @services[service.name] = service - end - - def service(name, desc = '', &block) - service = Service.new("#{@name} #{name}", desc) - service.instance_eval(&block) if block - self.<<(service) - end - - def services - @services.values - end - - end - - class Domain < Punch::Basic - - def initialize(name, desc = '') - super(name, desc) - # Hash to ensure uniq names - @sentries = {} - @entities = {} - @services = {} - @plugins = {} - @actors = {} - end - - def add_sentry(sentry) - @sentries[sentry.name] = sentry - end - - def add_entity(entity) - @entities[entity.name] = entity - end - - def add_service(service) - @services[service.name] = service - end - - def add_actor(actor) - @actors[actor.name] = actor - end - - def add_plugin(plugin) - @plugins[plugin.name] = plugin - end - - def sentries - @sentries.values - end - - def entities - @entities.values - end - - def services - Array.new(@services.values).tap{|ary| - actors.each{|a| - ary.concat(a.services) - } - } - end - - def actors - @actors.values - end - - def plugins - @plugins.values - end - - end - - end -end +require_relative 'dsl' +require_relative 'model' +require_relative 'actor' + +module Punch + module DSL + + Domain = DSL.define(Models::Domain) do + def initialize(name = '', desc = '') + super() + @store[:name] = name + @store[:desc] = desc + end + + def sentry(name, desc = '', block: '') + @store[:sentries] ||= [] + @store[:sentries] << Models::Sentry.new(name: name, desc: desc, proc: block) + end + + def entity(name, desc = '', &block) + @store[:entities] ||= [] + @store[:entities] << Model.(name, desc, &block) + end + + def service(name, desc = '', &block) + @store[:services] ||= [] + @store[:services] << Model.(name, desc, &block) + end + + def actor(name, desc = '', &block) + @store[:actors] ||= [] + @store[:actors] << Actor.(name, desc, &block) + end + + def plugin(name, desc = '') + @store[:plugins] ||= [] + @store[:plugins] << Models::Plugin.new(name: name, desc: desc) + end + end + + end +end diff --git a/lib/punch/dsl/dsl.rb b/lib/punch/dsl/dsl.rb new file mode 100644 index 0000000..f203cef --- /dev/null +++ b/lib/punch/dsl/dsl.rb @@ -0,0 +1,86 @@ +require_relative '../models' + +module Punch + module DSL + + Models = Punch::Models + + # Basic DSL for Data class + # + # @example + # + # Para = Data.define(:name, :type) + # ParaDSL = DSL.define(Para) + # para = ParaDSL.() { + # name ?a + # type ?b + # } + # + # Func = Data.define(:name, :params) + # FuncDSL = DSL.define(Func) do + # def param(name, type) + # @store[:params] ||= [] + # @store[:params] << Para.new(name: name, type: type) + # end + # + # def initialize(name) + # super() + # @store[:name] = name + # end + # end + # + # func = FuncDSL.(:func) { + # param ?a, ?b + # param ?b, ?c + # } + # + # Dom = Data.define(:name, :funcs) + # DomDSL = DSL.define(Dom) do + # def initialize + # super + # end + # + # def func(name, &block) + # @store[:funcs] ||= [] + # @store[:funcs] << FuncDSL.(name, &block) + # end + # end + # dom = DomDSL.build { + # name :domain + # func :foo do + # param :bar, ?a + # param :buz, ?b + # end + # } + class DSL + class << self + def define(klass, &block) + fail "klass must be < Data" unless klass < Data + Class.new(self) do + @klass = klass + @klass.members.each {|key| + define_method(key){|arg| @store[key] = arg} + } + class_eval(&block) if block + singleton_class.undef_method :define + end + end + + def call(*args, **kwargs, &block) + new(*args, **kwargs) + .tap {|o| o.instance_eval(&block) if block_given? } + .then{|o| @klass.new(**o.instance_variable_get(:@store)) } + end + alias :build :call + end + + # hide DataDSL.new + private_class_method :new + + def initialize + @store = {} + end + end + + end +end diff --git a/lib/punch/dsl/model.rb b/lib/punch/dsl/model.rb new file mode 100644 index 0000000..2b12ad0 --- /dev/null +++ b/lib/punch/dsl/model.rb @@ -0,0 +1,20 @@ +require_relative 'dsl' + +module Punch + module DSL + + Model = DSL.define(Models::Model) do + def initialize(name, desc = '') + super() + @store[:name] = name + @store[:desc] = desc + end + + def param(name, sentry: nil, default: nil, desc: '') + @store[:params] ||= [] + @store[:params] << Models::Param.new(name: name, sentry: sentry, default: default, desc: desc) + end + end + + end +end diff --git a/lib/punch/model.rb b/lib/punch/model.rb deleted file mode 100644 index da4bc2b..0000000 --- a/lib/punch/model.rb +++ /dev/null @@ -1,125 +0,0 @@ -# frozen_string_literal: true - -require_relative "basics" - -module Punch - - class Basic - attr_reader :name - attr_reader :desc - - def initialize(name, desc = '') - @name = name - @desc = desc - end - end - - class SentryModel < Basic - attr_reader :block - - def initialize(name, desc = '', block: '') - super(name, desc) - @block = block - @block = 'fail "UNDER CONSTRUCTION"' if @block.empty? - @desc = "must be #{name.capitalize}" if @desc.empty? - end - end - - class Param < Basic - attr_reader :sentry - attr_reader :default - - def initialize(name, desc = '', keyword: true, default: nil, sentry: '') - super(name, desc) - @keyword = keyword - @default = default - @sentry = sentry - end - - def sentry? - !@sentry.empty? - end - - def default? - !!@default - end - - def keyword? - @keyword == true - end - - def positional? - !keyword? - end - - def <=>(another) - return nil unless self.class == another.class - - return 1 if keyword? && another.positional? - return -1 if positional? && another.keyword? - - if positional? && another.positional? - return 1 if default? && !another.default? - return -1 if another.default? && !default? - end - - # @todo for Ruby purpose it does not have sense - # to sort keywords by default value, but for - # readers it might have - if keyword? && another.keyword? - return 1 if default? && !another.default? - return -1 if another.default? && !default? - end - - 0 - end - end - - class Model < Basic - - def initialize(name, desc = '') - super(name, desc) - @params = {} - end - - def <<(param) - fail ArgumentError, "Param required" unless param.is_a? Param - @params[param.name] = param - end - - def params - stable_sort(@params.values) - end - - protected - - def stable_sort(ary) - ary.sort_by.with_index{|x, idx| [x, idx]} - end - end - - # @param *args [Array] strings model representation - # @return [Model] builded from args0 - class ModelBuilder < Service - def call - # service user/create_order user - name = @args.shift - - Model.new(name).tap{|model| - @args.each{|str| model.<< para(str) } - } - end - - def para(str) - match = str.match(PARAM_CAPTURE) - name = match[:name] - keyword = !!match[:keyword] - sentry = match[:sentry] - default = match[:default].empty? ? nil : match[:default] - Param.new(name, keyword: keyword, default: default, sentry: sentry) - end - - PARAM_CAPTURE = /(?\w*)(?:)?(?\w*)?\s?(?.*)?$/.freeze - end - -end diff --git a/lib/punch/models.rb b/lib/punch/models.rb new file mode 100644 index 0000000..3ee859c --- /dev/null +++ b/lib/punch/models.rb @@ -0,0 +1,7 @@ +require_relative 'models/sentry' +require_relative 'models/param' +require_relative 'models/model' +require_relative 'models/build_model' +require_relative 'models/actor' +require_relative 'models/plugin' +require_relative 'models/domain' diff --git a/lib/punch/models/actor.rb b/lib/punch/models/actor.rb new file mode 100644 index 0000000..a7c85b7 --- /dev/null +++ b/lib/punch/models/actor.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +module Punch + module Models + + class Actor < Data.define(:name, :services, :desc) + def initialize(name:, services: [], desc: '') + super + end + end + + end +end diff --git a/lib/punch/models/build_model.rb b/lib/punch/models/build_model.rb new file mode 100644 index 0000000..a9572f9 --- /dev/null +++ b/lib/punch/models/build_model.rb @@ -0,0 +1,30 @@ +require_relative 'param' +require_relative 'model' + +module Punch + + # Build Model from Array + # @example + # + # BuildModel.('create', 'user_id', 'user_name:string 42') + # + module BuildModel + extend self + include Punch::Models + + def call(*args) + name, *params = args + para = params.map{|s| + kwar = s.match(PARSER).named_captures + .transform_keys(&:to_sym) + .reject{ |_, v| v == '' } + Param.new(**kwar) + } + Model.new(name: name, params: para) + end + + alias :build :call + + PARSER = /(?\w*)(:?(?\w*))?(\s?(?.*))?$/.freeze + end +end diff --git a/lib/punch/models/domain.rb b/lib/punch/models/domain.rb new file mode 100644 index 0000000..ba1006a --- /dev/null +++ b/lib/punch/models/domain.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +module Punch + module Models + + class Domain < Data.define(:name, :actors, :sentries, :entities, :services, :plugins, :desc) + def initialize(name:, actors: [], sentries: [], entities: [], services: [], plugins: [], desc: '') + super + end + end + + end +end diff --git a/lib/punch/models/model.rb b/lib/punch/models/model.rb new file mode 100644 index 0000000..504f740 --- /dev/null +++ b/lib/punch/models/model.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +module Punch + module Models + + class Model < Data.define(:name, :params, :desc) + def initialize(name:, params: [], desc: '') + super + end + end + + end +end diff --git a/lib/punch/models/param.rb b/lib/punch/models/param.rb new file mode 100644 index 0000000..bbb8185 --- /dev/null +++ b/lib/punch/models/param.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +module Punch + module Models + + class Param < Data.define(:name, :sentry, :default, :desc) + def initialize(name:, sentry: nil, default: nil, desc: '') + super + end + + def <=>(another) + return nil unless self.class == another.class + return 1 if default && another.default.nil? + return -1 if default.nil? && another.default + 0 + end + + def sentry? = !sentry.nil? + def default? = !default.nil? + end + + end +end diff --git a/lib/punch/models/plugin.rb b/lib/punch/models/plugin.rb new file mode 100644 index 0000000..9683c42 --- /dev/null +++ b/lib/punch/models/plugin.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +module Punch + module Models + + class Plugin < Data.define(:name, :desc) + def initialize(name:, desc: '') + super + end + end + + end +end diff --git a/lib/punch/models/sentry.rb b/lib/punch/models/sentry.rb new file mode 100644 index 0000000..151e817 --- /dev/null +++ b/lib/punch/models/sentry.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +module Punch + module Models + + class Sentry < Data.define(:name, :desc, :proc) + def initialize(name:, desc: '', proc: "fail \"desing the sentry\"") + super + end + end + + end +end diff --git a/lib/punch/plugins.rb b/lib/punch/plugins.rb index b801e01..92952fe 100644 --- a/lib/punch/plugins.rb +++ b/lib/punch/plugins.rb @@ -3,14 +3,14 @@ require "logger" require_relative "plugins/playbox" require_relative "plugins/preview" +require_relative "config" module Punch - PlayboxPlug = Playbox.plugin - PreviewPlug = Preview.plugin + PlayboxHolder = Playbox.plugin + PreviewHolder = Preview.plugin Logger.extend(Plugin) - LoggerPlug = Logger.plugin - LoggerPlug.object = Logger.new(IO::NULL) + LoggerHolder = Logger.plugin end diff --git a/lib/punch/plugins/playbox.rb b/lib/punch/plugins/playbox.rb index 631f3f9..8906aae 100644 --- a/lib/punch/plugins/playbox.rb +++ b/lib/punch/plugins/playbox.rb @@ -3,10 +3,13 @@ require_relative "../config" require_relative "hashed" require "fileutils" +require "forwardable" module Punch class Playbox + extend Forwardable + def_delegator :ConfigHolder, :object, :conf extend Plugin include FileUtils include Hashed @@ -19,7 +22,7 @@ def punch_home(dir) end def punch_home? - File.exist?(Punch::CONFIG) || File.exist?('Gemfile') + File.exist?(Punch::CONF) || File.exist?('Gemfile') end SAMPLES = '.punch/samples'.freeze @@ -49,9 +52,9 @@ def punch_basics basicsrb = 'lib/punch/basics.rb' File.write(basicsrb, sources.map(&fn).join(?\n)) File.write('lib/punch.rb', "require_relative \"punch/basics\"") - location = [config.lib, config.domain, 'basics.rb'].reject(&:empty?) + location = [conf.lib, conf.domain, 'basics.rb'].reject(&:empty?) basicsrb = File.join(*location) - punchrb = config.domain.empty? ? "punch" : "../punch" + punchrb = conf.domain.empty? ? "punch" : "../punch" content = <<~EOF require_relative "#{punchrb}" @@ -142,12 +145,13 @@ def append(name, string) File.open(name, 'a') do |f| f.puts string end + [name + '~'] end # @param model [Symbol] # @return [Array] array of two samples, where the first for source and the second for test def samples(model) - payload = case model + payload = case model.to_sym when :sentry; ['sentry.rb.erb', 'test_sentry.rb.erb'] when :entity; ['entity.rb.erb', 'test_entity.rb.erb'] when :service; ['service.rb.erb', 'test_service.rb.erb'] @@ -160,9 +164,9 @@ def samples(model) protected - def config - @config ||= Punch.config - end + # def config + # @config ||= Punch.config + # end def punch_home! fail Failure, "Punch folder required!" @@ -173,7 +177,8 @@ def furnish(dir) Dir.chdir(dir) { src = File.join(Punch.starter, '.') cp_r src, Dir.pwd - Punch.config # just create punch.yml + # Punch.config # just create punch.yml + conf Dir.glob("#{Dir.pwd}/**/*") } end diff --git a/lib/punch/services.rb b/lib/punch/services.rb index 13abe7b..a651de1 100644 --- a/lib/punch/services.rb +++ b/lib/punch/services.rb @@ -1,5 +1,13 @@ -require_relative "services/punch_sentry" -require_relative "services/punch_source" +# require_relative "services/punch_source" +# require_relative "services/punch_plugin" +# require_relative "services/punch_domain" +# require_relative "services/report_status" + +require_relative "services/service" +require_relative "services/punch_sentries" +require_relative "services/punch_model" +require_relative "services/punch_entity" +require_relative "services/punch_service" require_relative "services/punch_plugin" require_relative "services/punch_domain" require_relative "services/report_status" diff --git a/lib/punch/services/punch_domain.rb b/lib/punch/services/punch_domain.rb index 397f9f3..6934da9 100644 --- a/lib/punch/services/punch_domain.rb +++ b/lib/punch/services/punch_domain.rb @@ -1,53 +1,68 @@ # frozen_string_literal: true -require_relative "../decors" require_relative "service" -require_relative "punch_sentry" -require_relative "punch_source" +require_relative "punch_sentries" +require_relative "punch_model" require_relative "punch_plugin" module Punch module Services - MustbeDomain = Sentry.new(:domain, 'must be Punch::DSL::Domain' - ) {|v| v.is_a?(Punch::DSL::Domain)} + MustbeDomain = Sentry.new(:domain, 'must be Punch::Models::Domain' + ) {|v| v.is_a?(Punch::Models::Domain)} class PunchDomain < Service def call log = [] - @domain = MustbeDomain.(@args[0]) + domain = MustbeDomain.(@args[0]) @block = proc{|event, payload|} unless @block - payload = @domain.sentries - @block.(:stage, 'sentries') if payload.any? - log.concat PunchSentry.(*payload) if payload.any? + if domain.sentries.any? + @block.(:stage, 'sentries') + log.concat PunchSentries.(domain.sentries) + end - payload = @domain.entities - @block.(:stage, 'entities') if payload.any? - log.concat PunchSource.(:entity, *payload) if payload.any? + if domain.entities.any? + @block.(:stage, 'entities') + domain.entities.each{|e| + log.concat PunchModel.(e, :entity, conf.entities) + } + end - payload = @domain.services - @block.(:stage, 'services') if payload.any? - log.concat PunchSource.(:service, *payload) if payload.any? + services = Array.new(domain.services) + services.concat domain.actors.inject([]){|memo, a| + memo.concat a.services.map{|s| + s.with(name: a.name.to_s + ?\s + s.name.to_s) + } + } - payload = @domain.plugins - @block.(:stage, 'plugins') if payload.any? - log.concat PunchPlugin.(:plugin, *payload) if payload.any? + if services.any? + @block.(:stage, 'services') + services.each{|e| + log.concat PunchModel.(e, :service, conf.services) + } + end + + if domain.plugins.any? + @block.(:stage, 'plugins') + domain.plugins.each{|e| + log.concat PunchPlugin.(e) + } + end log.uniq! - return log if config.domain.empty? + return log if conf.domain.empty? - reqstr = "require_relative \"#{config.domain}/%s\"" + reqstr = "require_relative \"#{conf.domain}/%s\"" content = [] content << reqstr % "basics" - content << reqstr % "config" - content << reqstr % config.sentries if @domain.sentries.any? - content << reqstr % config.entities if @domain.entities.any? - content << reqstr % config.services if @domain.services.any? - content << reqstr % config.plugins if @domain.plugins.any? - domainrb = File.join(config.lib, config.domain + '.rb') - log.concat storage.write(domainrb, content.join(?\n)) - log + content << reqstr % conf.sentries if domain.sentries.any? + content << reqstr % conf.entities if domain.entities.any? + content << reqstr % conf.services if services.any? + content << reqstr % conf.plugins if domain.plugins.any? + domainrb = File.join(conf.lib, conf.domain + '.rb') + log.concat store.write(domainrb, content.join(?\n)) + log.reject{|s| s =~ /~$/}.sort end end diff --git a/lib/punch/services/punch_entity.rb b/lib/punch/services/punch_entity.rb new file mode 100644 index 0000000..d9091bf --- /dev/null +++ b/lib/punch/services/punch_entity.rb @@ -0,0 +1,14 @@ +require_relative 'punch_model' + +module Punch + module Services + + class PunchEntity < PunchModel + # @param model [Models::Plugin] + def initialize(model) + super(model, :entity, conf.entities) + end + end + + end +end diff --git a/lib/punch/services/punch_model.rb b/lib/punch/services/punch_model.rb new file mode 100644 index 0000000..4005651 --- /dev/null +++ b/lib/punch/services/punch_model.rb @@ -0,0 +1,79 @@ +require_relative "service" +require_relative "punch_sentries" + +module Punch + module Services + + # Basic Model Puncher + # @example + # model = Models::Model.new(...) + # PunchModel.(model, :entity, conf.entities) + # + # + class PunchModel < Service + def initialize(model, type, location) + @model = Decors::Model.new(model, location) + @type = type + @location = location + end + + def call + @log = [] + # @todo punch sentries + punch_sentries + punch + @log + end + + def punch_sentries + sentries = @model.params.select(&:sentry?) + .map(&:sentry).uniq + .map{|e| Models::Sentry.new(name: e) } + PunchSentries.(sentries) + end + + def punch + src_erb, tst_erb = store.samples(@type) + @log.concat store.write(srcrb, render(src_erb)) + requirerb + @log.concat store.write(tstrb, render(tst_erb)) + end + + def render(erb) + # @model binding automatically + ERB.new(erb, trim_mode: '%').result(binding) + end + + def requirerb + source = store.exist?(reqrb) ? store.read(reqrb) : '' + reqstr = "require_relative '%s'" % File.join(@location, @model.name) + return if source =~ %r{reqstr} + store.append(reqrb, reqstr) + @log << reqrb + '~' + end + + def getrb(head, tail = @model.name) + [head, conf.domain, @location, tail].reject(&:empty?) + .then{|a| File.join(a) + '.rb' } + end + + def srcrb + getrb(conf.lib) + end + + def tstrb + getrb(conf.test).split(PATH_SEPARATOR) + .tap{|a| a.push('test_' + a.pop) } + .then{ File.join(_1) } + end + + PATH_SEPARATOR = %r{[\\\/]}.freeze + + def reqrb + [conf.lib, conf.domain, @location].reject(&:empty?) + .then{ File.join(_1) + '.rb' } + end + end + + end +end diff --git a/lib/punch/services/punch_plugin.rb b/lib/punch/services/punch_plugin.rb index 0465613..dddcfa8 100644 --- a/lib/punch/services/punch_plugin.rb +++ b/lib/punch/services/punch_plugin.rb @@ -1,42 +1,36 @@ -# frozen_string_literal: true - -require_relative "punch_source" - -module Punch - module Services - - # In differs from the PunchSource by punching "config.rb" with plugin hoders - class PunchPlugin < PunchSource - - CONFIGRB = 'config.rb'.freeze - # @todo it should be also erb template, and maybe basic.rb - CONFIGRB_HEADER = <<~EOF.freeze - require_relative "%s" - include %s - - EOF - - def requirerb(model) - super(model) - location = [config.lib, config.domain, CONFIGRB].reject(&:empty?) - configrb = File.join(*location) - holders = if File.exist?(configrb) - storage.read(configrb) - else - incl = [config.domain, config.plugins] - .reject(&:empty?) - .map(&:capitalize) - .join('::') - @log.concat storage.write(configrb, CONFIGRB_HEADER % [ - config.plugins, incl]) - '' - end - declare = "#{model.const}Holder = #{model.const}.plugin" - return if holders =~ %r{#{declare}} - storage.append(configrb, declare) - @log << configrb + '~' - end - end - - end -end +require_relative 'service' + +module Punch + module Services + + class PunchPlugin < PunchModel + # @param model [Models::Plugin] + def initialize(model) + super(model, :plugin, conf.plugins) + end + + # @todo PunchModel should not punch sentries + # but PunchEntity and PunchService sould! + def call + @log = [] + punch + @log + end + + protected + + def requirerb + # the same part as for PunchModel + source = store.exist?(reqrb) ? store.read(reqrb) : '' + reqstr = "require_relative '%s'" % File.join(@location, @model.name) + return if source =~ %r{reqstr} + # but for plugin there should be also PluginHodler = Plugin.plugin + hldstr = "#{@model.const}Holder = #{@model.const}.plugin" + @log.concat "#{reqstr}\n#{hldstr}".then{|str| + store.exist?(reqrb) ? store.append(reqrb, str) : store.write(reqrb, str) + } + end + end + + end +end diff --git a/lib/punch/services/punch_sentries.rb b/lib/punch/services/punch_sentries.rb new file mode 100644 index 0000000..e82a5bc --- /dev/null +++ b/lib/punch/services/punch_sentries.rb @@ -0,0 +1,74 @@ +require 'erb' +require_relative 'service' + +module Punch + module Services + + protected + + class PunchSentries < Service + # @param models [Array] + def initialize(models) + @models = models.map{|e| Decors::Sentry.new(e) } + end + + # @return [Array] log of punched files + def call + @log, @buffer = [], [] + @srcerb, @tsterb = store.samples(:sentry) + .then{|src, tst| [ + ERB.new(src, trim_mode: '%<>'), + ERB.new(tst, trim_mode: '%<>') + ]} + punched = already_punched + @models + .reject{|e| punched.include?(e.const) } + .each(&method(:punch)) + return [] if @buffer.empty? # nothing was punched + + store.append(srcrb, @buffer.join(?\n) ) + @log.unshift srcrb + end + + protected + + def punch(model) + @model = model + @buffer << @srcerb.result(binding) + body = @tsterb.result(binding) + file = tstrb(model.name) + store.write(file, body) + @log << file + end + + def already_punched + unless File.exist?(srcrb) + store.append(srcrb, STARTER) + [] + end + + File.read(srcrb).lines + .select{|l| l.match?(/Sentry.new/)} + .map{|l| l.split(?\s).first.strip } + end + + def srcrb + [conf.lib, conf.domain, conf.sentries].reject(&:empty?) + .then{ File.join(_1) + '.rb' } + end + + def tstrb(name) + [ conf.lib, conf.domain, conf.sentries, 'test_' + name + ].reject(&:empty?) + .then{ File.join(_1) + '.rb' } + end + + STARTER = <<~EOF.freeze + # frozen_string_literal: true + require_relative "basics" + + EOF + end + + end +end diff --git a/lib/punch/services/punch_sentry.rb b/lib/punch/services/punch_sentry.rb deleted file mode 100644 index 076fbfc..0000000 --- a/lib/punch/services/punch_sentry.rb +++ /dev/null @@ -1,82 +0,0 @@ -# frozen_string_literal: true - -require "erb" -require_relative "service" -require_relative "../decors" - -module Punch - module Services - - MustbeSentryArray = Sentry.new(:args, 'must be Array' - ) {|v| v.is_a?(Array) && v.any? && v.all?{|i| i.is_a?(Punch::SentryModel)} } - - # Punches bunch of Sentries from *args - # @param *args [Array] - # @return [Array] with punched sources - class PunchSentry < Service - - def call - MustbeSentryArray.(@args) - - @log, @buffer = [], [] - @source, @test = storage.samples(:sentry) - @args.map{|m| decorator(m) } - .reject{|d| declared?(d.const)} - .each {|d| punch(d)} - return [] if @buffer.empty? # nothing was punched - - storage.append(sentriesrb, @buffer.join(?\n) ) - @log.unshift sentriesrb - end - - protected - - def sentriesrb - @sentriesrb ||= decorator(@args[0]).source - end - - def decorator(model) - Factory.decorate(:sentry, model) - end - - def punch(model) - @buffer << render(@source, model) - test = render(@test, model) - storage.write(model.test, test) - @log << model.test - end - - def render(erb, model) - @model = model - renderer = ERB.new(erb, trim_mode: '-') - renderer.result(binding) - end - - # @return true when the const already declared in sentries.rb - def declared?(const) - declared.include?(const) - end - - def declared - @declared ||= begin - sentries = if storage.exist?(sentriesrb) - storage.read(sentriesrb) - else - storage.append(sentriesrb, SENTRIES) - SENTRIES - end - sentries.lines - .select{|l| l.match?(/Sentry.new/)} - .map{|l| l.split(?\s).first.strip } - end - end - - SENTRIES = <<~EOF.freeze - # frozen_string_literal: true - require_relative "basics" - - EOF - end - - end -end diff --git a/lib/punch/services/punch_service.rb b/lib/punch/services/punch_service.rb new file mode 100644 index 0000000..2edada4 --- /dev/null +++ b/lib/punch/services/punch_service.rb @@ -0,0 +1,14 @@ +require_relative 'punch_model' + +module Punch + module Services + + class PunchService < PunchModel + # @param model [Models::Plugin] + def initialize(model) + super(model, :service, conf.services) + end + end + + end +end diff --git a/lib/punch/services/punch_source.rb b/lib/punch/services/punch_source.rb deleted file mode 100644 index a5acbcf..0000000 --- a/lib/punch/services/punch_source.rb +++ /dev/null @@ -1,93 +0,0 @@ -# frozen_string_literal: true - -require "erb" -require_relative "../decors" -require_relative "service" -require_relative "punch_sentry" - -module Punch - module Services - - MustbeModelArray = Sentry.new(:args, 'must be Array' - ) {|v| v.is_a?(Array) && v.any? && v.all?{|i| i.is_a?(Punch::Model)} } - - MustbeKlassDecor = Sentry.new(:args0, 'must be :sentry|:entity|:service|:plugin' - ) {|v| v.is_a?(Symbol) && %i(sentry entity service plugin).any?{|s| s == v} } - - # Punches bunch of models from *args - # - # @example - # signup = Model.new('user/signup') - # signin = Model.new('user/signin') - # PunchSource.(:service, signup, signin) - # # => [ - # # 'lib/services.rb', - # # 'lib/services.rb~', - # # 'lib/services/signup.rb', - # # 'test/services/test_signup.rb', - # # 'lib/services/signin.rb', - # # 'test/services/test_signin.rb' - # # ] - # - # @param *args [klass, Array] - # @return [Array] with punched sources - class PunchSource < Service - - def call - @klass = MustbeKlassDecor.(@args.shift) - MustbeModelArray.(@args) - decorator(@args.first) # must fail for unknown klass - @log = [] - payload = sentries - @log.concat PunchSentry.(*payload) if payload.any? - - @source, @test = storage.samples(@klass) - @args.map{|m| decorator(m) } - .each{|d| punch(d) } - @log - end - - protected - - def decorator(model) - Factory.decorate(@klass, model) - end - - def sentries - fn = proc{|memo, model| - memo.concat( - model.params - .select(&:sentry?) - .map(&:sentry) - ) - } - @args.inject([], &fn).uniq - .map{|s| SentryModel.new(s)} - end - - def punch(model) - source = render(@source, model) - @log.concat storage.write(model.source, source) - requirerb(model) - test = render(@test, model) - @log.concat storage.write(model.test, test) - end - - def requirerb(model) - required = '' - required = storage.read(model.require) if storage.exist?(model.require) - requires = "require_relative \"#{model.require_string}\"" - return if required =~ %r{#{requires}} - storage.append(model.require, requires) - @log << model.require + '~' - end - - def render(erb, model) - @model = model - renderer = ERB.new(erb, trim_mode: '-') - renderer.result(binding) - end - end - - end -end diff --git a/lib/punch/services/report_status.rb b/lib/punch/services/report_status.rb index ee7c3a3..39dfc2a 100644 --- a/lib/punch/services/report_status.rb +++ b/lib/punch/services/report_status.rb @@ -10,7 +10,7 @@ class ReportStatus < Service include Punch::Hashed def call - look_through_folders(config.lib, config.test) + look_through_folders(conf.lib, conf.test) end protected diff --git a/lib/punch/services/service.rb b/lib/punch/services/service.rb index 0414964..b4fb0c6 100644 --- a/lib/punch/services/service.rb +++ b/lib/punch/services/service.rb @@ -1,5 +1,6 @@ require_relative "../basics" -require_relative "../config" +require_relative "../models" +require_relative "../decors" require_relative "../plugins" require "forwardable" @@ -9,16 +10,8 @@ module Services # Basic Service class class Service < Punch::Service extend Forwardable - def_delegator :PlayboxPlug, :object, :storage - def_delegator :Punch, :config, :config - - # def storage - # @storage ||= Home.new - # end - # - # def config - # @config ||= Punch.config - # end + def_delegator :PlayboxHolder, :object, :store + def_delegator :ConfigHolder, :object, :conf end end diff --git a/lib/punch/version.rb b/lib/punch/version.rb index 0c53b3c..5e1987d 100644 --- a/lib/punch/version.rb +++ b/lib/punch/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Punch - VERSION = "0.6.4" + VERSION = "0.7.0" end diff --git a/test/punch/basics/test_plugin.rb b/test/punch/basics/test_plugin.rb index d98dd4d..7d4b668 100644 --- a/test/punch/basics/test_plugin.rb +++ b/test/punch/basics/test_plugin.rb @@ -1,19 +1,19 @@ require_relative "../../test_helper" describe Plugin do - class Dummy + class PluginSample extend Punch::Plugin end let(:holder) { Module.new do extend Punch::Plugin::Holder - plugin Dummy + plugin PluginSample end } it '#plugin must return Pluging::Holder' do - assert_kind_of Punch::Plugin::Holder, Dummy.plugin + assert_kind_of Punch::Plugin::Holder, PluginSample.plugin end describe Plugin::Holder do @@ -23,7 +23,7 @@ class Dummy assert_respond_to holder, :object assert_respond_to holder, :object= - proper = Class.new(Dummy) + proper = Class.new(PluginSample) holder.object = proper.new assert_raises(ArgumentError) { holder.object = Object.new } diff --git a/test/punch/decors/test_model.rb b/test/punch/decors/test_model.rb new file mode 100644 index 0000000..a701741 --- /dev/null +++ b/test/punch/decors/test_model.rb @@ -0,0 +1,15 @@ +require_relative '../../test_helper' +require_relative '../dummy' + +class TestModelDecor < Minitest::Test + def subject = Decors::Model + + def test_dry_run + methods = %i[const namespace indentation open_namespace close_namespace regular_params keyword_params params_yardoc params_yarpro params] + + Dummy.models + .map{|e| subject.new(e, 'model') } + .each{|e|methods.each{|m| e.send(m) } + } + end +end diff --git a/test/punch/decors/test_param.rb b/test/punch/decors/test_param.rb new file mode 100644 index 0000000..96c4ff9 --- /dev/null +++ b/test/punch/decors/test_param.rb @@ -0,0 +1,13 @@ +require_relative '../../test_helper' +require_relative '../dummy' + +class TestParamDecor < Minitest::Test + def subject = Decors::Param + + def test_dry_run + methods = %i[sentry regular keyword yardoc yarpro guard] + Dummy.params.map{|e| subject.new(e)}.each{|e| + methods.each{|m| e.send(m) } + } + end +end diff --git a/test/punch/dsl/test_builder.rb b/test/punch/dsl/test_builder.rb deleted file mode 100644 index 7d087e5..0000000 --- a/test/punch/dsl/test_builder.rb +++ /dev/null @@ -1,65 +0,0 @@ -require_relative "../../test_helper" -include Punch::DSL - -describe Builder do - it 'must build domain' do - dom = Builder.build do - sentry :string, block: 'v.is_a?(String)' - sentry :header, block: 'v.is_a?(String) && !v.empty?' - - entity :user do - param :login, keyword: false - param :secret, keyword: false, default: 'pa$$w0rd' - end - - entity :credentials do - param :email - param :secret - end - - plugin :storage - - service :status - - service :authenticate - - actor :user do - service :sign_on do - param :email - param :secret - end - - service "Sign In" do - param :email - param :secret - end - end - - actor :admin do - service :query_users - - service :lock_user do - param :email - end - - service :unlock_user do - param :email - end - end - end - - assert_kind_of Domain, dom - - assert_kind_of Array, dom.sentries - assert_equal 2, dom.sentries.size - - assert_kind_of Array, dom.entities - assert_equal 2, dom.entities.size - - assert_kind_of Array, dom.services - assert_equal 7, dom.services.size - - assert_kind_of Array, dom.actors - assert_equal 2, dom.actors.size - end -end diff --git a/test/punch/dsl/test_domain.rb b/test/punch/dsl/test_domain.rb new file mode 100644 index 0000000..9b64255 --- /dev/null +++ b/test/punch/dsl/test_domain.rb @@ -0,0 +1,39 @@ +require_relative '../../test_helper' + +class TestDomainDSL < Minitest::Test + def subject + DSL::Domain + end + + def test_build + dom = subject.() do + name :spec + desc 'spec domain' + + entity :user do + param :email, :sentry => :string, default: 'user', desc: 'user' + param :secret, :sentry => :secret + end + + service :create_user do + param :email, :sentry => :string, default: 'user', desc: 'user' + param :secret, :sentry => :secret + end + + actor :user do + service :login do + param :email, :sentry => :string, default: 'user', desc: 'user' + param :secret, :sentry => :secret + end + end + + plugin :store + end + assert dom + refute_empty dom.entities + refute_empty dom.services + refute_empty dom.actors + refute_empty dom.actors.first.services + refute_empty dom.plugins + end +end diff --git a/test/punch/dummy.rb b/test/punch/dummy.rb new file mode 100644 index 0000000..a58a47b --- /dev/null +++ b/test/punch/dummy.rb @@ -0,0 +1,48 @@ +require_relative '../test_helper' +require 'punch/models' + +module Dummy + extend self + include Punch::Models + + ALPHABET = ?a.upto(?z).map{ _1 }.freeze + PHONETIC = %w[alpfa bravo charlie delta echo foxtrot golf hotel india juliet kilo lima mike november oscar papa quebec romeo sierra tango uniform victor whiskey xray yankee zulu].freeze + + # @return [Array] of all possible configurations + def params + sentries = [nil, :UUID] + defaults = [nil, 42 ] + document = ['', 'description'] + alpha = Array.new(ALPHABET) + sentries.product(defaults, document).map{|sentry, default, desc|\ + Param.new(name: alpha.shift, sentry: sentry, default: default, desc: desc) + } + end + + # @retrun [Array] of all possible configurations + def models + phonetic = Array.new(PHONETIC) + adesc = ['', 'description'] + aparam = [[], params] + adesc.product(aparam).map{|desc, params| + Model.new(name: phonetic.shift, params: params, desc: desc) + } + end + + def sentries + [ Sentry.new(name: 'foo'), + Sentry.new(name: 'bar') ] + end + + def plugins + [ Plugin.new(name: 'store'), + Plugin.new(name: 'broker', desc: 'Message Broker') ] + end + +end + +if __FILE__ == $0 + Dummy.params + Dummy.models + Dummy.plugins +end diff --git a/test/punch/test_model_builder.rb b/test/punch/models/test_build_model.rb similarity index 67% rename from test/punch/test_model_builder.rb rename to test/punch/models/test_build_model.rb index c08383a..b3cfa3e 100644 --- a/test/punch/test_model_builder.rb +++ b/test/punch/models/test_build_model.rb @@ -1,8 +1,10 @@ -require_relative "../test_helper" +require_relative "../../test_helper" + +class TestBuildModel < Minitest::Test + include Punch::Models -class TestModelBuilder < Minitest::Test def builder - ModelBuilder + BuildModel end def test_call @@ -16,16 +18,12 @@ def test_call param = m.params[0] assert_equal 'a', param.name - assert param.positional? - refute param.keyword? refute param.default? refute param.sentry? m = builder.('model', 'a:b "42"') param = m.params[0] assert_equal 'a', param.name - refute param.positional? - assert param.keyword? assert param.default? assert param.sentry? end diff --git a/test/punch/services/test_punch_domain.rb b/test/punch/services/test_punch_domain.rb index 1b5bc84..cec1e05 100644 --- a/test/punch/services/test_punch_domain.rb +++ b/test/punch/services/test_punch_domain.rb @@ -9,7 +9,7 @@ Sandbox.() { domain = build_sample_domain log = service.(domain) - refute log.empty? + refute_empty log # puts "-= Default Configuration =-" # payload = ["lib/config.rb"] # puts log @@ -17,17 +17,13 @@ } Sandbox.() { - @config = Punch::Config.new('lib', 'test', 'dom', 'sen', 'ent', 'ser', 'plg') + @config = Config.new(domain: 'dom') domain = build_sample_domain log = [] - Punch.stub :config, @config do + ConfigHolder.stub :object, @config do log = service.(domain) end - refute log.empty? - # puts "-= Custom Configuration =-" - # payload = ["lib/dom/config.rb", "lib/dom.rb"] - # puts log - # print_content(*payload) + refute_empty log } end end diff --git a/test/punch/services/test_punch_model.rb b/test/punch/services/test_punch_model.rb new file mode 100644 index 0000000..6ca136f --- /dev/null +++ b/test/punch/services/test_punch_model.rb @@ -0,0 +1,49 @@ +require_relative '../../test_helper' +require_relative '../dummy' + +class TestPunchModel < Minitest::Test + class Subject < Punch::Services::PunchModel + public_class_method :new + end + + def service + Services::PunchModel + end + + def test_filepaths + m = Dummy.models.first + s = Subject.new(m, :service, 'context') + assert_equal "lib/context/#{m.name}.rb", s.srcrb + assert_equal "lib/context.rb", s.reqrb + assert_equal "test/context/test_#{m.name}.rb", s.tstrb + + m = m.with(name: 'user create') + s = Subject.new(m, :service, 'context') + assert_equal "lib/context/user_create.rb", s.srcrb + assert_equal "lib/context.rb", s.reqrb + assert_equal "test/context/test_user_create.rb", s.tstrb + end + + def test_dry_run + Sandbox.() { + Dummy.models.each{|m| + service.(m, :entity, 'models') + } + } + end + + def test_call + model = Dummy.models.find{|e| e.params.size > 0} + Sandbox.() { + log = service.(model, :entity, 'models') + assert_equal 5, log.size # plus folders + log = service.(model, :entity, 'models') + assert_equal 5, log.size # plus backup + + log = service.(model, :service, 'services') + assert_equal 5, log.size + log = service.(model, :service, 'services') + assert_equal 5, log.size + } + end +end diff --git a/test/punch/services/test_punch_plugin.rb b/test/punch/services/test_punch_plugin.rb new file mode 100644 index 0000000..6d06781 --- /dev/null +++ b/test/punch/services/test_punch_plugin.rb @@ -0,0 +1,27 @@ +require_relative '../../test_helper' +require_relative '../dummy' + +class TestPunchPlugin < Minitest::Test + + def service + Services::PunchPlugin + end + + def test_dry_run + Sandbox.() { + Dummy.plugins.each{|m| + service.(m) + } + } + end + + def test_call + model = Dummy.models.last + Sandbox.() { + log = service.(model) + refute_empty log + # pp log, Dir.glob('*.*') + # log.each{|e| puts ?\n, ?\n, File.read(e) unless File.directory?(e) } + } + end +end diff --git a/test/punch/services/test_punch_sentries.rb b/test/punch/services/test_punch_sentries.rb new file mode 100644 index 0000000..808bd35 --- /dev/null +++ b/test/punch/services/test_punch_sentries.rb @@ -0,0 +1,28 @@ +require_relative '../../test_helper' +require_relative '../dummy' + +# module Punch +# module Services +# public :PunchSentries +# end +# end + +class TestPunchSentries < Minitest::Test + def subject + Services::PunchSentries + end + + def test_dry_run + Sandbox.() { + log = subject.(Dummy.sentries) + assert_equal 3, log.size + + log = subject.(Dummy.sentries) + assert_empty log # no changes + + payload = Dummy.sentries.push( Models::Sentry.new(name: 'SPEC') ) + log = subject.(payload) + assert_equal 2, log.size # new sentry and new test + } + end +end diff --git a/test/punch/services/test_punch_sentry.rb b/test/punch/services/test_punch_sentry.rb deleted file mode 100644 index cc0828e..0000000 --- a/test/punch/services/test_punch_sentry.rb +++ /dev/null @@ -1,84 +0,0 @@ -require_relative "../../test_helper" -include Punch::Services - -describe PunchSentry do - let(:service) { PunchSentry } - - it 'must punch fail for faulty arguments' do - assert_raises(ArgumentError) { service.() } - end - - let(:dummy) { SentryModel.new('dummy', block: 'v.is_a?(String)') } - let(:extra) { SentryModel.new('extra', block: 'v.is_a?(String)') } - - it 'must punch sentries.rb and test/test_dummy.rb' do - Sandbox.() { - refute File.exist?('lib/sentries.rb') - refute File.exist?('test/sentries/test_dummy.rb') - log = service.(dummy) - sample = ["lib/sentries.rb", "test/sentries/test_dummy.rb"] - assert_equal sample, log - assert File.exist?('lib/sentries.rb') - assert File.exist?('test/sentries/test_dummy.rb') - - log = service.(dummy) - assert_equal [], log # the sentry that already exist - } - end - - it 'must punch few sentries' do - Sandbox.() { - log = service.(dummy, extra) - sample = [ "lib/sentries.rb", - "test/sentries/test_dummy.rb", - "test/sentries/test_extra.rb"] - assert_equal sample, log - sample.each{|s| assert File.exist?(s)} - # puts File.read('lib/sentries.rb') - } - end - - it 'must punch nothing for declared sentries' do - Sandbox.() { - log = service.(dummy) - log = service.(dummy) - assert_equal [], log - } - end -end - -class Punch::Services::PunchSentry - public_class_method :new - public :storage, :punch, :render, :declared?, :declared - attr_accessor :source, :test -end - -class TestPunchSentry < Minitest::Test - def service - PunchSentry.new.tap{|s| - source, test = s.storage.samples(:sentry) - s.source = source - s.test = test - } - end - - def test_declared - Sandbox.() { - refute service.declared?('MustbeFaulty') - } - - Sandbox.() { - fewsentries = <<~EOF - MustbeInteger = Sentry.new( - MustbeAnother = Sentry.new( - EOF - Dir.mkdir('lib') unless Dir.exist?('lib') - File.write('lib/sentries.rb', fewsentries) - - declared = service.declared - assert service.declared?('MustbeInteger') - assert service.declared?('MustbeAnother') - assert_equal %w(MustbeInteger MustbeAnother), declared - } - end -end diff --git a/test/punch/services/test_punch_source.rb b/test/punch/services/test_punch_source.rb deleted file mode 100644 index f6bd4f7..0000000 --- a/test/punch/services/test_punch_source.rb +++ /dev/null @@ -1,40 +0,0 @@ -require_relative "../../test_helper" -include Punch::Services - -describe PunchSource do - let(:service) { PunchSource } - - let(:dummy) { ModelBuilder.('dummy', 'a', 'b:integer 42') } - let(:extra) { ModelBuilder.('extra', 'a', 'b:integer 42') } - - it 'must punch fail for faulty arguments' do - assert_raises(ArgumentError) { service.() } - assert_raises(ArgumentError) { service.(:unknown, dummy) } - end - - # @todo check result - it 'must punch a sentry and service' do - Sandbox.() { - service.(:service, dummy, extra) - assert File.exist?('lib/sentries.rb') - assert File.exist?('test/sentries/test_integer.rb') - assert File.exist?('lib/services.rb') - assert File.exist?('lib/services/dummy.rb') - assert File.exist?('lib/services/extra.rb') - assert File.exist?('test/services/test_dummy.rb') - assert File.exist?('test/services/test_extra.rb') - } - - Sandbox.() { - service.(:entity, dummy, extra) - assert File.exist?('lib/sentries.rb') - assert File.exist?('test/sentries/test_integer.rb') - assert File.exist?('lib/entities.rb') - assert File.exist?('lib/entities/dummy.rb') - assert File.exist?('lib/entities/extra.rb') - assert File.exist?('test/entities/test_dummy.rb') - assert File.exist?('test/entities/test_extra.rb') - } - end - -end diff --git a/test/punch/test_config.rb b/test/punch/test_config.rb index 35d3dd8..fe8675d 100644 --- a/test/punch/test_config.rb +++ b/test/punch/test_config.rb @@ -1,46 +1,41 @@ require_relative "../test_helper" -class TestConfig < Minitest::Test - def test_settings +describe Punch do + it { assert_equal Dir.pwd, Punch.root assert_equal File.join(Dir.pwd, 'lib', 'assets'), Punch.assets assert_equal File.join(Dir.pwd, 'lib', 'punch', 'basics'), Punch.basics assert_equal File.join(Punch.assets, 'starter'), Punch.starter assert_equal File.join(Punch.assets, 'samples'), Punch.samples assert_equal File.join(Punch.assets, 'domain'), Punch.domain - end + } +end - def test_config +describe Config do + it { Tempbox.() { - # not exist - conf = Punch.config - assert_respond_to conf, :lib - assert_respond_to conf, :test - assert_respond_to conf, :domain - assert_respond_to conf, :sentries - assert_respond_to conf, :services - assert_respond_to conf, :entities - assert_respond_to conf, :plugins - - # proper - File.write(Punch::CONFIG, <<~EOF + refute File.exist?(Punch::CONF) + conf = Config.read + assert File.exist?(Punch::CONF) + custom = <<~EOF lib: lib test: test - domain: proper_domain + domain: domain sentries: sentries services: services entities: entities plugins: plugins EOF - ) - conf = Punch.config - assert_kind_of Punch::Config, conf - assert_equal 'proper_domain', conf.domain + File.write(Punch::CONF, custom) + conf = Config.read + assert_equal 'domain', conf.domain # faulty - File.write(Punch::CONFIG, 'faulty maulty') - config = Punch.config - assert_kind_of Punch::Config, config + _, _ = capture_io do + File.write(Punch::CONF, 'faulty') + conf = Config.read + assert_empty conf.domain + end } - end + } end diff --git a/test/punch/test_decor.rb b/test/punch/test_decor.rb deleted file mode 100644 index a8f4545..0000000 --- a/test/punch/test_decor.rb +++ /dev/null @@ -1,84 +0,0 @@ -require_relative "../test_helper" - -class TestSentryDecor < Minitest::Test - def test_const - model = SentryModel.new('dummy') - decor = Factory.decorate(:sentry, model) - assert_equal 'MustbeDummy', decor.const - end -end - -class TestSourceDecor < Minitest::Test - - def model(name) - Model.new(name).tap{|m| - m.<< Param.new('para1') - } - end - - def decor(klass, name) - Factory.decorate(klass, model(name)) - end - - def test_config - appconf = Punch::Config.new('app', 'test', '', 'sen', 'ent', 'ser') - Punch.stub :config, appconf do - deco = decor(:service, 'get') - assert_equal "Get", deco.const - assert_equal 'Ser', deco.namespace - assert_equal 'app/ser/get.rb', deco.source - assert_equal 'app/ser.rb', deco.require - assert_equal 'ser/get', deco.require_string - assert_equal 'test/ser/test_get.rb', deco.test - assert_equal '../test_helper', deco.test_helper - - deco = decor(:service, 'user/get') - assert_equal "Get", deco.const - assert_equal 'Ser::User', deco.namespace - assert_equal 'app/ser/user/get.rb', deco.source - assert_equal 'app/ser/user.rb', deco.require - assert_equal 'user/get', deco.require_string - assert_equal 'test/ser/user/test_get.rb', deco.test - assert_equal '../../test_helper', deco.test_helper - end - end - - def test_domain_config - gemconf = Punch::Config.new('lib', 'test', 'dummy', 'sen', 'ent', 'ser') - Punch.stub :config, gemconf do - deco = decor(:service, 'get') - assert_equal 'Get', deco.const - assert_equal 'Dummy::Ser', deco.namespace - assert_equal 'lib/dummy/ser/get.rb', deco.source - assert_equal 'lib/dummy/ser.rb', deco.require - assert_equal 'ser/get', deco.require_string - assert_equal 'test/dummy/ser/test_get.rb', deco.test - assert_equal '../../test_helper', deco.test_helper - - deco = decor(:service, 'user/get') - assert_equal 'Get', deco.const - assert_equal 'Dummy::Ser::User', deco.namespace - assert_equal 'lib/dummy/ser/user/get.rb', deco.source - assert_equal 'lib/dummy/ser/user.rb', deco.require - assert_equal 'user/get', deco.require_string - assert_equal 'test/dummy/ser/user/test_get.rb', deco.test - assert_equal '../../../test_helper', deco.test_helper - end - end - - def test_properties - model = ModelBuilder.(:dummy, - 'a', 'b:', 'c nil', 'd:integer', 'e:integer 42') - decor = Factory.decorate(:entity, model) - decor.properties - decor.parameters - decor.assignment - decor.yardoc - - model = ModelBuilder.(:dummy) - decor = Factory.decorate(:entity, model) - # puts decor.properties - # puts decor.parameters - # puts decor.assignment - end -end diff --git a/test/punch_cli_test.rb b/test/punch_cli_test.rb index a6165a4..5e7e4dd 100644 --- a/test/punch_cli_test.rb +++ b/test/punch_cli_test.rb @@ -1,5 +1,7 @@ require_relative "test_helper" +LoggerHolder.object = Logger.new(IO::NULL) + class TestCLI < Minitest::Test def dummy diff --git a/test/punch_exe_test.rb b/test/punch_exe_test.rb index b2d4737..2682c63 100644 --- a/test/punch_exe_test.rb +++ b/test/punch_exe_test.rb @@ -43,7 +43,7 @@ it 'must punch domain by dogen' do bundled { system "punch domain" - system "ruby domain/dogen.rb" + system "ruby ./punch/domain/dogen.rb" } end diff --git a/test/test_helper.rb b/test/test_helper.rb index ce7afb7..cc57cb9 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -20,7 +20,7 @@ class Sandbox def self.call Dir.mktmpdir([TMPRX]) do |dir| Dir.chdir(dir) do - playbox = PlayboxPlug.object + playbox = PlayboxHolder.object playbox.punch_home(DUMMY) Dir.chdir(DUMMY){ yield } end