gorg_service 0.1.2 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3ef9f94b4bf945e579b3cd7e41bee32b4daa667a
4
- data.tar.gz: 0526af44cef52f9a89727b929ed5df8f0400711e
3
+ metadata.gz: c367d16bc5790468c190d509d84e89effef69fbb
4
+ data.tar.gz: 1951f35a70ffacddb8f1f141df9febeadc890420
5
5
  SHA512:
6
- metadata.gz: a8983e511b243a7f371646633c3fb3b19fdc96dbfcf2278296155f915bfd5c8dda3d13aa0cd30863692390532c012097ba11a447f1cd97f8ba1eb34b95e33076
7
- data.tar.gz: 5ed51cc437c9a347c7b9958a791a55f4605b52234c816f28bfda5709e379de91eb707a68a77245b91afcba70e5e0baf9a8f05a3d9ab292139be366cc200798ef
6
+ metadata.gz: 912bc14211df2c260d360023f3e6ee378bb228db615408d46615197bb0db4f0acaa4957aeb10bb1d3ba1c1a2b6e7593fbbb12b99a50467583fce7326b22028b4
7
+ data.tar.gz: 7c1bc8ac7f5f3268d35b5d481efe7fb2821b5d2abb6fdbfb3389a47c2de453f1b6660bfdd1e267ba4cbbba90318953b4c2e9bf313e8b3a0ef51c8dc430680d2e
data/.gitignore CHANGED
@@ -9,4 +9,5 @@
9
9
  /tmp/
10
10
  /build/
11
11
  *.gem
12
- /bin/worker
12
+ /bin/worker
13
+ /spec/gorg_service/support/conf/rabbit_mq.yml
data/.todo.reek ADDED
@@ -0,0 +1,26 @@
1
+ ---
2
+ Attribute:
3
+ exclude:
4
+ - GorgService#configuration
5
+ - GorgService::Configuration#application_id
6
+ - GorgService::Configuration#application_name
7
+ - GorgService::Configuration#message_handler_map
8
+ - GorgService::Configuration#rabbitmq_deferred_time
9
+ - GorgService::Configuration#rabbitmq_exchange_name
10
+ - GorgService::Configuration#rabbitmq_host
11
+ - GorgService::Configuration#rabbitmq_max_attempts
12
+ - GorgService::Configuration#rabbitmq_password
13
+ - GorgService::Configuration#rabbitmq_port
14
+ - GorgService::Configuration#rabbitmq_queue_name
15
+ - GorgService::Configuration#rabbitmq_user
16
+ - GorgService::Configuration#rabbitmq_vhost
17
+ - GorgService::Message#data
18
+ - GorgService::Message#errors
19
+ - GorgService::Message#event
20
+ - GorgService::Message#id
21
+ TooManyInstanceVariables:
22
+ exclude:
23
+ - GorgService::Configuration
24
+ ControlParameter:
25
+ exclude:
26
+ - GorgService::Message#initialize
data/.travis.yml CHANGED
@@ -4,4 +4,9 @@ rvm:
4
4
  before_install: gem install bundler -v 1.11.2
5
5
  addons:
6
6
  code_climate:
7
- repo_token: 8a2a969cd1d481fbc952943cc27b54584b2f27217ea2e2905e77652d44c502be
7
+ repo_token: 8a2a969cd1d481fbc952943cc27b54584b2f27217ea2e2905e77652d44c502be
8
+ before_script:
9
+ - cp spec/gorg_service/support/conf/rabbit_mq.yml.travis spec/gorg_service/support/conf/rabbit_mq.yml
10
+ sudo: required
11
+ services:
12
+ - rabbitmq
data/Gemfile CHANGED
@@ -2,3 +2,4 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
  gem "codeclimate-test-reporter", group: :test, require: nil
