hook-client 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 +7 -0
- data/LICENSE +22 -0
- data/README.md +64 -0
- data/lib/hook-client.rb +11 -0
- data/lib/hook-client/auth.rb +4 -0
- data/lib/hook-client/channel.rb +13 -0
- data/lib/hook-client/channel/sse.rb +9 -0
- data/lib/hook-client/channel/websocket.rb +4 -0
- data/lib/hook-client/client.rb +93 -0
- data/lib/hook-client/collection.rb +285 -0
- data/lib/hook-client/extensions.rb +12 -0
- data/lib/hook-client/extensions/symbol.rb +28 -0
- data/lib/hook-client/keys.rb +26 -0
- data/lib/hook-client/model.rb +116 -0
- data/lib/hook-client/version.rb +3 -0
- data/spec/client_spec.rb +11 -0
- data/spec/collection_spec.rb +68 -0
- data/spec/extensions_spec.rb +47 -0
- data/spec/keys_spec.rb +20 -0
- data/spec/model_spec.rb +55 -0
- data/spec/spec_helper.rb +15 -0
- metadata +133 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: c369258ba544d32bf8658fec16b773009810d138
|
|
4
|
+
data.tar.gz: cf1eda4213eda65e81875dd4ef2cb219b6160ee7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f069f6984f0d0f27cac78b0dfaa723ae9964786f048f1904a5b989121df2e4e6b9b31ad87f61846e7231b17ff9a4423dcef71a8b4edc724bb43f37b577208e2b
|
|
7
|
+
data.tar.gz: c2457a562ca1575585b8dc47baaaab14171722d67b1084d46708359b5423ed3e260deb31c40a4dd32322bcf15cc58be6379c07c21d3d54440cf40a1c98ed8ad2
|
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2014 Doubleleft
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
hook-ruby client 
|
|
2
|
+
===
|
|
3
|
+
|
|
4
|
+
ruby client for [hook](github.com/doubleleft/hook/).
|
|
5
|
+
|
|
6
|
+
- [API Reference](http://doubleleft.github.io/hook-ruby).
|
|
7
|
+
- [RubyGem](http://rubygems.org/gems/hook-client)
|
|
8
|
+
|
|
9
|
+
Getting started:
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
# Gemfile
|
|
14
|
+
gem 'hook-client'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Basic usage:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require 'hook-client'
|
|
21
|
+
client = Hook::Client(:app_id => 1, :key => "something", :endpoint => "https://dl-api.heroku.com")
|
|
22
|
+
client.collection(:posts).create(:title => "Getting Started", :description => "Getting started with dl-api-ruby.")
|
|
23
|
+
puts client.collection(:posts).where(:title => "Getting Started").count
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
For more examples, please see [our tests](spec).
|
|
27
|
+
|
|
28
|
+
Using it with Rails
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
Set-up with your credentials:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
Hook::Client.configure(
|
|
35
|
+
:app_id => 1,
|
|
36
|
+
:key => "1f143fde82d14643099ae45e6c98c8e1",
|
|
37
|
+
:endpoint => "https://dl-api.heroku.com"
|
|
38
|
+
)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Define your models:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
class Post
|
|
45
|
+
include Hook::Model
|
|
46
|
+
|
|
47
|
+
field :title
|
|
48
|
+
field :description
|
|
49
|
+
|
|
50
|
+
validates_presence_of :title
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Hook::Model's uses almost the same syntax as ActiveRecord, which you're already
|
|
55
|
+
familiar with.
|
|
56
|
+
|
|
57
|
+
You will be able to use any
|
|
58
|
+
[ActiveModel](https://github.com/rails/rails/tree/master/activemodel) goodies,
|
|
59
|
+
such as validation, serialization and dirty methods.
|
|
60
|
+
|
|
61
|
+
License
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
MIT
|
data/lib/hook-client.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Hook
|
|
2
|
+
autoload :VERSION, 'hook-client/version'
|
|
3
|
+
autoload :Client, 'hook-client/client'
|
|
4
|
+
|
|
5
|
+
autoload :Keys, 'hook-client/keys'
|
|
6
|
+
autoload :Collection, 'hook-client/collection'
|
|
7
|
+
autoload :Channel, 'hook-client/channel'
|
|
8
|
+
autoload :Model, 'hook-client/model'
|
|
9
|
+
|
|
10
|
+
autoload :Extensions, 'hook-client/extensions'
|
|
11
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Hook
|
|
2
|
+
module Channel
|
|
3
|
+
autoload :SSE, 'hook-client/channel/sse'
|
|
4
|
+
autoload :WEBSOCKET, 'hook-client/channel/websocket'
|
|
5
|
+
|
|
6
|
+
def self.create(client, name, options = {})
|
|
7
|
+
channel_klass = (options.delete(:transport) || 'sse').upcase
|
|
8
|
+
collection = Collection.new(name, :channel => true)
|
|
9
|
+
self.const_get(channel_klass).new(client, collection, options)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
require 'http'
|
|
2
|
+
require 'uri'
|
|
3
|
+
|
|
4
|
+
module Hook
|
|
5
|
+
class Client
|
|
6
|
+
class << self
|
|
7
|
+
attr_reader :instance
|
|
8
|
+
def configure(options = {})
|
|
9
|
+
@instance = self.new(options)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize options = {}
|
|
14
|
+
@app_id = options.delete(:app_id)
|
|
15
|
+
@key = options.delete(:key)
|
|
16
|
+
@endpoint = options.delete(:endpoint) || options.delete(:url)
|
|
17
|
+
@endpoint = "#{@endpoint}/" unless @endpoint.end_with? "/"
|
|
18
|
+
|
|
19
|
+
@logger = options.delete(:logger)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def keys
|
|
23
|
+
Keys.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def auth
|
|
27
|
+
Auth.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def files
|
|
31
|
+
Files.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def collection(name)
|
|
35
|
+
Collection.new(:name => name, :client => self)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def logger=(logger)
|
|
39
|
+
@logger = logger
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def channel(name, options = {})
|
|
43
|
+
throw NotImplementedError.new("channels not implemented")
|
|
44
|
+
Channel.create(self, name, options)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def post segments, data
|
|
48
|
+
request :post, segments, data
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def get segments, data = {}
|
|
52
|
+
request :get, segments, data
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def put segments, data
|
|
56
|
+
request :put, segments, data
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def remove segments, data = {}
|
|
60
|
+
request :delete, segments, data
|
|
61
|
+
end
|
|
62
|
+
alias_method :delete, :remove
|
|
63
|
+
|
|
64
|
+
protected
|
|
65
|
+
def request method, segments, data = {}
|
|
66
|
+
response = nil, headers = {
|
|
67
|
+
:accept => 'application/json',
|
|
68
|
+
'X-App-Id' => @app_id,
|
|
69
|
+
'X-App-Key' => @key,
|
|
70
|
+
'User-Agent' => "hook-ruby"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if method == :get
|
|
74
|
+
segments = "#{segments}?#{URI::escape(data.to_json)}"
|
|
75
|
+
data = nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if @logger
|
|
79
|
+
start_time = Time.now
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
response = HTTP.with(headers).send(method, "#{@endpoint}#{segments}", :json => data).to_s
|
|
83
|
+
|
|
84
|
+
if @logger
|
|
85
|
+
@logger.info "#{self.class.name} - #{(Time.now - start_time).round(3)}s - #{method.upcase} #{@endpoint}#{segments}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# If JSON.parse don't suceed, return response as integer
|
|
89
|
+
JSON.parse(response) rescue JSON.parse("{\"value\":#{response}}")['value'] rescue response.to_i
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
module Hook
|
|
2
|
+
class Collection
|
|
3
|
+
|
|
4
|
+
def initialize options = {}
|
|
5
|
+
@name = options.delete(:name)
|
|
6
|
+
@client = options.delete(:client) || Hook::Client.instance
|
|
7
|
+
|
|
8
|
+
segment = options.delete(:channel) ? "channel" : "collection"
|
|
9
|
+
@segments = "#{segment}/#{@name}"
|
|
10
|
+
reset!
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Find item by _id.
|
|
14
|
+
#
|
|
15
|
+
# @param _id [String]
|
|
16
|
+
# @return [Hash]
|
|
17
|
+
def find _id
|
|
18
|
+
if _id.kind_of?(Array)
|
|
19
|
+
self.where(:_id.in => _id).all
|
|
20
|
+
else
|
|
21
|
+
@client.get "#{@segments}/#{_id}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Return only the first result from the database
|
|
26
|
+
def first
|
|
27
|
+
@options[:first] = 1
|
|
28
|
+
self.query!
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Create an item into the collection
|
|
32
|
+
#
|
|
33
|
+
# @param data [Hash, Array] item or array of items
|
|
34
|
+
# @return [Hash, Array]
|
|
35
|
+
def create data
|
|
36
|
+
if data.kind_of?(Array)
|
|
37
|
+
# TODO: server should accept multiple items to create,
|
|
38
|
+
# instead of making multiple requests.
|
|
39
|
+
data.map {|item| self.create(item) }
|
|
40
|
+
else
|
|
41
|
+
@client.post @segments, data
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Retrieve the first item with the given params in the database,
|
|
46
|
+
# if not found this will create one.
|
|
47
|
+
#
|
|
48
|
+
# @param data [Hash] query and data to store
|
|
49
|
+
# @return [Hash]
|
|
50
|
+
def first_or_create data
|
|
51
|
+
@options[:first] = 1
|
|
52
|
+
@options[:data] = data
|
|
53
|
+
@client.post(@segments, self.build_query)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Remove items by id, or by query
|
|
57
|
+
#
|
|
58
|
+
# @param _id [String, nil] _id
|
|
59
|
+
# @return [Hash] status of the operation (ex: {success: true, affected_rows: 7})
|
|
60
|
+
def remove _id = nil
|
|
61
|
+
path = @segments
|
|
62
|
+
path = "#{path}/#{_id}" if _id
|
|
63
|
+
@client.remove(path, build_query)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Remove items by query. This is the same as calling `remove` without `_id` param.
|
|
67
|
+
# @see remove
|
|
68
|
+
def delete_all
|
|
69
|
+
self.remove(nil)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Add where clause to the current query.
|
|
73
|
+
#
|
|
74
|
+
# Supported modifiers on fields: .gt, .gte, .lt, .lte, .ne, .in, .not_in, .nin, .like, .between, .not_between
|
|
75
|
+
#
|
|
76
|
+
# @param fields [Hash] fields and values to filter
|
|
77
|
+
# @param [String] operation (and, or)
|
|
78
|
+
#
|
|
79
|
+
# @example
|
|
80
|
+
# hook.collection(:movies).where({
|
|
81
|
+
# :name => "Hook",
|
|
82
|
+
# :year.gt => 1990
|
|
83
|
+
# })
|
|
84
|
+
#
|
|
85
|
+
# @example Using Range
|
|
86
|
+
# hook.collection(:movies).where({
|
|
87
|
+
# :name.like => "%panic%",
|
|
88
|
+
# :year.between => 1990..2014
|
|
89
|
+
# })
|
|
90
|
+
#
|
|
91
|
+
# @return [Collection] self
|
|
92
|
+
def where fields = {}, operation = 'and'
|
|
93
|
+
fields.each_pair do |k, value|
|
|
94
|
+
field = (k.respond_to?(:field) ? k.field : k).to_s
|
|
95
|
+
comparation = k.respond_to?(:comparation) ? k.comparation : '='
|
|
96
|
+
|
|
97
|
+
# Range syntatic sugar
|
|
98
|
+
value = [ value.first, value.last ] if value.kind_of?(Range)
|
|
99
|
+
|
|
100
|
+
@wheres << [field, comparation, value, operation]
|
|
101
|
+
end
|
|
102
|
+
self
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Add where clause with 'OR' operation to the current query.
|
|
106
|
+
# @param fields [Hash] fields and values to filter
|
|
107
|
+
# @return [Collection] self
|
|
108
|
+
def or_where fields = {}
|
|
109
|
+
self.where(fields, 'or')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Add order clause to the query.
|
|
113
|
+
#
|
|
114
|
+
# @param fields [String] ...
|
|
115
|
+
# @return [Collection] self
|
|
116
|
+
def order fields
|
|
117
|
+
by_num = { 1 => 'asc', -1 => 'desc' }
|
|
118
|
+
ordering = []
|
|
119
|
+
fields.each_pair do |key, value|
|
|
120
|
+
ordering << [key.to_s, by_num[value] || value]
|
|
121
|
+
end
|
|
122
|
+
@ordering = ordering
|
|
123
|
+
self
|
|
124
|
+
end
|
|
125
|
+
alias_method :sort, :order
|
|
126
|
+
|
|
127
|
+
# Limit the number of results to retrieve.
|
|
128
|
+
#
|
|
129
|
+
# @param int [Integer]
|
|
130
|
+
# @return [Collection] self
|
|
131
|
+
def limit int
|
|
132
|
+
@limit = int
|
|
133
|
+
self
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @param int [Integer]
|
|
137
|
+
# @return [Collection] self
|
|
138
|
+
def offset int
|
|
139
|
+
@offset = int
|
|
140
|
+
self
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Run the query, and return all it's results.
|
|
144
|
+
# @yield You may use a block with all returned results
|
|
145
|
+
# @return [Array]
|
|
146
|
+
def all(&block)
|
|
147
|
+
rows = query!
|
|
148
|
+
yield rows if block_given?
|
|
149
|
+
rows
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def query!
|
|
153
|
+
@client.get @segments, build_query
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def method_missing method, *args, &block
|
|
157
|
+
if Enumerator.method_defined? method
|
|
158
|
+
Enumerator.new(self.all).send(method, args, block)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
throw NoMethodError.new("#{self.class.name}: method '#{method}' not found")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Specify the target field names to retrieve
|
|
165
|
+
#
|
|
166
|
+
# @param fields [String] ...
|
|
167
|
+
# @return [Collection] self
|
|
168
|
+
def select *fields
|
|
169
|
+
@options[:select] = fields
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Return related collection's data
|
|
173
|
+
#
|
|
174
|
+
# @param relationships [String] ...
|
|
175
|
+
#
|
|
176
|
+
# @example Retrieving a single relation
|
|
177
|
+
# hook.collection(:books).with(:publisher).each do |book|
|
|
178
|
+
# puts book[:name]
|
|
179
|
+
# puts book[:publisher][:name]
|
|
180
|
+
# end
|
|
181
|
+
#
|
|
182
|
+
# @example Retrieving multiple relations
|
|
183
|
+
# hook.collection(:books).with(:publisher, :author).each do |book|
|
|
184
|
+
# puts book[:name]
|
|
185
|
+
# puts book[:publisher][:name]
|
|
186
|
+
# puts book[:author][:name]
|
|
187
|
+
# end
|
|
188
|
+
#
|
|
189
|
+
# @return [Collection] self
|
|
190
|
+
def with *relationships
|
|
191
|
+
@options[:with] = relationships
|
|
192
|
+
self
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Group query results
|
|
196
|
+
# @param fields [String] ...
|
|
197
|
+
# @return [Collection] self
|
|
198
|
+
def group *fields
|
|
199
|
+
@group = fields
|
|
200
|
+
self
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def count field = '*'
|
|
204
|
+
@options[:aggregation] = { :method => 'count', :field => field }
|
|
205
|
+
self.query!
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def max field
|
|
209
|
+
@options[:aggregation] = { :method => :max, :field => field }
|
|
210
|
+
self.query!
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def min field
|
|
214
|
+
@options[:aggregation] = { :method => :min, :field => field }
|
|
215
|
+
self.query!
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def avg field
|
|
219
|
+
@options[:aggregation] = { :method => :avg, :field => field }
|
|
220
|
+
self.query!
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def sum field
|
|
224
|
+
@options[:aggregation] = { :method => :sum, :field => field }
|
|
225
|
+
self.query!
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def increment field, value = 1
|
|
229
|
+
@options[:operation] = { :method => 'increment', :field => field, :value => value }
|
|
230
|
+
self.query!
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def decrement field, value = 1
|
|
234
|
+
@options[:operation] = { :method => 'decrement', :field => field, :value => value }
|
|
235
|
+
self.query!
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def update _id, data
|
|
239
|
+
@client.post "#{@segments}/#{_id}", data
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def update_all data
|
|
243
|
+
@options[:data] = data
|
|
244
|
+
@client.put @segments, build_query
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def build_query
|
|
248
|
+
query = {}
|
|
249
|
+
query[:limit] = @limit if @limit
|
|
250
|
+
query[:offset] = @offset if @offset
|
|
251
|
+
|
|
252
|
+
query[:q] = @wheres unless @wheres.empty?
|
|
253
|
+
query[:s] = @ordering unless @ordering.empty?
|
|
254
|
+
query[:g] = @group unless @group.empty?
|
|
255
|
+
|
|
256
|
+
{
|
|
257
|
+
:paginate => 'p',
|
|
258
|
+
:first => 'f',
|
|
259
|
+
:aggregation => 'aggr',
|
|
260
|
+
:operation => 'op',
|
|
261
|
+
:data => 'data',
|
|
262
|
+
:with => 'with',
|
|
263
|
+
:select => 'select',
|
|
264
|
+
}.each_pair do |option, shortname|
|
|
265
|
+
query[ shortname ] = @options[ option ] if @options[ option ]
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
self.reset!
|
|
269
|
+
query
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
protected
|
|
273
|
+
|
|
274
|
+
def reset!
|
|
275
|
+
@wheres = []
|
|
276
|
+
@options = {}
|
|
277
|
+
@wheres = []
|
|
278
|
+
@ordering = []
|
|
279
|
+
@group = []
|
|
280
|
+
@limit = nil
|
|
281
|
+
@offset = nil
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
end
|
|
285
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module DL
|
|
2
|
+
module Extensions
|
|
3
|
+
module Symbol
|
|
4
|
+
|
|
5
|
+
{
|
|
6
|
+
:gt => '>',
|
|
7
|
+
:gte => '>=',
|
|
8
|
+
:lt => '<',
|
|
9
|
+
:lte => '<=',
|
|
10
|
+
:ne => '!=',
|
|
11
|
+
:in => 'in',
|
|
12
|
+
:not_in => 'not_in',
|
|
13
|
+
:nin => 'not_in', # alias
|
|
14
|
+
:like => 'like',
|
|
15
|
+
:between => 'between',
|
|
16
|
+
:not_between => 'not_between',
|
|
17
|
+
# :max_distance,
|
|
18
|
+
}.each_pair do |method, comparation|
|
|
19
|
+
define_method(method) do
|
|
20
|
+
OpenStruct.new(:field => self, :comparation => comparation)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
::Symbol.__send__(:include, DL::Extensions::Symbol)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Hook
|
|
2
|
+
class Keys
|
|
3
|
+
|
|
4
|
+
def initialize(options={})
|
|
5
|
+
@client = options.delete(:client) || Hook::Client.instance
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# Return the unserialized value
|
|
9
|
+
#
|
|
10
|
+
# @param key [String, Symbol]
|
|
11
|
+
# @return [Object] value
|
|
12
|
+
def get(key)
|
|
13
|
+
@client.get("key/#{key}")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Store serialized value
|
|
17
|
+
#
|
|
18
|
+
# @param key [String, Symbol] key
|
|
19
|
+
# @param value [Object] JSON serializable object
|
|
20
|
+
# @return [Object] The object you just stored.
|
|
21
|
+
def set(key, value)
|
|
22
|
+
@client.post("key/#{key}", { :value => value });
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
require 'active_model'
|
|
2
|
+
|
|
3
|
+
module Hook
|
|
4
|
+
#
|
|
5
|
+
# Provides ActiveRecord/like methods for querying Collections
|
|
6
|
+
#
|
|
7
|
+
module Model
|
|
8
|
+
def self.included(base)
|
|
9
|
+
base.class_eval do
|
|
10
|
+
extend ClassMethods
|
|
11
|
+
extend ActiveModel::Naming
|
|
12
|
+
extend ActiveModel::Translation
|
|
13
|
+
extend ActiveModel::Callbacks
|
|
14
|
+
include ActiveModel::Validations
|
|
15
|
+
include ActiveModel::Conversion
|
|
16
|
+
include ActiveModel::Dirty
|
|
17
|
+
include ActiveModel::AttributeMethods
|
|
18
|
+
include ActiveModel::Serializers::JSON
|
|
19
|
+
include ActiveModel::Serializers::Xml
|
|
20
|
+
|
|
21
|
+
field :_id
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
module InstanceMethods
|
|
26
|
+
def initialize(attrs = {})
|
|
27
|
+
# is Hook::Client configured?
|
|
28
|
+
throw RuntimeError.new("Please use Hook::Client.configure.") unless Hook::Client.instance
|
|
29
|
+
|
|
30
|
+
@collection = Hook::Client.instance.collection(self.class.collection_name)
|
|
31
|
+
self.attributes = {}
|
|
32
|
+
attrs.each_pair do |name, value|
|
|
33
|
+
self.send(:"#{name}=", value) if self.respond_to?(:"#{name}")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# reset_changes
|
|
37
|
+
changes_applied if self._id
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def save
|
|
41
|
+
return false if !self.changed? || !self.valid?
|
|
42
|
+
|
|
43
|
+
changes_applied
|
|
44
|
+
if self._id.nil?
|
|
45
|
+
self.attributes = @collection.update(self._id, attributes)
|
|
46
|
+
else
|
|
47
|
+
self.attributes = @collection.create(attributes)
|
|
48
|
+
end
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def attributes=(attrs)
|
|
53
|
+
@attributes = attrs
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def attributes
|
|
57
|
+
@attributes
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def inspect
|
|
61
|
+
"#<#{self.class.name}: attributes=#{attributes.inspect}>"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Delegator methods
|
|
65
|
+
def method_missing(m, *args, &block)
|
|
66
|
+
res = @collection.send(m, *args, &block)
|
|
67
|
+
|
|
68
|
+
# Check for success/error responses
|
|
69
|
+
if res.kind_of?(Hash) && res.length == 1
|
|
70
|
+
return res['success'] unless res['success'].nil?
|
|
71
|
+
throw RuntimeError.new(res['error']) unless res['error'].nil?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if (res.kind_of?(Hook::Collection))
|
|
75
|
+
self
|
|
76
|
+
else
|
|
77
|
+
(res.kind_of?(Array)) ? res.map {|r| self.class.new(r) } : self.class.new(res)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
module ClassMethods
|
|
84
|
+
def collection_name(name = nil)
|
|
85
|
+
if name
|
|
86
|
+
@collection_name = name
|
|
87
|
+
else
|
|
88
|
+
@collection_name = ActiveModel::Naming.route_key(self)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def field name, options = {}
|
|
93
|
+
define_attribute_method name
|
|
94
|
+
|
|
95
|
+
# Define getter
|
|
96
|
+
define_method name do
|
|
97
|
+
# self.instance_variable_get("@#{name}")
|
|
98
|
+
attributes[name]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Define setter
|
|
102
|
+
define_method "#{name}=" do |value|
|
|
103
|
+
self.send(:"#{name}_will_change!")
|
|
104
|
+
attributes[name] = value
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def method_missing(m, *args, &block)
|
|
109
|
+
instance = self.new
|
|
110
|
+
instance.send(m, *args, &block)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
include InstanceMethods
|
|
115
|
+
end
|
|
116
|
+
end
|
data/spec/client_spec.rb
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
describe Hook::Collection do
|
|
2
|
+
|
|
3
|
+
subject do
|
|
4
|
+
Hook::Client.instance
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
it "should create new items to collection" do
|
|
8
|
+
user = subject.collection(:users).create(:name => "Endel", :newsletter => true)
|
|
9
|
+
expect(user['name']).to eq("Endel")
|
|
10
|
+
expect(user['newsletter']).to eq(true)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "should query for items" do
|
|
14
|
+
rows = subject.collection(:users).where(:name => "Endel").all
|
|
15
|
+
expect(rows.length).to be > 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "should delete all items" do
|
|
19
|
+
# create a dummy item
|
|
20
|
+
subject.collection(:users).create(:name => "Endel", :newsletter => true)
|
|
21
|
+
# remove every items
|
|
22
|
+
subject.collection(:users).delete_all
|
|
23
|
+
rows = subject.collection(:users).where(:name => "Endel").all
|
|
24
|
+
expect(rows.length).to be == 0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "general methods" do
|
|
28
|
+
subject.collection(:highscores).delete_all
|
|
29
|
+
|
|
30
|
+
one = created = subject.collection(:highscores).create(:player => "One", :score => 50)
|
|
31
|
+
expect(created['player']).to eq("One")
|
|
32
|
+
expect(created['score']).to eq(50)
|
|
33
|
+
|
|
34
|
+
two = subject.collection(:highscores).create(:player => "Two", :score => 100)
|
|
35
|
+
three = subject.collection(:highscores).create(:player => "Three", :score => 25)
|
|
36
|
+
four = subject.collection(:highscores).create(:player => "Four", :score => 10)
|
|
37
|
+
five = subject.collection(:highscores).create(:player => "Five", :score => 150)
|
|
38
|
+
six = subject.collection(:highscores).create(:player => "Six", :score => 125)
|
|
39
|
+
|
|
40
|
+
# find multiple by id
|
|
41
|
+
rows = subject.collection(:highscores).find([two['_id'], three['_id']])
|
|
42
|
+
expect(rows.length).to be == 2
|
|
43
|
+
expect(rows[0]['player']).to be == 'Two'
|
|
44
|
+
expect(rows[1]['player']).to be == 'Three'
|
|
45
|
+
|
|
46
|
+
# .all
|
|
47
|
+
all = subject.collection(:highscores).all
|
|
48
|
+
expect(all.length).to be >= 6
|
|
49
|
+
|
|
50
|
+
# .first
|
|
51
|
+
five = subject.collection(:highscores).where(:player => "Five").first
|
|
52
|
+
expect(five['player']).to eq("Five")
|
|
53
|
+
|
|
54
|
+
# .count
|
|
55
|
+
count = subject.collection(:highscores).where(:score.lt => 25).count
|
|
56
|
+
expect(count).to be == 1
|
|
57
|
+
|
|
58
|
+
count = subject.collection(:highscores).where(:score.lte => 25).count
|
|
59
|
+
expect(count).to be == 2
|
|
60
|
+
|
|
61
|
+
count = subject.collection(:highscores).where(:score.between => 11..149).count
|
|
62
|
+
expect(count).to be == 4
|
|
63
|
+
|
|
64
|
+
count = subject.collection(:highscores).where(:player.ne => "One").count
|
|
65
|
+
expect(count).to be == 5
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
describe Hook::Extensions do
|
|
2
|
+
it "should extend core Ruby classes" do
|
|
3
|
+
gt = :field_gt.gt
|
|
4
|
+
expect(gt.comparation).to eq('>')
|
|
5
|
+
expect(gt.field).to eq(:field_gt)
|
|
6
|
+
|
|
7
|
+
gte = :field_gte.gte
|
|
8
|
+
expect(gte.comparation).to eq('>=')
|
|
9
|
+
expect(gte.field).to eq(:field_gte)
|
|
10
|
+
|
|
11
|
+
lt = :field_lt.lt
|
|
12
|
+
expect(lt.comparation).to eq('<')
|
|
13
|
+
expect(lt.field).to eq(:field_lt)
|
|
14
|
+
|
|
15
|
+
lte = :field_lte.lte
|
|
16
|
+
expect(lte.comparation).to eq('<=')
|
|
17
|
+
expect(lte.field).to eq(:field_lte)
|
|
18
|
+
|
|
19
|
+
ne = :field_ne.ne
|
|
20
|
+
expect(ne.comparation).to eq('!=')
|
|
21
|
+
expect(ne.field).to eq(:field_ne)
|
|
22
|
+
|
|
23
|
+
_in = :field_in.in
|
|
24
|
+
expect(_in.comparation).to eq('in')
|
|
25
|
+
expect(_in.field).to eq(:field_in)
|
|
26
|
+
|
|
27
|
+
not_in = :field_not_in.not_in
|
|
28
|
+
expect(not_in.comparation).to eq('not_in')
|
|
29
|
+
expect(not_in.field).to eq(:field_not_in)
|
|
30
|
+
|
|
31
|
+
nin = :field_nin.nin
|
|
32
|
+
expect(nin.comparation).to eq('not_in')
|
|
33
|
+
expect(nin.field).to eq(:field_nin)
|
|
34
|
+
|
|
35
|
+
like = :field_like.like
|
|
36
|
+
expect(like.comparation).to eq('like')
|
|
37
|
+
expect(like.field).to eq(:field_like)
|
|
38
|
+
|
|
39
|
+
between = :field_between.between
|
|
40
|
+
expect(between.comparation).to eq('between')
|
|
41
|
+
expect(between.field).to eq(:field_between)
|
|
42
|
+
|
|
43
|
+
not_between = :field_not_between.not_between
|
|
44
|
+
expect(not_between.comparation).to eq('not_between')
|
|
45
|
+
expect(not_between.field).to eq(:field_not_between)
|
|
46
|
+
end
|
|
47
|
+
end
|
data/spec/keys_spec.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
describe Hook::Keys do
|
|
2
|
+
|
|
3
|
+
subject do
|
|
4
|
+
Hook::Client.instance
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
it "should get and set keys" do
|
|
8
|
+
expect(subject.keys.get(:inexistent)).to be_nil
|
|
9
|
+
|
|
10
|
+
subject.keys.set :numeric, 12345
|
|
11
|
+
expect(subject.keys.get(:numeric)).to be == 12345
|
|
12
|
+
|
|
13
|
+
subject.keys.set :float, 19.99
|
|
14
|
+
expect(subject.keys.get(:float)).to be == 19.99
|
|
15
|
+
|
|
16
|
+
subject.keys.set :string, 'hello there!'
|
|
17
|
+
expect(subject.keys.get(:string)).to be == 'hello there!'
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
data/spec/model_spec.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
class MyCollection
|
|
2
|
+
include Hook::Model
|
|
3
|
+
field :name
|
|
4
|
+
field :score
|
|
5
|
+
validates_presence_of :score
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class CustomHighscore
|
|
9
|
+
include Hook::Model
|
|
10
|
+
field :name
|
|
11
|
+
field :score
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe Hook::Model do
|
|
15
|
+
it "should respond to activemodel dirty methods" do
|
|
16
|
+
instance = MyCollection.new
|
|
17
|
+
expect(instance.name).to be_nil
|
|
18
|
+
expect(instance.name_changed?).to be == false
|
|
19
|
+
|
|
20
|
+
instance.name = 'Endel'
|
|
21
|
+
expect(instance.name).to be == 'Endel'
|
|
22
|
+
expect(instance.name_changed?).to be == true
|
|
23
|
+
|
|
24
|
+
expect(instance.save).to be == false, "shouldn't save when model has validation errors"
|
|
25
|
+
expect(instance.errors.messages.length).to be == 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "should set default attributes" do
|
|
29
|
+
instance = MyCollection.new(:name => "Endel", :score => 100)
|
|
30
|
+
expect(instance.name).to be == "Endel"
|
|
31
|
+
expect(instance.score).to be == 100
|
|
32
|
+
expect(instance.changed?).to be == true
|
|
33
|
+
instance.save
|
|
34
|
+
expect(instance.changed?).to be == false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "general methods" do
|
|
38
|
+
instance = MyCollection.create(:name => "Endel", :score => 100)
|
|
39
|
+
expect(instance.name).to be == "Endel"
|
|
40
|
+
expect(instance.score).to be == 100
|
|
41
|
+
expect(instance.changed?).to be == false
|
|
42
|
+
expect(instance.save).to be == false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "should create multiple" do
|
|
46
|
+
rows = CustomHighscore.create([
|
|
47
|
+
{:name => "Somebody", :score => 50},
|
|
48
|
+
{:name => "Anybody", :score => 10},
|
|
49
|
+
])
|
|
50
|
+
expect(rows.length).to be == 2
|
|
51
|
+
expect(rows[0].name).to be == "Somebody"
|
|
52
|
+
expect(rows[1].name).to be == "Anybody"
|
|
53
|
+
expect(CustomHighscore.delete_all).to be == 2
|
|
54
|
+
end
|
|
55
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
$: << File.expand_path('../lib')
|
|
2
|
+
require 'hook-client'
|
|
3
|
+
require 'logger'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
logger = Logger.new(STDOUT)
|
|
7
|
+
app = JSON.parse(File.open('spec/app.json').read)
|
|
8
|
+
app_key = app['keys'].select{|k| k['type'] == 'server' }.first
|
|
9
|
+
|
|
10
|
+
Hook::Client.configure(
|
|
11
|
+
:app_id => app_key['app_id'],
|
|
12
|
+
:key => app_key['key'],
|
|
13
|
+
:endpoint => 'http://hook.dev/public/index.php/'
|
|
14
|
+
# :logger => logger
|
|
15
|
+
)
|
metadata
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: hook-client
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Endel Dreyer
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2014-11-08 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rake
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - '>='
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - '>='
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: addressable
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ~>
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.3'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ~>
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.3'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: http
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ~>
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: 0.6.0
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ~>
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: 0.6.0
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - '>='
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: 2.0.0
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - '>='
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: 2.0.0
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: activemodel
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - '>='
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: 3.0.0
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - '>='
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: 3.0.0
|
|
83
|
+
description: Hook Client for Ruby
|
|
84
|
+
email: endel@doubleleft.com
|
|
85
|
+
executables: []
|
|
86
|
+
extensions: []
|
|
87
|
+
extra_rdoc_files: []
|
|
88
|
+
files:
|
|
89
|
+
- lib/hook-client/auth.rb
|
|
90
|
+
- lib/hook-client/channel/sse.rb
|
|
91
|
+
- lib/hook-client/channel/websocket.rb
|
|
92
|
+
- lib/hook-client/channel.rb
|
|
93
|
+
- lib/hook-client/client.rb
|
|
94
|
+
- lib/hook-client/collection.rb
|
|
95
|
+
- lib/hook-client/extensions/symbol.rb
|
|
96
|
+
- lib/hook-client/extensions.rb
|
|
97
|
+
- lib/hook-client/keys.rb
|
|
98
|
+
- lib/hook-client/model.rb
|
|
99
|
+
- lib/hook-client/version.rb
|
|
100
|
+
- lib/hook-client.rb
|
|
101
|
+
- spec/client_spec.rb
|
|
102
|
+
- spec/collection_spec.rb
|
|
103
|
+
- spec/extensions_spec.rb
|
|
104
|
+
- spec/keys_spec.rb
|
|
105
|
+
- spec/model_spec.rb
|
|
106
|
+
- spec/spec_helper.rb
|
|
107
|
+
- README.md
|
|
108
|
+
- LICENSE
|
|
109
|
+
homepage: http://github.com/doubleleft/hook-ruby
|
|
110
|
+
licenses: []
|
|
111
|
+
metadata: {}
|
|
112
|
+
post_install_message:
|
|
113
|
+
rdoc_options: []
|
|
114
|
+
require_paths:
|
|
115
|
+
- lib
|
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
117
|
+
requirements:
|
|
118
|
+
- - '>='
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: '0'
|
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
122
|
+
requirements:
|
|
123
|
+
- - '>='
|
|
124
|
+
- !ruby/object:Gem::Version
|
|
125
|
+
version: '0'
|
|
126
|
+
requirements: []
|
|
127
|
+
rubyforge_project:
|
|
128
|
+
rubygems_version: 2.0.6
|
|
129
|
+
signing_key:
|
|
130
|
+
specification_version: 4
|
|
131
|
+
summary: Hook Ruby Client
|
|
132
|
+
test_files: []
|
|
133
|
+
has_rdoc:
|