gorg_service 0.1.2 → 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: 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