lieutenant 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4b4bd4305e270917371083e9ad6138b7a45f6473
4
- data.tar.gz: 5bd3c11a3945578b07e45226dc95efdd4388ba3a
3
+ metadata.gz: c13d7331d61d26a8ec7a9c816a25b959d2d94466
4
+ data.tar.gz: f26f9cf92f4a8064078859bd488a5cb15eb3954b
5
5
  SHA512:
6
- metadata.gz: 11e225acbc456c2e8093eb467a1537df64c98400686d295a1ecfbd1c96f2d7463f896cc1c2363d38b8d37700a30db74f7cb731c4b967d11562a1ad0e461b9f30
7
- data.tar.gz: 95136b8ad7cd14e41d4bc2eb7b2a3c8e4a76f2fad74b684574ef76d63e962422585377d2b044eb6ee1d7ec813269c5970c6ba4888ad57cf57276ffea4704e467
6
+ metadata.gz: 7f85b854951dd7e9556186ba9a173331cbd7cef3dd45e23e4a8e52a27508ac2d2a78a67cb2279dee7fe270fd85bb552d621d4c0d3c174b5fb6f90ec901ce22fd
7
+ data.tar.gz: 1ca3adbc3fb5fdc021b00f2621d1b65b722f724f7fa7dd554a650a0cef32167a64e48d1a54adddcdb6bc26d795ffa62a506d5e920de50cd7b22e20eb3b9f3b2e
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.4
2
+ TargetRubyVersion: 2.1
3
3
 
4
4
  Metrics/LineLength:
5
5
  Max: 120
@@ -1,5 +1,11 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
+ - 2.1
5
+ - 2.2
6
+ - 2.3.0
4
7
  - 2.4.1
5
- before_install: gem install bundler -v 1.16.0.pre.2
8
+ - ruby-head
9
+ before_install: gem install bundler -v 1.16.0
10
+ script:
11
+ - bundle exec rake fulltest
data/README.md CHANGED
@@ -2,9 +2,15 @@
2
2
 
3
3
  ## **CQRS/ES Toolkit to command them all**
4
4
 