5
+ gem 'reek', '~> 4.0'
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # GorgService
2
- [![Code Climate](https://codeclimate.com/github/Zooip/gorg_service/badges/gpa.svg)](https://codeclimate.com/github/Zooip/gorg_service) [![Test Coverage](https://codeclimate.com/github/Zooip/gorg_service/badges/coverage.svg)](https://codeclimate.com/github/Zooip/gorg_service/coverage) [![Build Status](https://travis-ci.org/Zooip/gorg_service.svg?branch=master)](https://travis-ci.org/Zooip/gorg_service)
2
+ [![Code Climate](https://codeclimate.com/github/Zooip/gorg_service/badges/gpa.svg)](https://codeclimate.com/github/Zooip/gorg_service) [![Test Coverage](https://codeclimate.com/github/Zooip/gorg_service/badges/coverage.svg)](https://codeclimate.com/github/Zooip/gorg_service/coverage) [![Build Status](https://travis-ci.org/Zooip/gorg_service.svg?branch=master)](https://travis-ci.org/Zooip/gorg_service) [![Gem Version](https://badge.fury.io/rb/gorg_service.svg)](https://badge.fury.io/rb/gorg_service) [![Dependency Status](https://gemnasium.com/badges/github.com/Zooip/gorg_service.svg)](https://gemnasium.com/github.com/Zooip/gorg_service)
3
+ Standard RabbitMQ bot used in Gadz.org SOA
3
4
 
4
5
  ## Installation
5
6
 
@@ -19,7 +20,7 @@ Or install it yourself as:
19
20
 
20
21
  ## Setup
21
22
 
22
- Before being used GramV1Client must be configured. In Rails app, put it in an Initializer.
23
+ Before being used, GorgService must be configured. In Rails app, put it in an Initializer.
23
24
 
24
25
  ```ruby
25
26
  GorgService.configure do |c|
@@ -45,18 +46,97 @@ GorgService.configure do |c|
45
46
  # c.rabbitmq_queue_name = c.application_name
46
47
  # c.rabbitmq_exchange_name = "exchange"
47
48
  #
48
- # time before trying again on softfail (temporary error)
49
+ # time before trying again on softfail in milliseconds (temporary error)
49
50
  # c.rabbitmq_deferred_time = 1800000 # 30min
50
51
  #
51
52
  # maximum number of try before discard a message
52
53
  # c.rabbitmq_max_attempts = 48 # 24h with default deferring delay
54
+ #
55
+ # Routing hash
56
+ # map routing_key of received message with MessageHandler
57
+ # exemple:
58
+ # c.message_handler_map={
59
+ # "some.routing.key" => MyMessageHandler,
60
+ # "Another.routing.key" => OtherMessageHandler,
61
+ # "third.routing.key" => MyMessageHandler,
62
+ # }
63
+ c.message_handler_map= {} #TODO : Set my routing hash
53
64
 
54
65
  end
55
66
  ```
56
67
 
57
68
  ## Usage
58
69
 
59
- TODO: Write usage instructions here
70
+ To start the RabbitMQ consummer use :
71
+ ```ruby
72
+ my_service = GorgService.new
73
+ my_service.run
74
+ ```
75
+ ### Routing and MessageHandler
76
+ When running, GorgService act as a consumer on Gadz.org RabbitMQ network.
77
+ It bind its queue on the main exchange and subscribes to routing keys defines in `message_handler_map`
78
+
79
+ Each received message will be routed to the corresponding `MessageHandler`
80
+ > **Warning** : RabbitMQ wildcards characters `#` and `*`are NOT supported for now
81
+
82
+ A `MessageHandler` is a kind of controller. This is where you put the message is processed.
83
+ A `MessageHandler` expect a `GorgService::Message` as param of its `initializer`method.
84
+
85
+ Here is an exemple `MessageHandler` :
86
+ ```ruby
87
+ require 'json'
88
+ require 'json-schema' #Checkout https://github.com/ruby-json-schema/json-schema
89
+
90
+ class ExampleMessageHandler < GorgService::MessageHandler
91
+
92
+ EXPECTED_SCHEMA = {
93
+ "type" => "object",
94
+ "required" => ["user_id"],
95
+ "properties" => {
96
+ "user_id" => {"type" => "integer"}
97
+ }
98
+ }
99
+
100
+ def initialize(msg)
101
+ data=msg.data
102
+ begin
103
+ JSON::Validator.validate!(EXPECTED_SCHEMA, data)
104
+ rescue JSON::Schema::ValidationError => e
105
+ #This message can't be processed, it will be discarded
106
+ raise_hardfail("Invalid message",e)
107
+ end
108
+
109
+ begin
110
+ MyAPI.send(msg.data)
111
+ rescue MyAPI::UnavailableConnection => e
112
+ # This message can be processed but external resources
113
+ # are not available at this moment, retry later
114
+ raise_softfail("Can't connect to MyAPI",e)
115
+ end
116
+ end
117
+ end
118
+ ```
119
+
120
+ As showed in this example,`GorgService::MessageHandler` provides 2 helpers :
121
+
122
+ - `raise_hardfail`: This will raise a `HardfailError`exception. The message can't be processed and will never be. It is logged and send back to main exchange for audit purpose.
123
+
124
+ - `raise_softfail`: This will raise a `SoftfailError`exception. The message can't be processed at this moment but may be processed in future : connection problems, rate limiting API, etc. It is sent to a deferred queue where it will be delayed for `rabbitmq_deferred_time`millisecconds before being sent back in the main exchange for re-queuing.
125
+
126
+ Each one of this helpers expect two params :
127
+
128
+ - `message` : The information to be displayed in message's error log
129
+ - `exception` (optional) : The runtime esception causing the hardfail, for debug purpose
130
+
131
+ ### Message structure
132
+ `GorgService::Message` is defined [here](https://github.com/Zooip/gorg_service/blob/master/lib/gorg_service/message.rb)
133
+
134
+ It provides 4 attributes :
135
+
136
+ - `event` : this is the same as the routing key
137
+ - `id`: message UUID
138
+ - `errors`: `Hash` containing the message error log with previous errors
139
+ - `data`: `Hash` containing the data to be processed
60
140
 
61
141
  ## Development
62
142
 
data/gorg_service.gemspec CHANGED
@@ -31,4 +31,8 @@ Gem::Specification.new do |spec|
31
31
  spec.add_development_dependency "bundler", "~> 1.11"
32
32
  spec.add_development_dependency "rake", "~> 10.0"
33
33
  spec.add_development_dependency "rspec", "~> 3.0"
34
+ spec.add_development_dependency "codeclimate-test-reporter", "~> 3.0"
35
+ spec.add_development_dependency 'bogus', '~> 0.1.6'
36
+ spec.add_development_dependency 'bunny-mock', '~> 1.4'
37
+ spec.add_development_dependency 'byebug', '~> 9.0'
34
38
  end
@@ -1,3 +1,4 @@
1
+ # Add configuration features to GorgService
1
2
  class GorgService
2
3
  class << self
3
4
  attr_writer :configuration
@@ -8,10 +9,12 @@ class GorgService
8
9
 
9
10
 
10
11
  def configure
12
+ @configuration = Configuration.new
11
13
  yield(configuration)
12
14
  end
13
15
  end
14
16
 
17
+ # Hold configuration of GorgService in instance variables
15
18
  class Configuration
16
19
  attr_accessor :application_name,
17
20
  :application_id,
@@ -23,11 +26,11 @@ class GorgService
23
26
  :rabbitmq_max_attempts,
24
27
  :rabbitmq_user,
25
28
  :rabbitmq_password,
26
- :message_handler_map,
29
+ :rabbitmq_vhost,
30
+ :message_handler_map
27
31
 
28
32
 
29
33
  def initialize
30
-
31
34
  @application_name = "GorgService"
32
35
  @application_id = "gs"
33
36
  @message_handler_map = {}
@@ -38,6 +41,7 @@ class GorgService
38
41
  @rabbitmq_exchange_name = "exchange"
39
42
  @rabbitmq_user = nil
40
43
  @rabbitmq_password = nil
44
+ @rabbitmq_vhost = "/"
41
45
  @rabbitmq_max_attempts = 48 #24h with default timeout
42
46
  end
43
47
  end
@@ -2,17 +2,36 @@
2
2
  # encoding: utf-8
3
3
 
4
4
  class GorgService
5
+
6
+ #Common behavior of failling errors
5
7
  class FailError < StandardError
6
8
  attr_reader :error_raised
7
9
 
8
- def initialize(error_raised)
10
+ def initialize(message = nil, error_raised = nil)
11
+ @message = message
9
12
  @error_raised = error_raised
10
13
  end
14
+
15
+ def message
16
+ @message
17
+ end
18
+
19
+ def type
20
+ ""
21
+ end
11
22
  end
12
23
 
24
+ #Softfail error : This message should be processed again later
13
25
  class SoftfailError < FailError
26
+ def type
27
+ "softfail"
28
+ end
14
29
  end
15
30
 
31
+ #Hardfail error : This message is not processable and will never be
16
32
  class HardfailError < FailError
33
+ def type
34
+ "hardfail"
35
+ end
17
36
  end
18
37
  end
@@ -6,16 +6,13 @@ require "bunny"
6
6
  class GorgService
7
7
  class Listener
8
8
 
9
- def initialize(host: "localhost", port: 5672, queue_name: "gapps", rabbitmq_user: nil, rabbitmq_password: nil, exchange_name: nil, message_handler_map: {default: DefaultMessageHandler}, deferred_time: 1800000)
10
- @host=host
11
- @port=port
9
+ def initialize(bunny_session: nil,queue_name: "gapps", exchange_name: nil, message_handler_map: {default: DefaultMessageHandler}, deferred_time: 1800000, max_attempts: 48)
12
10
  @queue_name=queue_name
13
11
  @exchange_name=exchange_name
14
- @rabbitmq_user=rabbitmq_user
15
- @rabbitmq_password=rabbitmq_password
16
12
  @message_handler_map=message_handler_map
17
13
  @deferred_time=deferred_time
18
-
14
+ @max_attempts=max_attempts
15
+ @rmq_connection=bunny_session
19
16
  end
20
17
 
21
18
  def listen
@@ -31,52 +28,55 @@ class GorgService
31
28
 
32
29
  q.bind(@exchange_name, :routing_key => '#')
33
30
 
34
- puts " [*] Waiting for messages in #{q.name}. To exit press CTRL+C"
35
31
  ch.prefetch(1)
36
32
 
37
- begin
38
- q.subscribe(:manual_ack => true, :block => true) do |delivery_info, properties, body|
39
- routing_key=delivery_info[:routing_key]
40
- puts " [#] Received message with routing key #{routing_key} containing : #{body}"
41
- message_handler=message_handler_for routing_key
42
- message=Message.parse_body(body)
43
- begin
44
- if message_handler
45
- message_handler.new(message)
46
- else
47
- raise HardfailError.new(), "Unknown routing_key"
48
- end
49
- rescue SoftfailError => e
50
- message.log_error(e)
51
- puts " [*] SOFTFAIL ERROR : #{e.message}"
52
- if message.errors.count > 5
53
- puts " [*] DISCARD MESSAGE : #{message.errors.count} errors in message log"
54
- else
55
-
56
- send_to_deferred_queue(message)
57
- end
58
- rescue HardfailError => e
59
- puts " [*] SOFTFAIL ERROR : #{e.message}"
60
- puts " [*] DISCARD MESSAGE"
61
- end
62
-
63
- ch.ack(delivery_info.delivery_tag)
64
- end
65
- rescue Interrupt => _
66
- conn.close
33
+ q.subscribe(:manual_ack => true) do |delivery_info, properties, body|
34
+ routing_key=delivery_info[:routing_key]
35
+ puts " [#] Received message with routing key #{routing_key} containing : #{body}"
36
+ message_handler=message_handler_for routing_key
37
+ message=Message.parse_body(body)
38
+
39
+ call_message_handler(message_handler, message)
40
+
41
+ ch.ack(delivery_info.delivery_tag)
67
42
  end
43
+
68
44
  end
69
45
 
70
46
  def rmq_connection
71
- if @rmq_connection
72
- @rmq_connection
47
+ @rmq_connection.start unless @rmq_connection.connected?
48
+ @rmq_connection
49
+ end
50
+
51
+ def call_message_handler(message_handler, message)
52
+ begin
53
+ raise HardfailError.new(), "Routing error" unless message_handler
54
+ message_handler.new(message)
55
+
56
+ rescue SoftfailError => e
57
+ message.log_error(e)
58
+ process_softfail(e,message)
59
+
60
+ rescue HardfailError => e
61
+ message.log_error(e)
62
+ process_hardfail(e)
63
+ end
64
+ end
65
+
66
+ def process_softfail(e,message)
67
+ puts " [*] SOFTFAIL ERROR : #{e.message}"
68
+ if message.errors.count >= @max_attempts
69
+ puts " [*] DISCARD MESSAGE : #{message.errors.count} errors in message log"
73
70
  else
74
- @rmq_connection=Bunny.new(:hostname => @host, :user => @rabbitmq_user, :pass => @rabbitmq_password)
75
- @rmq_connection.start
76
- @rmq_connection
71
+ send_to_deferred_queue(message)
77
72
  end
78
73
  end
79
74
 
75
+ def process_hardfail(e)
76
+ puts " [*] SOFTFAIL ERROR : #{e.message}"
77
+ puts " [*] DISCARD MESSAGE"
78
+ end
79
+
80
80
  def send_to_deferred_queue(msg)
81
81
  conn=rmq_connection
82
82
  @delayed_chan||=conn.create_channel
@@ -89,7 +89,7 @@ class GorgService
89
89
  }
90
90
  )
91
91
  puts " [*] DEFER MESSAGE : message sent to #{@queue_name}_deferred qith routing key #{msg.event}"
92
- q.publish(msg.to_str, :routing_key => msg.event)
92
+ q.publish(msg.to_json, :routing_key => msg.event)
93
93
  end
94
94
 
95
95
  def message_handler_for routing_key
@@ -23,16 +23,8 @@ class GorgService
23
23
  @errors=errors
24
24
  end
25
25
 
26
- # Generate new id
27
- #
28
- # TODO
29
- # Avoid using AppConfig
30
- def generate_id
31
- "#{GorgService.configuration.application_id}_#{Time.now.to_i}"
32
- end
33
-
34
26
  # Generate RabbitMQ message body
35
- def to_str
27
+ def to_json
36
28
  body={
37
29
  id: @id,
38
30
  event: @event,
@@ -48,7 +40,7 @@ class GorgService
48
40
  # Log FailError in message body
49
41
  def log_error error
50
42
  errors<<{
51
- type: error.class.to_s.downcase,
43
+ type: error.type.downcase,
52
44
  message: error.message,
53
45
  timestamp: Time.now.utc.iso8601,
54
46
  extra: error.error_raised.inspect,
@@ -66,15 +58,19 @@ class GorgService
66
58
  begin
67
59
  json_body=JSON.parse(body)
68
60
 
69
- self.new(
61
+ msg=self.new(
70
62
  id: json_body["id"],
71
63
  event: json_body["event"],
72
64
  data: convert_keys_to_sym(json_body["data"]),
73
- errors: convert_keys_to_sym(json_body["errors"]),
65
+ errors: json_body["errors"]&&json_body["errors"].map{|e| convert_keys_to_sym(e)},
74
66
  )
75
67
 
68
+ msg.errors=msg.errors.each do |e|
69
+ e[:timestamp]=(e[:timestamp] ? DateTime.parse(e[:timestamp]) : nil)
70
+ end
71
+ msg
76
72
  rescue JSON::ParserError => e
77
- raise HardfailError(e), "Unprocessable message : Unable to parse JSON message body"
73
+ raise GorgService::HardfailError.new(e), "Unprocessable message : Unable to parse JSON message body"
78
74
  end
79
75
  end
80
76
 
@@ -91,6 +87,14 @@ class GorgService
91
87
  end
92
88
  s2s[input_hash]
93
89
  end
90
+
91
+ private
92
+
93
+ # Generate new id
94
+ def generate_id
95
+ "#{GorgService.configuration.application_id}_#{Time.now.to_i}"
96
+ end
97
+
94
98
 
95
99
 
96
100
  end
@@ -2,5 +2,5 @@
2
2
  # encoding: utf-8
3
3
 
4
4
  class GorgService
5
- VERSION = "0.1.2"
5
+ VERSION = "0.2.0"
6
6
  end
data/lib/gorg_service.rb CHANGED
@@ -6,20 +6,44 @@ require "gorg_service/message"
6
6
  require "gorg_service/message_handler"
7
7
 
8
8
  class GorgService
9
- def initialize()
10
- @listener=Listener.new(
9
+ def initialize(listener: nil, bunny_session: nil)
10
+
11
+ @bunny_session= bunny_session || Bunny.new(
12
+ :hostname => GorgService.configuration.rabbitmq_host,
13
+ :port => GorgService.configuration.rabbitmq_port,
14
+ :user => GorgService.configuration.rabbitmq_user,
15
+ :pass => GorgService.configuration.rabbitmq_password,
16
+ :vhost => GorgService.configuration.rabbitmq_vhost
17
+ )
18
+
19
+ @listener= listener || Listener.new(
20
+ bunny_session: @bunny_session,
11
21
  message_handler_map:GorgService.configuration.message_handler_map,
12
- host: GorgService.configuration.rabbitmq_host,
13
- port: GorgService.configuration.rabbitmq_port,
14
22
  queue_name: GorgService.configuration.rabbitmq_queue_name,
15
23
  exchange_name: GorgService.configuration.rabbitmq_exchange_name,
16
- rabbitmq_user: GorgService.configuration.rabbitmq_user,
17
- rabbitmq_password: GorgService.configuration.rabbitmq_password,
18
24
  deferred_time: GorgService.configuration.rabbitmq_deferred_time,
25
+ max_attempts: GorgService.configuration.rabbitmq_max_attempts,
19
26
  )
20
27
  end
21
28
 
22
29
  def run
30
+ begin
31
+ self.start
32
+ puts " [*] Waiting for messages. To exit press CTRL+C"
33
+ loop do
34
+ sleep(1)
35
+ end
36
+ rescue SystemExit, Interrupt => _
37
+ self.stop
38
+ end
39
+ end
40
+
41
+ def start
42
+ @bunny_session.start
23
43
  @listener.listen
24
44
  end
45
+
46
+ def stop
47
+ @bunny_session.close
48
+ end
25
49
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gorg_service
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexandre Narbonne
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-05-27 00:00:00.000000000 Z
11
+ date: 2016-05-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -72,6 +72,62 @@ dependencies:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
74
  version: '3.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: codeclimate-test-reporter
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '3.0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: bogus
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: 0.1.6
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: 0.1.6
103
+ - !ruby/object:Gem::Dependency
104
+ name: bunny-mock
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '1.4'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '1.4'
117
+ - !ruby/object:Gem::Dependency
118
+ name: byebug
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '9.0'
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '9.0'
75
131
  description:
76
132
  email:
77
133
  - alexandre.narbonne@gadz.org
@@ -81,6 +137,7 @@ extra_rdoc_files: []
81
137
  files:
82
138
  - ".gitignore"
83
139
  - ".rspec"
140
+ - ".todo.reek"
84
141
  - ".travis.yml"
85
142
  - Gemfile
86
143
  - LICENSE.txt