exel 1.2.1 → 1.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.codeclimate.yml +4 -4
- data/.gitignore +1 -2
- data/.rubocop.yml +23 -14
- data/.rubocop_airbnb.yml +2 -0
- data/.rubocop_todo.yml +1 -13
- data/.travis.yml +26 -0
- data/Gemfile +2 -2
- data/Gemfile.lock +118 -0
- data/Guardfile +1 -0
- data/README.md +96 -31
- data/Rakefile +2 -0
- data/exel.gemspec +7 -7
- data/lib/exel.rb +7 -1
- data/lib/exel/ast_node.rb +6 -10
- data/lib/exel/context.rb +4 -1
- data/lib/exel/deferred_context_value.rb +3 -1
- data/lib/exel/error/job_termination.rb +12 -0
- data/lib/exel/events.rb +6 -0
- data/lib/exel/instruction.rb +5 -2
- data/lib/exel/instruction_node.rb +2 -0
- data/lib/exel/job.rb +8 -4
- data/lib/exel/listen_instruction.rb +2 -0
- data/lib/exel/logging.rb +24 -1
- data/lib/exel/logging/logger_wrapper.rb +31 -0
- data/lib/exel/logging_helper.rb +36 -0
- data/lib/exel/middleware/chain.rb +67 -0
- data/lib/exel/middleware/logging.rb +30 -0
- data/lib/exel/null_instruction.rb +2 -0
- data/lib/exel/processor_helper.rb +9 -1
- data/lib/exel/processors/async_processor.rb +2 -8
- data/lib/exel/processors/run_processor.rb +2 -6
- data/lib/exel/processors/split_processor.rb +15 -10
- data/lib/exel/providers/local_file_provider.rb +9 -6
- data/lib/exel/providers/threaded_async_provider.rb +2 -0
- data/lib/exel/remote_value.rb +11 -0
- data/lib/exel/sequence_node.rb +2 -0
- data/lib/exel/value.rb +2 -0
- data/lib/exel/version.rb +3 -1
- data/spec/exel/ast_node_spec.rb +48 -27
- data/spec/exel/context_spec.rb +77 -77
- data/spec/exel/deferred_context_value_spec.rb +42 -42
- data/spec/exel/events_spec.rb +68 -59
- data/spec/exel/instruction_node_spec.rb +17 -16
- data/spec/exel/instruction_spec.rb +49 -42
- data/spec/exel/job_spec.rb +99 -84
- data/spec/exel/listen_instruction_spec.rb +11 -10
- data/spec/exel/logging/logger_wrapper_spec.rb +93 -0
- data/spec/exel/logging_helper_spec.rb +24 -0
- data/spec/exel/logging_spec.rb +69 -24
- data/spec/exel/middleware/chain_spec.rb +65 -0
- data/spec/exel/middleware/logging_spec.rb +31 -0
- data/spec/exel/middleware_spec.rb +68 -0
- data/spec/exel/null_instruction_spec.rb +4 -4
- data/spec/exel/processors/async_processor_spec.rb +17 -18
- data/spec/exel/processors/run_processor_spec.rb +10 -11
- data/spec/exel/processors/split_processor_spec.rb +99 -74
- data/spec/exel/providers/local_file_provider_spec.rb +26 -28
- data/spec/exel/providers/threaded_async_provider_spec.rb +37 -38
- data/spec/exel/sequence_node_spec.rb +12 -11
- data/spec/exel/value_spec.rb +33 -33
- data/spec/exel_spec.rb +9 -7
- data/spec/integration/integration_spec.rb +3 -1
- data/spec/spec_helper.rb +4 -2
- data/spec/support/integration_test_classes.rb +4 -3
- metadata +37 -48
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 546280a360b5092ca3cca1b0e0ae670560c60fb87e0c143b04405f9a5a3de9f9
|
4
|
+
data.tar.gz: 1b4b4f78ae5797c82f12abdd61057f915d1f06f53e775b4b1f502b06e1bea463
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0117f598e372c74606421159b405fdd4aa4fa1be3d2d658741ed4163b8c223cc3f75ce68ab0e67adbd495cd53a72b47b8411cf74135b067f2c430f00a3ff70da
|
7
|
+
data.tar.gz: 7a40ed7071070e4799681b750595c78b07326d72859f3824b9e411d94a782147fc94101b0c9466795270b41126a0ad0bbdb3a137185db7ae0ef825a90f3f5f5d
|
data/.codeclimate.yml
CHANGED
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -1,22 +1,31 @@
|
|
1
|
-
|
2
|
-
-
|
3
|
-
-
|
4
|
-
|
5
|
-
inherit_from: .rubocop_todo.yml
|
1
|
+
inherit_from:
|
2
|
+
- .rubocop_airbnb.yml
|
3
|
+
- .rubocop_todo.yml
|
6
4
|
|
7
5
|
AllCops:
|
6
|
+
TargetRubyVersion: 2.7
|
8
7
|
DisplayCopNames: true
|
9
8
|
DisplayStyleGuide: true
|
10
9
|
|
11
|
-
|
12
|
-
|
10
|
+
Airbnb/OptArgParameters:
|
11
|
+
Exclude:
|
12
|
+
- "lib/exel/processor_helper.rb"
|
13
|
+
- "lib/exel/logging/logger_wrapper.rb"
|
14
|
+
- "lib/exel/processors/*.rb"
|
15
|
+
- "lib/exel/error/job_termination.rb"
|
13
16
|
|
14
|
-
|
15
|
-
EnforcedStyle:
|
17
|
+
Layout/DotPosition:
|
18
|
+
EnforcedStyle: leading
|
16
19
|
|
17
|
-
|
18
|
-
|
20
|
+
Layout/IndentFirstArrayElement:
|
21
|
+
IndentationWidth: 4
|
19
22
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
+
Layout/MultilineMethodCallIndentation:
|
24
|
+
EnforcedStyle: indented
|
25
|
+
IndentationWidth: 4
|
26
|
+
|
27
|
+
Layout/SpaceInsideHashLiteralBraces:
|
28
|
+
EnforcedStyle: no_space
|
29
|
+
|
30
|
+
Metrics/LineLength:
|
31
|
+
Max: 120
|
data/.rubocop_airbnb.yml
ADDED
data/.rubocop_todo.yml
CHANGED
@@ -1,23 +1,11 @@
|
|
1
1
|
# This configuration was generated by
|
2
2
|
# `rubocop --auto-gen-config`
|
3
|
-
# on 2016-
|
3
|
+
# on 2016-10-14 22:09:13 -0400 using RuboCop version 0.39.0.
|
4
4
|
# The point is for the user to remove these configuration records
|
5
5
|
# one by one as the offenses are removed from the code base.
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
7
7
|
# versions of RuboCop, may require this file to be generated again.
|
8
8
|
|
9
|
-
# Offense count: 5
|
10
|
-
RSpec/AnyInstance:
|
11
|
-
Exclude:
|
12
|
-
- 'spec/exel/processors/split_processor_spec.rb'
|
13
|
-
- 'spec/exel/value_spec.rb'
|
14
|
-
- 'spec/integration/integration_spec.rb'
|
15
|
-
|
16
|
-
# Offense count: 1
|
17
|
-
RSpec/InstanceVariable:
|
18
|
-
Exclude:
|
19
|
-
- 'spec/exel/logging_spec.rb'
|
20
|
-
|
21
9
|
# Offense count: 1
|
22
10
|
Style/Documentation:
|
23
11
|
Exclude:
|
data/.travis.yml
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
env:
|
2
|
+
global:
|
3
|
+
- CC_TEST_REPORTER_ID=29a10c062e6416be84441296b1ec7b212f9c01a252e78b3e3df02cd9fb076abe
|
4
|
+
|
5
|
+
language: ruby
|
6
|
+
|
7
|
+
before_script:
|
8
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
9
|
+
- chmod +x ./cc-test-reporter
|
10
|
+
- ./cc-test-reporter before-build
|
11
|
+
|
12
|
+
script: bundle exec rspec
|
13
|
+
|
14
|
+
after_script:
|
15
|
+
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
16
|
+
|
17
|
+
rvm:
|
18
|
+
- 2.6
|
19
|
+
- 2.7
|
20
|
+
|
21
|
+
notifications:
|
22
|
+
email:
|
23
|
+
recipients:
|
24
|
+
- dev@yroo.com
|
25
|
+
on_success: change
|
26
|
+
on_failure: always
|
data/Gemfile
CHANGED
data/Gemfile.lock
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
exel (1.5.2)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
ast (2.4.0)
|
10
|
+
byebug (11.1.3)
|
11
|
+
coderay (1.1.2)
|
12
|
+
diff-lcs (1.3)
|
13
|
+
docile (1.3.2)
|
14
|
+
ffi (1.12.2)
|
15
|
+
formatador (0.2.5)
|
16
|
+
guard (2.16.2)
|
17
|
+
formatador (>= 0.2.4)
|
18
|
+
listen (>= 2.7, < 4.0)
|
19
|
+
lumberjack (>= 1.0.12, < 2.0)
|
20
|
+
nenv (~> 0.1)
|
21
|
+
notiffany (~> 0.0)
|
22
|
+
pry (>= 0.9.12)
|
23
|
+
shellany (~> 0.0)
|
24
|
+
thor (>= 0.18.1)
|
25
|
+
guard-compat (1.2.1)
|
26
|
+
guard-rspec (4.7.3)
|
27
|
+
guard (~> 2.1)
|
28
|
+
guard-compat (~> 1.1)
|
29
|
+
rspec (>= 2.99.0, < 4.0)
|
30
|
+
guard-rubocop (1.3.0)
|
31
|
+
guard (~> 2.0)
|
32
|
+
rubocop (~> 0.20)
|
33
|
+
jaro_winkler (1.5.4)
|
34
|
+
listen (3.2.1)
|
35
|
+
rb-fsevent (~> 0.10, >= 0.10.3)
|
36
|
+
rb-inotify (~> 0.9, >= 0.9.10)
|
37
|
+
lumberjack (1.2.4)
|
38
|
+
method_source (1.0.0)
|
39
|
+
nenv (0.3.0)
|
40
|
+
notiffany (0.1.3)
|
41
|
+
nenv (~> 0.1)
|
42
|
+
shellany (~> 0.0)
|
43
|
+
parallel (1.19.1)
|
44
|
+
parser (2.7.1.1)
|
45
|
+
ast (~> 2.4.0)
|
46
|
+
pry (0.13.1)
|
47
|
+
coderay (~> 1.1)
|
48
|
+
method_source (~> 1.0)
|
49
|
+
pry-byebug (3.9.0)
|
50
|
+
byebug (~> 11.0)
|
51
|
+
pry (~> 0.13.0)
|
52
|
+
rack (2.2.2)
|
53
|
+
rainbow (3.0.0)
|
54
|
+
rake (13.0.1)
|
55
|
+
rb-fsevent (0.10.3)
|
56
|
+
rb-inotify (0.10.1)
|
57
|
+
ffi (~> 1.0)
|
58
|
+
rspec (3.9.0)
|
59
|
+
rspec-core (~> 3.9.0)
|
60
|
+
rspec-expectations (~> 3.9.0)
|
61
|
+
rspec-mocks (~> 3.9.0)
|
62
|
+
rspec-core (3.9.1)
|
63
|
+
rspec-support (~> 3.9.1)
|
64
|
+
rspec-expectations (3.9.1)
|
65
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
66
|
+
rspec-support (~> 3.9.0)
|
67
|
+
rspec-mocks (3.9.1)
|
68
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
69
|
+
rspec-support (~> 3.9.0)
|
70
|
+
rspec-support (3.9.2)
|
71
|
+
rubocop (0.76.0)
|
72
|
+
jaro_winkler (~> 1.5.1)
|
73
|
+
parallel (~> 1.10)
|
74
|
+
parser (>= 2.6)
|
75
|
+
rainbow (>= 2.2.2, < 4.0)
|
76
|
+
ruby-progressbar (~> 1.7)
|
77
|
+
unicode-display_width (>= 1.4.0, < 1.7)
|
78
|
+
rubocop-airbnb (3.0.2)
|
79
|
+
rubocop (~> 0.76.0)
|
80
|
+
rubocop-performance (~> 1.5.0)
|
81
|
+
rubocop-rails (~> 2.3.2)
|
82
|
+
rubocop-rspec (~> 1.30.0)
|
83
|
+
rubocop-performance (1.5.2)
|
84
|
+
rubocop (>= 0.71.0)
|
85
|
+
rubocop-rails (2.3.2)
|
86
|
+
rack (>= 1.1)
|
87
|
+
rubocop (>= 0.72.0)
|
88
|
+
rubocop-rspec (1.30.1)
|
89
|
+
rubocop (>= 0.60.0)
|
90
|
+
ruby-progressbar (1.10.1)
|
91
|
+
shellany (0.0.1)
|
92
|
+
simplecov (0.18.5)
|
93
|
+
docile (~> 1.1)
|
94
|
+
simplecov-html (~> 0.11)
|
95
|
+
simplecov-html (0.12.2)
|
96
|
+
terminal-notifier (1.6.3)
|
97
|
+
terminal-notifier-guard (1.7.0)
|
98
|
+
thor (1.0.1)
|
99
|
+
unicode-display_width (1.6.1)
|
100
|
+
|
101
|
+
PLATFORMS
|
102
|
+
ruby
|
103
|
+
|
104
|
+
DEPENDENCIES
|
105
|
+
exel!
|
106
|
+
guard (~> 2)
|
107
|
+
guard-rspec (~> 4)
|
108
|
+
guard-rubocop (~> 1)
|
109
|
+
pry-byebug
|
110
|
+
rake (~> 13)
|
111
|
+
rspec (~> 3)
|
112
|
+
rubocop-airbnb (~> 3.0)
|
113
|
+
simplecov (~> 0.17)
|
114
|
+
terminal-notifier (~> 1.6.0)
|
115
|
+
terminal-notifier-guard (~> 1.7.0)
|
116
|
+
|
117
|
+
BUNDLED WITH
|
118
|
+
2.1.4
|
data/Guardfile
CHANGED
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
[![Gem Version](https://badge.fury.io/rb/exel.svg)](https://badge.fury.io/rb/exel)
|
3
3
|
[![Code Climate](https://codeclimate.com/github/47colborne/exel/badges/gpa.svg)](https://codeclimate.com/github/47colborne/exel)
|
4
4
|
[![Test Coverage](https://codeclimate.com/github/47colborne/exel/badges/coverage.svg)](https://codeclimate.com/github/47colborne/exel/coverage)
|
5
|
-
[![Build Status](https://
|
5
|
+
[![Build Status](https://travis-ci.org/47colborne/exel.svg?branch=master)](https://travis-ci.org/47colborne/exel)
|
6
6
|
[![Documentation](http://img.shields.io/badge/docs-rdoc.info-blue.svg)](http://www.rubydoc.info/github/47colborne/exel/master)
|
7
7
|
|
8
8
|
EXEL is the Elastic eXEcution Language, a simple Ruby DSL for creating processing jobs that can be run on a single machine, or scaled up to run on dozens of machines with no changes to the job itself. To run a job on more than one machine, simply install EXEL async and remote provider gems to integrate with your preferred platforms. The currently implemented providers so far are:
|
@@ -35,15 +35,17 @@ Or install it yourself as:
|
|
35
35
|
|
36
36
|
A processor can be any class that provides the following interface:
|
37
37
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
38
|
+
```ruby
|
39
|
+
class MyProcessor
|
40
|
+
def initialize(context)
|
41
|
+
# typically context is assigned to @context here
|
42
|
+
end
|
43
|
+
|
44
|
+
def process(block)
|
45
|
+
# do your work here
|
46
|
+
end
|
47
|
+
end
|
48
|
+
```
|
47
49
|
|
48
50
|
Processors are initialized immediately before ```#process``` is called, allowing them to set up any state that they need from the context. The ```#process``` method is where your processing logic will be implemented. Processors should be focused on performing one particular aspect of the processing that you want to accomplish, allowing your job to be composed of a sequence of small processing steps. If a block was given in the call to ```process``` in the job DSL, it will be passed as the argument to ```#process``` and can be run with: ```block.run(@context)```
|
49
51
|
|
@@ -59,35 +61,98 @@ If you use EXEL with an async provider, such as [exel-sidekiq](https://github.co
|
|
59
61
|
|
60
62
|
### Supported Instructions
|
61
63
|
|
62
|
-
* ```process```
|
63
|
-
* ```split```
|
64
|
-
* ```async``` Asynchronously
|
64
|
+
* ```process``` Executes the given processor class (specified by the ```:with``` option), given the current context and any additional arguments provided
|
65
|
+
* ```split``` Splits the input data into 1000 line chunks and run the given block for each chunk. Assumes that the input data is a CSV formatted file referenced by ```context[:resource]```. When each block is run, ```context[:resource]``` will reference to the chunk file.
|
66
|
+
* ```async``` Asynchronously runs the given block. Uses the configured async provider to execute the block.
|
65
67
|
* ```run``` Runs the job specified by the ```:job``` option. The job will run using the current context.
|
68
|
+
* ```listen``` Registers an event listener. See the [Events](#events) section below for more detail.
|
66
69
|
|
67
70
|
### Example job
|
68
71
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
end
|
72
|
+
```ruby
|
73
|
+
EXEL::Job.define :example_job do
|
74
|
+
# Download a large CSV data file
|
75
|
+
process with: FTPDownloader, host: ftp.example.com, path: context[:file_path]
|
76
|
+
|
77
|
+
# split it into smaller 1000 line files
|
78
|
+
split do
|
79
|
+
# for each file asynchronously run the following sequence of processors
|
80
|
+
async do
|
81
|
+
process with: RecordLoader # convert each row of data into your domain model
|
82
|
+
process with: SomeProcessor # apply some additional processing to each record
|
83
|
+
process with: RecordSaver # write this batch of records to your database
|
84
|
+
process with: ExternalServiceProcessor # interact with some service, ex: updating a search index
|
83
85
|
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
```
|
84
89
|
|
85
90
|
Elsewhere in your application, you could run this job as follows:
|
86
91
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
92
|
+
```ruby
|
93
|
+
def run_example_job(file_path)
|
94
|
+
# context can also be passed as a Hash
|
95
|
+
context = EXEL::Context.new(file_path: file_path, user: 'username')
|
96
|
+
EXEL::Job.run(:example_job, context)
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
### Events
|
101
|
+
|
102
|
+
Event listeners can be registered using the ```listen``` instruction:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
listen for: :my_event, with: MyEventListener
|
106
|
+
```
|
107
|
+
|
108
|
+
The event listener must implement a method with the same name as the event which accepts two arguments: the context and any data passed when the event was triggered:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
class MyEventListener
|
112
|
+
def self.my_event(context, data)
|
113
|
+
# handle event
|
114
|
+
end
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
To trigger an event, include the ```EXEL::Events``` module and call #trigger with the event name and data:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
include EXEL::Events
|
122
|
+
|
123
|
+
def process(_block)
|
124
|
+
# trigger event and optionally pass data to the event listener
|
125
|
+
trigger :my_event, foo: 'bar'
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
### Middleware
|
130
|
+
|
131
|
+
Middleware is code configured to run around each processor execution. It is modelled after [Rack](https://github.com/rack/rack) and [Sidekiq](https://github.com/mperham/sidekiq). Custom middleware can be added as follows:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
EXEL.configure do |config|
|
135
|
+
config.middleware.add(MyMiddleware)
|
136
|
+
config.middleware.add(AnotherMiddleware, 'constructor arg')
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
Middleware can be any class that implements a ```call``` method that includes a call to ```yield```:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
class MyMiddleware
|
144
|
+
def call(processor_class, context, args)
|
145
|
+
puts 'before process'
|
146
|
+
|
147
|
+
# must yield so other middleware and processor will run
|
148
|
+
yield
|
149
|
+
|
150
|
+
puts 'after process'
|
151
|
+
end
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
The ```call``` method will be passed the class of the processor that will be executed, the current context, and any args that were passed to the processor in the job definition.
|
91
156
|
|
92
157
|
## Contributing
|
93
158
|
|
data/Rakefile
CHANGED
data/exel.gemspec
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
2
4
|
lib = File.expand_path('../lib', __FILE__)
|
3
5
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
6
|
require 'exel/version'
|
@@ -18,16 +20,14 @@ Gem::Specification.new do |spec|
|
|
18
20
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
21
|
spec.require_paths = ['lib']
|
20
22
|
|
21
|
-
spec.add_development_dependency '
|
22
|
-
spec.add_development_dependency 'rake', '~> 10'
|
23
|
+
spec.add_development_dependency 'rake', '~> 13'
|
23
24
|
spec.add_development_dependency 'rspec', '~> 3'
|
25
|
+
spec.add_development_dependency 'simplecov', '~> 0.17'
|
24
26
|
spec.add_development_dependency 'guard', '~> 2'
|
25
27
|
spec.add_development_dependency 'guard-rspec', '~> 4'
|
26
28
|
spec.add_development_dependency 'guard-rubocop', '~> 1'
|
27
|
-
spec.add_development_dependency 'terminal-notifier', '~> 1'
|
28
|
-
spec.add_development_dependency 'terminal-notifier-guard', '~> 1'
|
29
|
-
spec.add_development_dependency 'rubocop', '~>
|
30
|
-
spec.add_development_dependency 'rubocop-rspec', '~> 1'
|
31
|
-
spec.add_development_dependency 'rubocop-rspec-focused', '~> 0'
|
29
|
+
spec.add_development_dependency 'terminal-notifier', '~> 1.6.0'
|
30
|
+
spec.add_development_dependency 'terminal-notifier-guard', '~> 1.7.0'
|
31
|
+
spec.add_development_dependency 'rubocop-airbnb', '~> 3.0'
|
32
32
|
spec.add_development_dependency 'pry-byebug'
|
33
33
|
end
|