rider-kick 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +45 -8
- data/lib/generators/rider_kick/clean_arch_generator.rb +1 -0
- data/lib/generators/rider_kick/entity_type_mapping_spec.rb +52 -0
- data/lib/generators/rider_kick/repositories_contract_spec.rb +62 -0
- data/lib/generators/rider_kick/scaffold_generator_builder_uploaders_spec.rb +54 -0
- data/lib/generators/rider_kick/scaffold_generator_contracts_spec.rb +69 -0
- data/lib/generators/rider_kick/scaffold_generator_contracts_with_scope_spec.rb +54 -0
- data/lib/generators/rider_kick/scaffold_generator_idempotent_spec.rb +59 -0
- data/lib/generators/rider_kick/scaffold_generator_success_spec.rb +82 -0
- data/lib/generators/rider_kick/scaffold_generator_with_scope_spec.rb +55 -0
- data/lib/generators/rider_kick/structure_generator.rb +33 -0
- data/lib/generators/rider_kick/structure_generator_spec.rb +20 -0
- data/lib/generators/rider_kick/structure_generator_success_spec.rb +36 -0
- data/lib/generators/rider_kick/templates/db/structures/example.yaml.tt +114 -82
- data/lib/generators/rider_kick/templates/domains/core/repositories/abstract_repository.rb.tt +0 -12
- data/lib/generators/rider_kick/templates/domains/core/repositories/list.rb.tt +2 -2
- data/lib/generators/rider_kick/templates/domains/core/utils/abstract_utils.rb.tt +29 -0
- data/lib/generators/rider_kick/templates/domains/core/utils/request_methods.rb.tt +2 -1
- data/lib/rider-kick.rb +1 -0
- data/lib/rider_kick/entities/failure_details.rb +22 -14
- data/lib/rider_kick/entities/failure_details_spec.rb +22 -0
- data/lib/rider_kick/matchers/use_case_result_edge_spec.rb +28 -0
- data/lib/rider_kick/use_cases/abstract_use_case_spec.rb +57 -0
- data/lib/rider_kick/version.rb +1 -1
- metadata +223 -51
- data/.rspec +0 -3
- data/.rubocop.yml +0 -1141
- data/Rakefile +0 -12
- data/lib/rider_kick/builders/abstract_active_record_entity_builder_spec.rb +0 -116
- data/lib/rider_kick/matchers/use_case_result_spec.rb +0 -64
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
require 'tmpdir'
|
5
|
+
require 'generators/rider_kick/structure_generator'
|
6
|
+
|
7
|
+
RSpec.describe 'rider_kick:structure generator' do
|
8
|
+
let(:klass) { RiderKick::Structure }
|
9
|
+
|
10
|
+
it 'mengangkat Thor::Error jika app/domains belum ada' do
|
11
|
+
Dir.mktmpdir do |dir|
|
12
|
+
Dir.chdir(dir) do
|
13
|
+
expect(Dir.exist?('app/domains')).to be false
|
14
|
+
instance = klass.new(['Models::User']) # ← instansiasi dengan argumen
|
15
|
+
expect { instance.generate_use_case } # ← panggil task langsung
|
16
|
+
.to raise_error(Thor::Error, /clean_arch.*--setup/i)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
require 'tmpdir'
|
5
|
+
require 'active_support/inflector'
|
6
|
+
require 'generators/rider_kick/structure_generator'
|
7
|
+
require 'ostruct'
|
8
|
+
|
9
|
+
RSpec.describe 'rider_kick:structure generator (success)' do
|
10
|
+
let(:klass) { RiderKick::Structure }
|
11
|
+
|
12
|
+
it 'membuat db/structures/<resource>_structure.yaml ketika environment valid' do
|
13
|
+
Dir.mktmpdir do |dir|
|
14
|
+
Dir.chdir(dir) do
|
15
|
+
# 1) siapkan struktur minimal Clean Arch
|
16
|
+
FileUtils.mkdir_p('app/domains/core/use_cases')
|
17
|
+
FileUtils.mkdir_p('app/models/models')
|
18
|
+
|
19
|
+
# 2) stub namespace & model + metadata kolom
|
20
|
+
|
21
|
+
# 3) jalankan generator
|
22
|
+
instance = klass.new(['Models::User', 'actor:owner']) # ← pakai token
|
23
|
+
instance.generate_use_case
|
24
|
+
|
25
|
+
# 4) verifikasi file output
|
26
|
+
expect(File).to exist('db/structures/users_structure.yaml')
|
27
|
+
yaml = File.read('db/structures/users_structure.yaml')
|
28
|
+
expect(yaml).to include('model: Models::User')
|
29
|
+
expect(yaml).to include('resource_name: users')
|
30
|
+
expect(yaml).to include('actor: owner')
|
31
|
+
expect(yaml).to include('- name') # field dari kolom
|
32
|
+
expect(yaml).to include('- price')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -2,99 +2,131 @@ model: <%= @model_class %>
|
|
2
2
|
resource_name: <%= @scope_path %>
|
3
3
|
resource_owner_id: <%= @resource_owner_id %>
|
4
4
|
actor: <%= @actor %>
|
5
|
+
|
5
6
|
fields:
|
6
|
-
<% @
|
7
|
-
|
7
|
+
<% @fields.each do |f| -%>
|
8
|
+
- <%= f %>
|
8
9
|
<% end -%>
|
9
|
-
<% @uploaders.each do |
|
10
|
-
|
10
|
+
<% @uploaders.each do |f| -%>
|
11
|
+
- <%= f %>
|
11
12
|
<% end -%>
|
13
|
+
|
12
14
|
uploaders:
|
13
|
-
<% @uploaders.each do |
|
14
|
-
|
15
|
+
<% @uploaders.each do |f| -%>
|
16
|
+
- <%= f %>
|
15
17
|
<% end -%>
|
18
|
+
|
16
19
|
search_able:
|
17
|
-
<%
|
18
|
-
|
19
|
-
|
20
|
+
<% @search_able.each do |f| -%>
|
21
|
+
- <%= f %>
|
22
|
+
<% end -%>
|
23
|
+
|
24
|
+
# ---- Enriched metadata (opsional, untuk tooling/insight) ----
|
25
|
+
schema:
|
26
|
+
columns:
|
27
|
+
<% columns_meta.each do |c| -%>
|
28
|
+
- name: <%= c[:name] %>
|
29
|
+
type: <%= c[:type] %>
|
30
|
+
sql_type: <%= c[:sql_type] %>
|
31
|
+
null: <%= c[:null] %>
|
32
|
+
<% if c[:default].present? -%>
|
33
|
+
default: <%= c[:default].inspect %>
|
34
|
+
<% end -%>
|
35
|
+
<% if c[:precision] || c[:scale] -%>
|
36
|
+
precision: <%= c[:precision] || 'null' %>
|
37
|
+
scale: <%= c[:scale] || 'null' %>
|
38
|
+
<% end -%>
|
39
|
+
<% if c[:limit] -%>
|
40
|
+
limit: <%= c[:limit] %>
|
41
|
+
<% end -%>
|
42
|
+
<% end -%>
|
43
|
+
foreign_keys:
|
44
|
+
<% (fkeys_meta.presence || []).each do |fk| -%>
|
45
|
+
- column: <%= fk[:column] %>
|
46
|
+
to_table: <%= fk[:to_table] %>
|
47
|
+
<% end -%>
|
48
|
+
indexes:
|
49
|
+
<% (indexes_meta.presence || []).each do |ix| -%>
|
50
|
+
- columns: [<%= ix[:columns].join(', ') %>]
|
51
|
+
unique: <%= ix[:unique] %>
|
52
|
+
<% end -%>
|
53
|
+
enums:
|
54
|
+
<% if enums_meta.present? -%>
|
55
|
+
<% enums_meta.each do |name, map| -%>
|
56
|
+
<%= name %>: <%= map.keys %>
|
20
57
|
<% end -%>
|
58
|
+
<% else -%>
|
59
|
+
{}
|
21
60
|
<% end -%>
|
61
|
+
|
22
62
|
controllers:
|
23
|
-
|
24
|
-
<% @fields.each do |
|
25
|
-
|
26
|
-
<% end -%>
|
27
|
-
show_fields:
|
28
|
-
<% @model_class.columns.each do |field| -%>
|
29
|
-
- <%= field.name.to_s %>
|
30
|
-
<% end -%>
|
31
|
-
<% @uploaders.each do |field| -%>
|
32
|
-
- <%= field %>
|
33
|
-
<% end -%>
|
34
|
-
form_fields:
|
35
|
-
<% (@fields).each do |field| -%>
|
36
|
-
- name: <%= field %>
|
37
|
-
type: string
|
38
|
-
<% end -%>
|
39
|
-
<% (@uploaders).each do |field| -%>
|
40
|
-
<% if is_singular?(field) -%>
|
41
|
-
- name: <%= field %>
|
42
|
-
type: file
|
43
|
-
<% else -%>
|
44
|
-
- name: <%= field %>
|
45
|
-
type: files
|
63
|
+
list_fields:
|
64
|
+
<% @fields.each do |f| -%>
|
65
|
+
- <%= f %>
|
46
66
|
<% end -%>
|
67
|
+
show_fields:
|
68
|
+
<% (@columns.map { _1[:name] } + @uploaders).uniq.each do |f| -%>
|
69
|
+
- <%= f %>
|
47
70
|
<% end -%>
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
action_fetch_by_id:
|
53
|
-
use_case:
|
54
|
-
contract:
|
55
|
-
- required(:id).filled(:string)
|
56
|
-
action_create:
|
57
|
-
use_case:
|
58
|
-
contract:
|
59
|
-
<% (@fields + @uploaders).each do |field| -%>
|
60
|
-
<% column_type = get_column_type(field) -%>
|
61
|
-
<% dry_type = @type_mapping[column_type.to_s] || ':string' -%>
|
62
|
-
<% if @uploaders.include?(field) -%>
|
63
|
-
<% if is_singular?(field) -%>
|
64
|
-
- optional(:<%= field %>).maybe(<%= dry_type %>)
|
65
|
-
<% else -%>
|
66
|
-
- optional(:<%= field %>).maybe(:array)
|
71
|
+
form_fields:
|
72
|
+
<% @fields.each do |f| -%>
|
73
|
+
- name: <%= f %>
|
74
|
+
type: <%= get_column_type(f) %>
|
67
75
|
<% end -%>
|
68
|
-
<%
|
69
|
-
|
70
|
-
|
71
|
-
<% end -%>
|
72
|
-
action_update:
|
73
|
-
use_case:
|
74
|
-
contract:
|
75
|
-
- required(:id).filled(:string)
|
76
|
-
<% (@fields + @uploaders).each do |field| -%>
|
77
|
-
<% column_type = get_column_type(field) -%>
|
78
|
-
<% dry_type = @type_mapping[column_type.to_s] || ':string' -%>
|
79
|
-
<% if @uploaders.include?(field) -%>
|
80
|
-
<% if is_singular?(field) -%>
|
81
|
-
- optional(:<%= field %>).maybe(<%= dry_type %>)
|
82
|
-
<% else -%>
|
83
|
-
- optional(:<%= field %>).maybe(:array)
|
76
|
+
<% @uploaders.each do |f| -%>
|
77
|
+
- name: <%= f %>
|
78
|
+
type: <%= is_singular?(f) ? 'file' : 'files' %>
|
84
79
|
<% end -%>
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
80
|
+
|
81
|
+
domains:
|
82
|
+
action_list:
|
83
|
+
use_case:
|
84
|
+
contract:
|
85
|
+
<% @search_able.each do |f| -%>
|
86
|
+
# optional search fields: <%= f %>
|
87
|
+
<% end -%>
|
88
|
+
action_fetch_by_id:
|
89
|
+
use_case:
|
90
|
+
contract:
|
91
|
+
- required(:id).filled(:string)
|
92
|
+
<% if @resource_owner_id.present? -%>
|
93
|
+
- required(:<%= @resource_owner_id %>).filled(:string)
|
94
|
+
<% end -%>
|
95
|
+
|
96
|
+
action_create:
|
97
|
+
use_case:
|
98
|
+
contract:
|
99
|
+
<% if @resource_owner_id.present? -%>
|
100
|
+
- required(:<%= @resource_owner_id %>).filled(:string)
|
101
|
+
<% end -%>
|
102
|
+
<% contract_lines_for_create.each do |line| -%>
|
103
|
+
<%= line %>
|
104
|
+
<% end -%>
|
105
|
+
|
106
|
+
action_update:
|
107
|
+
use_case:
|
108
|
+
contract:
|
109
|
+
- required(:id).filled(:string)
|
110
|
+
<% if @resource_owner_id.present? -%>
|
111
|
+
- required(:<%= @resource_owner_id %>).filled(:string)
|
112
|
+
<% end -%>
|
113
|
+
<% contract_lines_for_update.each do |line| -%>
|
114
|
+
<%= line %>
|
115
|
+
<% end -%>
|
116
|
+
|
117
|
+
action_destroy:
|
118
|
+
use_case:
|
119
|
+
contract:
|
120
|
+
- required(:id).filled(:string)
|
121
|
+
<% if @resource_owner_id.present? -%>
|
122
|
+
- required(:<%= @resource_owner_id %>).filled(:string)
|
123
|
+
<% end -%>
|
124
|
+
|
93
125
|
entity:
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
<% if @
|
99
|
-
|
100
|
-
<% end -%>
|
126
|
+
skipped_fields:
|
127
|
+
- id
|
128
|
+
- created_at
|
129
|
+
- updated_at
|
130
|
+
<% if @columns.map { _1[:name] }.include?('type') -%>
|
131
|
+
- type
|
132
|
+
<% end -%>
|
data/lib/generators/rider_kick/templates/domains/core/repositories/abstract_repository.rb.tt
CHANGED
@@ -7,18 +7,6 @@ class Core::Repositories::AbstractRepository
|
|
7
7
|
extend(Core::Utils::RequestMethods)
|
8
8
|
end
|
9
9
|
|
10
|
-
def parse_response(response)
|
11
|
-
begin
|
12
|
-
res = JSON.parse(response.body)
|
13
|
-
rescue => e
|
14
|
-
return Failure e
|
15
|
-
end
|
16
|
-
unless ['200', '201', '202'].include?(response.code.to_s)
|
17
|
-
return Failure Hashie::Mash.new(res)
|
18
|
-
end
|
19
|
-
Success Hashie::Mash.new(res)
|
20
|
-
end
|
21
|
-
|
22
10
|
def error_messages_for(record)
|
23
11
|
record.errors.to_a.join(', ')
|
24
12
|
end
|
@@ -12,8 +12,8 @@ class Core::Repositories::<%= @scope_class %>::<%= @repository_class%> < Core::R
|
|
12
12
|
<% else -%>
|
13
13
|
resources = <%= @model_class %>
|
14
14
|
<% end -%>
|
15
|
-
if @search.present?
|
16
|
-
resources = resources.where(
|
15
|
+
if @search.present? && @search_able.any?
|
16
|
+
resources = resources.where("<%= @search_able.map { |f| "LOWER(#{f}) LIKE :search" }.join(' OR ') %>", search: "%#{@search.to_s.downcase}%")
|
17
17
|
end
|
18
18
|
return Success Hashie::Mash.new(response: [], meta: {}) unless resources.present?
|
19
19
|
pagy, results = pagy(resources.order(created_at: :desc), limit: @per_page, page: @page)
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Core::Utils::AbstractUtils
|
4
|
+
include Dry::Monads[:result, :do]
|
5
|
+
|
6
|
+
def http
|
7
|
+
extend(Core::Utils::RequestMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
def error_messages_for(record)
|
11
|
+
record.errors.to_a.join(', ')
|
12
|
+
end
|
13
|
+
|
14
|
+
def build_errors(resource)
|
15
|
+
errors = []
|
16
|
+
resource.errors.each do |error|
|
17
|
+
errors << Core::Builders::Error.new(error.as_json).build
|
18
|
+
end
|
19
|
+
errors
|
20
|
+
end
|
21
|
+
|
22
|
+
def prepare!(params, sanitize: true)
|
23
|
+
if sanitize
|
24
|
+
Hashie::Mash.new(params.reject { |_, v| v.nil? || (v.is_a?(String) && v.blank?) })
|
25
|
+
else
|
26
|
+
Hashie::Mash.new(params)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/rider-kick.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
# typed: false
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
require 'rider_kick/types'
|
@@ -7,23 +6,32 @@ require 'dry/struct'
|
|
7
6
|
module RiderKick
|
8
7
|
module Entities
|
9
8
|
class FailureDetails < Dry::Struct
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
attribute :
|
18
|
-
attribute :message, Types::Strict::String
|
9
|
+
# enum + default HARUS: default dulu, baru enum (sesuai dry-types)
|
10
|
+
TYPE = Types::Coercible::Symbol
|
11
|
+
.default(:error)
|
12
|
+
.enum(:error, :expectation_failed, :not_found, :unauthorized, :unprocessable_entity)
|
13
|
+
private_constant :TYPE
|
14
|
+
|
15
|
+
attribute :type, TYPE
|
16
|
+
attribute :message, Types::Strict::String
|
19
17
|
attribute :other_properties, Types::Strict::Hash.default({}.freeze)
|
20
18
|
|
21
|
-
|
22
|
-
|
19
|
+
# Kumpulkan array pesan jadi satu kalimat, type default :error
|
20
|
+
def self.from_array(array, type: :error, **extras)
|
21
|
+
new(
|
22
|
+
type: type,
|
23
|
+
message: Array(array).map!(&:to_s).join(', '),
|
24
|
+
other_properties: extras
|
25
|
+
)
|
23
26
|
end
|
24
27
|
|
25
|
-
|
26
|
-
|
28
|
+
# Bungkus string jadi FailureDetails, type default :error
|
29
|
+
def self.from_string(string, type: :error, **extras)
|
30
|
+
new(
|
31
|
+
type: type,
|
32
|
+
message: string.to_s,
|
33
|
+
other_properties: extras
|
34
|
+
)
|
27
35
|
end
|
28
36
|
end
|
29
37
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe RiderKick::Entities::FailureDetails do
|
4
|
+
it 'from_array menggabungkan pesan secara dinamis' do
|
5
|
+
fd = described_class.from_array(%w[alpha beta gamma])
|
6
|
+
expect(fd.message).to eq('alpha, beta, gamma')
|
7
|
+
expect(fd.type).to eq(:error)
|
8
|
+
expect(fd.other_properties).to eq({})
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'from_string membungkus string menjadi FailureDetails' do
|
12
|
+
fd = described_class.from_string('oops')
|
13
|
+
expect(fd.message).to eq('oops')
|
14
|
+
expect(fd.type).to eq(:error)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'bisa dibuat dengan default type error' do
|
18
|
+
fd = described_class.new(message: 'X') # sengaja tanpa :type
|
19
|
+
expect(fd.type).to eq(:error)
|
20
|
+
expect(fd.other_properties).to eq({})
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rider_kick/matchers/use_case_result'
|
4
|
+
require 'dry/monads'
|
5
|
+
|
6
|
+
RSpec.describe RiderKick::Matchers::UseCaseResult do
|
7
|
+
subject(:matcher) { described_class }
|
8
|
+
|
9
|
+
it 'melempar error untuk Failure value yang tidak didukung' do
|
10
|
+
weird = Object.new
|
11
|
+
expect {
|
12
|
+
matcher.call(Dry::Monads::Failure(weird)) { |m| m.failure { |_| :ok } }
|
13
|
+
}.to raise_error(ArgumentError, /Unexpected failure value/)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'meneruskan Entities::FailureDetails apa adanya' do
|
17
|
+
fd = RiderKick::Entities::FailureDetails.new(message: 'boom')
|
18
|
+
out = nil
|
19
|
+
|
20
|
+
RiderKick::Matchers::UseCaseResult.call(Dry::Monads::Failure(fd)) do |m|
|
21
|
+
m.success { |_| raise 'unexpected success' } # <- tambahkan handler success
|
22
|
+
m.failure { |v| out = v }
|
23
|
+
end
|
24
|
+
|
25
|
+
expect(out).to be_a(RiderKick::Entities::FailureDetails)
|
26
|
+
expect(out.message).to eq('boom')
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/validation'
|
4
|
+
require 'rider_kick/use_cases/abstract_use_case'
|
5
|
+
|
6
|
+
RSpec.describe RiderKick::UseCases::AbstractUseCase do
|
7
|
+
# Use case dummy untuk menguji .contract, .contract! dan build_parameter!
|
8
|
+
class DummyUseCase < RiderKick::UseCases::AbstractUseCase
|
9
|
+
contract do
|
10
|
+
params do
|
11
|
+
required(:name).filled(:string)
|
12
|
+
optional(:age).maybe(:integer)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
include Dry::Monads::Do.for(:result)
|
17
|
+
def result
|
18
|
+
params = yield build_parameter!
|
19
|
+
Success(params)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '.contract/.contract!' do
|
24
|
+
it 'membangun dan menjalankan contract' do
|
25
|
+
contract = DummyUseCase.contract!(name: 'Kotaro')
|
26
|
+
expect(contract).to be_success
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#build_parameter!' do
|
31
|
+
it 'mengembalikan Success(Hashie::Mash) ketika valid' do
|
32
|
+
contract = DummyUseCase.contract!(name: 'Kotaro', age: 7)
|
33
|
+
use_case = DummyUseCase.new(contract)
|
34
|
+
result = use_case.build_parameter!
|
35
|
+
expect(result).to be_a(Dry::Monads::Success)
|
36
|
+
expect(result.value!).to respond_to(:name)
|
37
|
+
expect(result.value!.name).to eq('Kotaro')
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'mengembalikan Failure(hash error) ketika tidak valid' do
|
41
|
+
contract = DummyUseCase.contract!(age: 'tujuh')
|
42
|
+
result = DummyUseCase.new(contract).build_parameter!
|
43
|
+
expect(result).to be_a(Dry::Monads::Failure)
|
44
|
+
expect(result.failure).to be_a(Hash)
|
45
|
+
expect(result.failure.keys).to include(:name) # name wajib
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#result' do
|
50
|
+
it 'menggunakan Do-notation dengan mulus' do
|
51
|
+
contract = DummyUseCase.contract!(name: 'Alam')
|
52
|
+
res = DummyUseCase.new(contract).result
|
53
|
+
expect(res).to be_success
|
54
|
+
expect(res.value!.name).to eq('Alam')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|