5
+ [![Gem Version](https://badge.fury.io/rb/lieutenant.svg)](https://badge.fury.io/rb/lieutenant)
6
+ [![Build Status](https://travis-ci.org/gabteles/lieutenant.svg?branch=master)](https://travis-ci.org/gabteles/lieutenant)
7
+ [![Coverage Status](https://coveralls.io/repos/github/gabteles/lieutenant/badge.svg?branch=master)](https://coveralls.io/github/gabteles/lieutenant?branch=master)
8
+ [![Maintainability](https://api.codeclimate.com/v1/badges/c96a6dd822547e657829/maintainability)](https://codeclimate.com/github/gabteles/lieutenant/maintainability)
9
+
10
+
5
11
  Lieutenant is a toolkit that implements various of the components of Command & Query Responsability Segregation (CQRS) and Event Sourcing (ES). It means that your application can get rid of the "current" state of the entities you choose and store all the *changes* that led them to it.
6
12
 
7
- This gem aims to be most independent as possible of your tecnological choices, it means that it should work with Rails, Sinatra, pure Rack apps or whatever you want.
13
+ This gem aims to be most independent as possible of your tecnological choices: it should work with Rails, Sinatra, pure Rack apps or whatever you want.
8
14
 
9
15
  If you are not familiarized, you may check this references:
10
16
 
@@ -37,63 +43,307 @@ By now, Lieutenant offer the components listed below. With each one, there's a d
37
43
  - [Commands](#commands)
38
44
  - [Command Sender](#command-sender)
39
45
  - [Command Handlers](#command-handlers)
40
- - [Aggregate Repositories](#aggregate-repositories)
46
+ - [Aggregate Repository](#aggregate-repository)
41
47
  - [Aggregates](#aggregates)
42
48
  - [Events](#events)
43
49
  - [Event Store](#event-store)
44
50
  - [Event Bus](#event-bus)
51
+ - [Projections](#projections)
52
+ - [Configuration](#configuration)
45
53
 
46
54
  ### Commands
47
55
 
48
- TODO
56
+ Commands are the representation of the system actions. They describe a **intention** to do something (e.g. `ScheduleMeeting`, `DisableProduct`).
57
+
58
+ This classes do not need any special methods, just define attributes and validations.
49
59
 
60
+ To use define them, just include `Lieutenant::Command` module. It'll allow you to use [ActiveModel Validations](http://guides.rubyonrails.org/active_record_validations.html).
61
+
62
+ ```ruby
63
+ class ScheduleMeeting
64
+ include Lieutenant::Command
65
+
66
+ attr_accessor :meeting_room_uuid
67
+ attr_accessor :description
68
+ attr_accessor :date_start
69
+ attr_accessor :date_end
70
+
71
+ validates :meeting_room_uuid, presence: true
72
+ validates :description, presence: true, length: { minimum: 3 }
73
+ validates :date_start, presence: true
74
+ validates :date_end, presence: true
75
+
76
+ validate do
77
+ date_start.is_a?(Time) && date_end.is_a?(Time) && date_start < date_end
78
+ end
79
+ end
80
+ ```
81
+
82
+ To instantiate commands you can use `.new` or helper method `.with`, that receives the parameters as a Hash:
83
+
84
+ ```ruby
85
+ ScheduleMeeting.with(
86
+ meeting_room_uuid: '4bb0a8a0-9234-477d-8df4-5f10a2fb1faa',
87
+ description: 'Annual planning',
88
+ date_start: Time.mktime(2017, 12, 15, 14, 0, 0),
89
+ date_end: Time.mktime(2017, 12, 15, 18, 0, 0)
90
+ )
91
+ ```
50
92
 
51
93
  ### Command Sender
52
94
 
53
- TODO
95
+ Command sender is the component that receives commands and forward them to the right handlers. It also instantiate the aggregate repository's unit of work, in order to help persistence to save only the generated events in each command handling.
96
+
97
+ You can access the command sender throught Lieutenant's config:
98
+
99
+ ```ruby
100
+ Lieutenant.config.command_sender
101
+ ```
102
+
103
+ It dependes on all the configuration components, so be sure to config them before calling it. See [Configuration](#configuration).
104
+
105
+ Once with the Command Sender, dispatch events by using `#dispatch` (aliased as `#call`):
106
+
107
+ ```ruby
108
+ Lieutenant.config.command_sender.dispatch(command)
109
+ ```
54
110
 
55
111
 
56
112
  ### Command Handlers
57
113
 
58
- TODO
114
+ Command Handlers are orchestrators to your business logic. They will receive a command and a aggregate repository then will call the needed operations, they can load a aggregate by the identifier or add a new one to the repository.
59
115
 
116
+ Handlers are simply objects that respond to `#call`. You can define them as [Proc's](https://ruby-doc.org/core-2.5.0/Proc.html), for example.
60
117
 
61
- ### Aggregate Repositories
118
+ Lieutenant also defines a syntax sugar to help definition of them:
62
119
 
63
- TODO
120
+ ```ruby
121
+ module ScheduleHandler
122
+ include Lieutenant::CommandHandler
123
+
124
+ on(ScheduleMeeting) do |repository, command|
125
+ # ...
126
+ end
127
+ end
128
+ ```
129
+
130
+ You can also register them directly on command sender:
131
+
132
+ ```ruby
133
+ Lieutenant.config.command_sender.register(ScheduleMeeting) do |repository, command|
134
+ # ...
135
+ end
136
+ ```
137
+
138
+ It's important that command handlers do not have side-effects, since the commands **can** be retried (and eventually they will). If you, for example, send an email inside your handler, it may be sent twice in case of command retry.
139
+
140
+ ### Aggregate Repository
141
+
142
+ The aggregate repository is responsible to control the changes in the application state. It means that it will collect events from created or modified aggregates.
143
+
144
+ It also implements the [Unit of Work Pattern](https://martinfowler.com/eaaCatalog/unitOfWork.html) for each dispatched command, meaning that it will know what new events where created when processing the command.
145
+
146
+ You'll interact only with the Repository Unit of Work, that is the `repository` parameter that command handlers receive. It allows you to:
147
+
148
+ #### Add an aggregate:
149
+ ```ruby
150
+ aggregate = MeetingRoom.new
151
+ repository.add_aggregate(aggregate)
152
+ ```
153
+
154
+ #### Load an aggregate:
155
+ ```ruby
156
+ meeting_room = repository.load(MeetingRoom, command.meeting_room_uuid)
157
+ ```
64
158
 
65
159
 
66
160
  ### Aggregates
67
161
 
68
- TODO
162
+ Aggregates contain your business logic, rules between multiple entities are kept by them. Aggregates are all about the transaction consistency.
163
+
164
+ To define them, include `Lieutenant::Aggregate` into your class. When defining it's initializer, you'll need to also setup the instance, calling `#setup(id)`, where `id` is the identifier of the aggregates' instance (`SecureRandom.uuid` is encouraged).
69
165
 
166
+ ```ruby
167
+ class MeetingRoom
168
+ include Lieutenant::Aggregate
169
+
170
+ def initialize(name)
171
+ setup(SecureRandom.uuid)
172
+ end
173
+ end
174
+ ```
175
+
176
+ Aggregates' state should only be modified by events, that can be applied using `#apply`, that will instantiate the event with provided params and fire them to aggregate's internal handlers (registered with `.on`):
177
+
178
+ ```ruby
179
+ class MeetingRoom
180
+ include Lieutenant::Aggregate
181
+
182
+ def initialize(name)
183
+ setup(SecureRandom.uuid)
184
+ apply(MeetingRoomCreated, name: name)
185
+ end
186
+
187
+ on(MeetingRoomCreated) do |event|
188
+ @name = event.name
189
+ end
190
+ end
191
+ ```
192
+
193
+ To allow command handlers to modify aggregates, you can define handlers that also handles your business logic or throw errors:
194
+
195
+ ```ruby
196
+ class MeetingRoom
197
+ include Lieutenant::Aggregate
198
+
199
+ def initialize(name)
200
+ setup(SecureRandom.uuid)
201
+ apply(MeetingRoomCreated, name: name)
202
+ end
203
+
204
+ def schedule_meeting(description, date_start, date_end)
205
+ # Check if meeting room is available to needed dates
206
+
207
+ raise(MeetingRoomNotAvailable) unless room_available
208
+
209
+ apply(
210
+ MeetingScheduled,
211
+ description: description,
212
+ date_start: date_start,
213
+ date_end: date_end
214
+ )
215
+ end
216
+
217
+ on(MeetingRoomCreated) do |event|
218
+ @name = event.name
219
+ @meetings = []
220
+ end
221
+
222
+ on(MeetingScheduled) do |event|
223
+ # Note that we could push a PORO instead of a Hash
224
+ # (and it wouldn't be a Lieutenant::Aggregate)
225
+ @meetings.push({
226
+ description: event.description,
227
+ date_start: event.date_start,
228
+ date_end: event.date_end
229
+ })
230
+ end
231
+ end
232
+ ```
233
+
234
+ For the same reason of the command handlers, aggregates should not have side-effects inside them.
70
235
 
71
236
  ### Events
72
237
 
73
- TODO
238
+ Events register what happened with aggregates since they were created. They have same features as `Commands`: you can use ActiveModel Validations and instantiate them using `#with` method.
239
+
240
+ Events exposes `aggregate_id` and `sequence_number`, that are used to know to which aggregate each event belongs to and it's order into the event stream. You should not worry about them, we use them internally ;)
74
241
 
242
+ ```ruby
243
+ class MeetingScheduled
244
+ include Lieutenant::Event
245
+
246
+ attr_accessor :description
247
+ attr_accessor :date_start
248
+ attr_accessor :date_end
249
+ # Implicity defined:
250
+ # attr_accessor :aggregate_id (Meeting room's UUID)
251
+ # attr_accessor :sequence_number
252
+ end
253
+ ```
75
254
 
76
255
  ### Event Store
77
256
 
78
- TODO
257
+ Event stores handles pushing and pulling events to/from the persistence. They are used by the Aggregate Repository to commit changes collected by one unit of work.
258
+
259
+ You need to set what implementation will be used, them Lieutenant will do the magic. Please refer to [Configuration](#configuration).
79
260
 
80
261
 
81
262
  ### Event Bus
82
263
 
83
- TODO
264
+ The event bus publishes and receives messages from the aggregates updates.
265
+
266
+ You can also listen to it's events by subscribing to them:
267
+
268
+ ```ruby
269
+ Lieutenant.config.event_bus.subscribe(MeetingScheduled) do |event|
270
+ puts "Meeting scheduled on room #{event.aggregate_id}, starts at #{event.date_start.iso8601}, ends at #{event.date_end.iso8601}"
271
+ end
272
+ ```
273
+
274
+
275
+ ### Projections
276
+
277
+ The Projections listens to events it is interested in and updates read models
278
+ as needed. It means that they maintain the *current* state of the data. To use
279
+ it, just include `Lieutenant::Projection`.
84
280
 
281
+ ```ruby
282
+ module MeetingRoomProjection
283
+ include Lieutenant::Projection
284
+
285
+ on(MeetingRoomCreated) do |event|
286
+ MeetingRoomRecord.create!(
287
+ uuid: event.aggregate_id,
288
+ name: event.name,
289
+ meetings: []
290
+ )
291
+ end
292
+
293
+ on(MeetingScheduled) do |event|
294
+ meeting_room = MeetingRoomRecord.find(event.aggregate_id)
295
+ meeting_room.meetings.push({
296
+ description: event.description,
297
+ date_start: event.date_start,
298
+ date_end: event.date_end
299
+ })
300
+ meeting_room.save!
301
+ end
302
+ end
303
+ ```
304
+
305
+
306
+ ### Configuration
307
+
308
+ Lieutenant's configuration can be modified by using an structured or block way. By default, it uses InMemory implementation of event store.
309
+
310
+ ```ruby
311
+ Lieutenant.config do |configuration|
312
+ configuration.event_store(Lieutenant::EventStore::InMemory)
313
+ end
314
+
315
+ # OR
316
+
317
+ Lieutenant.config.event_store(Lieutenant::EventStore::InMemory)
318
+ ```
319
+
320
+ You can also access configuration the same way:
321
+
322
+ ```ruby
323
+ Lieutenant.config do |configuration|
324
+ configuration.event_bus # => Lieutenant::EventBus
325
+ configuration.event_store # => Lieutenant::EventStore::InMemory
326
+ configuration.aggregate_repository # => Lieutentant::AggregateRepository
327
+ configuration.command_sender # => Lieutenant::CommandSender
328
+ end
329
+
330
+ # OR
331
+
332
+ Lieutenant.config.event_bus # => Lieutenant::EventBus
333
+ Lieutenant.config.event_store # => Lieutenant::EventStore::InMemory
334
+ Lieutenant.config.aggregate_repository # => Lieutentant::AggregateRepository
335
+ Lieutenant.config.command_sender # => Lieutenant::CommandSender
336
+ ```
85
337
 
86
338
  ## Roadmap
87
339
 
88
340
  In order to give some directions to the development of this gem, the roadmap below presents in a large picture of the plans to the future (more or less ordered).
89
341
 
90
- - Projections
91
- - Better documentation
92
- - Command filters
93
342
  - Command retry policies
94
- - Sagas
95
343
  - More implementations of event store
96
- - More implementations of event bus
344
+ - Sagas
345
+ - Command filters
346
+ - Better documentation
97
347
 
98
348
  ## Development
99
349
 
@@ -16,6 +16,7 @@ module Lieutenant
16
16
  autoload :EventStore, 'lieutenant/event_store'
17
17
  autoload :Exception, 'lieutenant/exception'
18
18
  autoload :Message, 'lieutenant/message'
19
+ autoload :Projection, 'lieutenant/projection'
19
20
  autoload :VERSION, 'lieutenant/version'
20
21
 
21
22
  module_function
@@ -4,7 +4,9 @@ module Lieutenant
4
4
  # Syntax helper to define commands
5
5
  module Command
6
6
  def self.included(base)
7
- base.include(Message)
7
+ base.instance_eval do
8
+ include Lieutenant::Message
9
+ end
8
10
  end
9
11
  end
10
12
  end
@@ -3,15 +3,14 @@
3
3
  module Lieutenant
4
4
  # Manages configuration
5
5
  class Config
6
- # :reek:BooleanParameter
7
- def event_bus(implementation = false)
8
- return @event_bus = implementation if implementation
9
- @event_bus ||= EventBus::InMemory.new
6
+ def event_bus
7
+ @event_bus ||= EventBus.new
10
8
  end
11
9
 
12
10
  # :reek:BooleanParameter
13
11
  def event_store(implementation = false)
14
12
  return @event_store_implementation = implementation if implementation
13
+ @event_store_implementation ||= EventStore::InMemory
15
14
  @event_store ||= EventStore.new(@event_store_implementation, event_bus)
16
15
  end
17
16
 
@@ -4,7 +4,9 @@ module Lieutenant
4
4
  # The basic interface to register the aggregates events
5
5
  module Event
6
6
  def self.included(base)
7
- base.include(Message)
7
+ base.instance_eval do
8
+ include Lieutenant::Message
9
+ end
8
10
  end
9
11
 
10
12
  attr_reader :aggregate_id, :sequence_number
@@ -2,7 +2,23 @@
2
2
 
3
3
  module Lieutenant
4
4
  # Publishes and receives messages from the aggregates updates
5
- module EventBus
6
- autoload :InMemory, 'lieutenant/event_bus/in_memory'
5
+ class EventBus
6
+ def initialize
7
+ @handlers = Hash.new { [] }
8
+ end
9
+
10
+ def subscribe(*event_classes, &handler)
11
+ event_classes.each do |event_class|
12
+ handlers[event_class] = handlers[event_class].push(handler)
13
+ end
14
+ end
15
+
16
+ def publish(event)
17
+ handlers[event.class].each { |handler| handler.call(event) }
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :handlers
7
23
  end
8
24
  end
@@ -5,22 +5,27 @@ module Lieutenant
5
5
  # Memory implementation of the event store. Stores events while the application is running
6
6
  class InMemory
7
7
  def initialize
8
- @store = {}
8
+ @store = []
9
+ @index = {}
9
10
  end
10
11
 
11
12
  def persist(events)
12
- events.each { |event| (store[event.aggregate_id] ||= []).push(event) }
13
+ events.each do |event|
14
+ (index[event.aggregate_id] ||= []).push(store.size)
15
+ store.push(event)
16
+ end
13
17
  end
14
18
 
15
19
  def event_stream_for(aggregate_id)
16
- events = store[aggregate_id]
17
- return nil unless events
20
+ aggregate_stream = index[aggregate_id]
21
+ return nil unless aggregate_stream
22
+ events = aggregate_stream.lazy.map(&store.method(:[]))
18
23
  Enumerator.new { |yielder| events.each(&yielder.method(:<<)) }
19
24
  end
20
25
 
21
26
  def aggregate_sequence_number(aggregate_id)
22
- return -1 unless store.key?(aggregate_id)
23
- store[aggregate_id].last.sequence_number
27
+ return -1 unless index.key?(aggregate_id)
28
+ store[index[aggregate_id].last].sequence_number
24
29
  end
25
30
 
26
31
  def transaction
@@ -30,7 +35,7 @@ module Lieutenant
30
35
 
31
36
  private
32
37
 
33
- attr_reader :store
38
+ attr_reader :store, :index
34
39
  end
35
40
  end
36
41
  end
@@ -4,8 +4,10 @@ module Lieutenant
4
4
  # Helper to define messages with validation
5
5
  module Message
6
6
  def self.included(base)
7
- base.extend(ClassMethods)
8
- base.include(ActiveModel::Validations)
7
+ base.instance_eval do
8
+ extend Lieutenant::Message::ClassMethods
9
+ include ActiveModel::Validations
10
+ end
9
11
  end
10
12
 
11
13
  # Define common class methods to commands
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lieutenant
4
+ # Projection helper. Allows clean syntax to subscribe to events:
5
+ #
6
+ # module FooProjection
7
+ # include Lieutenant::Projection
8
+ #
9
+ # on(CreatedBarEvent) do |event|
10
+ # # ...
11
+ # end
12
+ # end
13
+ module Projection
14
+ def self.included(base)
15
+ base.class_eval do
16
+ extend Lieutenant::Projection
17
+ end
18
+ end
19
+
20
+ # :reek:UtilityFunction
21
+ def on(*event_classes, &block)
22
+ Lieutenant.config.event_bus.subscribe(*event_classes, &block)
23
+ end
24
+ end
25
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lieutenant
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'.freeze
5
5
  end
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.require_paths = ['lib']
23
23
 
24
24
  spec.add_development_dependency 'bundler'
25
+ spec.add_development_dependency 'coveralls'
25
26
  spec.add_development_dependency 'pry'
26
27
  spec.add_development_dependency 'rake'
27
28
  spec.add_development_dependency 'reek'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lieutenant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriel Teles
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-12-03 00:00:00.000000000 Z
11
+ date: 2018-02-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: coveralls
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: pry
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -161,7 +175,6 @@ files:
161
175
  - lib/lieutenant/config.rb
162
176
  - lib/lieutenant/event.rb
163
177
  - lib/lieutenant/event_bus.rb
164
- - lib/lieutenant/event_bus/in_memory.rb
165
178
  - lib/lieutenant/event_store.rb
166
179
  - lib/lieutenant/event_store/in_memory.rb
167
180
  - lib/lieutenant/exception.rb
@@ -169,6 +182,7 @@ files:
169
182
  - lib/lieutenant/exception/concurrency_conflict.rb
170
183
  - lib/lieutenant/exception/no_registered_handler.rb
171
184
  - lib/lieutenant/message.rb
185
+ - lib/lieutenant/projection.rb
172
186
  - lib/lieutenant/version.rb
173
187
  - lieutenant.gemspec
174
188
  homepage: https://github.com/gabteles/lieutenant
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lieutenant
4
- module EventBus
5
- # Memory implementation of the event bus. Publishes and notifies on the same memory space.
6
- class InMemory
7
- def initialize
8
- @handlers = Hash.new { [] }
9
- end
10
-
11
- def subscribe(*event_classes, &handler)
12
- event_classes.each do |event_class|
13
- handlers[event_class] = handlers[event_class].push(handler)
14
- end
15
- end
16
-
17
- def publish(event)
18
- block = CALL_HANDLER_WITH_EVENT[event]
19
- handlers[:all].each(&block)
20
- handlers[event.class].each(&block)
21
- end
22
-
23
- private
24
-
25
- attr_reader :handlers
26
-
27
- CALL_HANDLER_WITH_EVENT = ->(event) { ->(handler) { handler.call(event) } }
28
- private_constant :CALL_HANDLER_WITH_EVENT
29
- end
30
- end
31
- end