hook-client 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 ![Build status](https://travis-ci.org/doubleleft/hook-ruby.svg?branch=master)
|
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:
|