gom-client 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.travis.yml +17 -0
- data/Gemfile +2 -0
- data/Guardfile +8 -0
- data/README.md +146 -0
- data/Rakefile +2 -0
- data/gom-client-ruby.gemspec +36 -0
- data/lib/gom/client/version.rb +6 -0
- data/lib/gom/client.rb +226 -0
- data/spec/fixtures/vcr_cassettes/127_0_0_1_3000.yml +4469 -0
- data/spec/fixtures/vcr_cassettes/gom_dev_artcom_de.yml +243 -0
- data/spec/lib/gom/client/version_spec.rb +8 -0
- data/spec/lib/gom/client_spec.rb +306 -0
- data/spec/spec_helper.rb +43 -0
- metadata +173 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 47c079f7884f7690f09f7ce8c4c0a50bfc65e38e
|
4
|
+
data.tar.gz: 6231c9a90c47c9fa3f791b350e99e3219bbcd018
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e0c2f0fefd4cd006b1dad046c576ba8cd1e1720a763a502c062c8629ff18ab42ef30ef26ab019ca7e81bfc102333b36560eaa5af6adc1ad4f558f8eb474e064e
|
7
|
+
data.tar.gz: 63e505910da3384fca83820a086ac192de61280f1e1f3a397941bcbf5aa02522a6221a6fd938cff1e0011a9b9704abbf70ce074affa7037a1d8d1f8d6b571017
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
coverage
|
6
|
+
InstalledFiles
|
7
|
+
lib/bundler/man
|
8
|
+
pkg
|
9
|
+
rdoc
|
10
|
+
spec/reports
|
11
|
+
test/tmp
|
12
|
+
test/version_tmp
|
13
|
+
tmp
|
14
|
+
|
15
|
+
# YARD artifacts
|
16
|
+
.yardoc
|
17
|
+
_yardoc
|
18
|
+
doc/
|
19
|
+
|
20
|
+
## PROJECT::SPECIFIC
|
21
|
+
README.html
|
22
|
+
Gemfile.lock
|
data/.travis.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
language: ruby
|
2
|
+
bundler_args: --without development
|
3
|
+
before_script:
|
4
|
+
script: "bundle exec rspec"
|
5
|
+
env:
|
6
|
+
- CI=true
|
7
|
+
rvm:
|
8
|
+
- 1.9.3
|
9
|
+
- 2.0.0
|
10
|
+
gemfile:
|
11
|
+
- Gemfile
|
12
|
+
notifications:
|
13
|
+
recipients:
|
14
|
+
- dirk.luesebrink@artcom.com
|
15
|
+
branches:
|
16
|
+
only:
|
17
|
+
- webmock
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# Guardfile info at: https://github.com/guard/guard#readme
|
2
|
+
|
3
|
+
guard 'rspec', all_on_start: true, cli: '--format nested --debug --color' do
|
4
|
+
watch(%r{^spec/.+_spec\.rb$})
|
5
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
6
|
+
watch('spec/spec_helper.rb') { "spec" }
|
7
|
+
watch('lib/gom/client.rb') { "spec" }
|
8
|
+
end
|
data/README.md
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
# Ruby Gom Client
|
2
|
+
|
3
|
+
## Requirements
|
4
|
+
|
5
|
+
* Ruby 1.9
|
6
|
+
|
7
|
+
## Setup and Initialization
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
GOM = Gom::Client.new GOM_ROOT
|
11
|
+
```
|
12
|
+
|
13
|
+
Where the GOM_ROOT is of format `"http://<ip | hostname>[:<port>]"`. Further
|
14
|
+
operations are then performed via the initialized object (in this example
|
15
|
+
`GOM`)
|
16
|
+
|
17
|
+
## RESTful operations
|
18
|
+
|
19
|
+
### GET/retrieve
|
20
|
+
|
21
|
+
Attribute retrieval:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
>>> myAttribute = GOM.retrieve("/test:myAttr")
|
25
|
+
{'attribute': {'ctime': '2012-10-12T08:46:48+02:00',
|
26
|
+
'mtime': '2012-10-12T08:46:48+02:00',
|
27
|
+
'name': 'myAttr',
|
28
|
+
'node': '/test',
|
29
|
+
'type': 'string',
|
30
|
+
'value': 'test'}}
|
31
|
+
```
|
32
|
+
|
33
|
+
Node retrieval:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
>>> myNode = GOM.retrieve("/areas")
|
37
|
+
{'node': {'ctime': '2012-09-20T04:51:56+02:00',
|
38
|
+
'entries': [{'ctime': '2012-07-30T16:13:02+02:00',
|
39
|
+
'mtime': '2012-07-30T16:13:02+02:00',
|
40
|
+
'node': '/areas/home'},
|
41
|
+
{'ctime': '2012-09-29T17:51:47+02:00',
|
42
|
+
'mtime': '2012-09-29T17:51:47+02:00',
|
43
|
+
'node': '/areas/life'},
|
44
|
+
{'ctime': '2012-06-26T21:13:35+02:00',
|
45
|
+
'mtime': '2012-06-26T21:13:35+02:00',
|
46
|
+
'node': '/areas/mobile'},
|
47
|
+
{'ctime': '2012-10-10T18:30:50+02:00',
|
48
|
+
'mtime': '2012-10-10T18:30:50+02:00',
|
49
|
+
'node': '/areas/move'},
|
50
|
+
{'ctime': '2012-09-20T02:19:30+02:00',
|
51
|
+
'mtime': '2012-09-20T02:19:30+02:00',
|
52
|
+
'node': '/areas/pre-show'},
|
53
|
+
{'ctime': '2012-07-30T14:03:57+02:00',
|
54
|
+
'mtime': '2012-07-30T14:03:57+02:00',
|
55
|
+
'node': '/areas/welcome'},
|
56
|
+
{'attribute': {'ctime': '2012-10-11T07:02:18+02:00',
|
57
|
+
'mtime': '2012-10-11T07:02:18+02:00',
|
58
|
+
'name': 'operational_mode',
|
59
|
+
'node': '/areas',
|
60
|
+
'type': 'string',
|
61
|
+
'value': 'idle'}}],
|
62
|
+
'mtime': '2012-09-20T04:51:56+02:00',
|
63
|
+
'uri': '/areas'}}
|
64
|
+
```
|
65
|
+
|
66
|
+
Retrieval of non-existent Node/Attribute:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
>>> pprint(GOM.retrieve("/test/does-not-exist"))
|
70
|
+
None
|
71
|
+
>>> pprint(GOM.retrieve("/test:does-not-exist"))
|
72
|
+
None
|
73
|
+
```
|
74
|
+
|
75
|
+
### PUT/update
|
76
|
+
|
77
|
+
Attribute update
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
>>> GOM.update!("/test:temperature", "50 °C")
|
81
|
+
'50 °C'
|
82
|
+
```
|
83
|
+
|
84
|
+
Node update
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
>>> GOM.update!("/test/weather", {"temperature": "50 °C", "wind_velocity": "3 km/h", "wind_direction": "NNW"})
|
88
|
+
{'status': 201}
|
89
|
+
```
|
90
|
+
|
91
|
+
### DELETE/destroy
|
92
|
+
|
93
|
+
Delete existing node
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
>>> GOM.destroy("/test/c18bf546-e577-414a-92d2-2ebdfb69b4f6")
|
97
|
+
True
|
98
|
+
```
|
99
|
+
|
100
|
+
Delete non-existing node
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
>>> print(GOM.destroy("/test/does-not-exist"))
|
104
|
+
None
|
105
|
+
```
|
106
|
+
|
107
|
+
Attributes are deleted accordingly
|
108
|
+
|
109
|
+
### POST/create
|
110
|
+
|
111
|
+
Create empty node
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
>>> GOM.create!("/test")
|
115
|
+
'/test/c18bf546-e577-414a-92d2-2ebdfb69b4f6'
|
116
|
+
```
|
117
|
+
|
118
|
+
Create node with attributes
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
>>> GOM.create!("/test", {"name":"Hans", "profession": "Lumberjack"})
|
122
|
+
'/test/419e9db0-2800-43ed-9053-edaafd4f60b3'
|
123
|
+
>>> GOM.retrieve("/test/419e9db0-2800-43ed-9053-edaafd4f60b3")
|
124
|
+
{'node': {'ctime': '2012-10-12T10:43:25+02:00',
|
125
|
+
'entries': [{'attribute': {'ctime': '2012-10-12T10:43:25+02:00',
|
126
|
+
'mtime': '2012-10-12T10:43:25+02:00',
|
127
|
+
'name': 'name',
|
128
|
+
'node': '/test/419e9db0-2800-43ed-9053-edaafd4f60b3',
|
129
|
+
'type': 'string',
|
130
|
+
'value': 'Hans'}},
|
131
|
+
{'attribute': {'ctime': '2012-10-12T10:43:25+02:00',
|
132
|
+
'mtime': '2012-10-12T10:43:25+02:00',
|
133
|
+
'name': 'profession',
|
134
|
+
'node': '/test/419e9db0-2800-43ed-9053-edaafd4f60b3',
|
135
|
+
'type': 'string',
|
136
|
+
'value': 'Lumberjack'}}],
|
137
|
+
'mtime': '2012-10-12T10:43:25+02:00',
|
138
|
+
'uri': '/test/419e9db0-2800-43ed-9053-edaafd4f60b3'}}
|
139
|
+
```
|
140
|
+
|
141
|
+
## Packaging
|
142
|
+
|
143
|
+
## TODO
|
144
|
+
|
145
|
+
* Document script runner (run_script)
|
146
|
+
* Document gom observer creation (register_observer)
|
data/Rakefile
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
# -*- encoding: utf-8 -*-
|
4
|
+
$:.push File.expand_path("../lib", __FILE__)
|
5
|
+
require 'gom/client/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = 'gom-client'
|
9
|
+
s.version = Gom::Client::VERSION
|
10
|
+
s.date = Gom::Client::DATE
|
11
|
+
s.authors = "ART+COM"
|
12
|
+
s.homepage = 'http://www.artcom.de/'
|
13
|
+
s.summary = 'REST client for the gom HTTP API'
|
14
|
+
|
15
|
+
s.add_dependency 'json'
|
16
|
+
|
17
|
+
s.add_development_dependency 'rspec'
|
18
|
+
s.add_development_dependency 'guard'
|
19
|
+
s.add_development_dependency 'guard-rspec'
|
20
|
+
s.add_development_dependency 'rb-fsevent', '~>0.9.1'
|
21
|
+
s.add_development_dependency 'webmock'
|
22
|
+
s.add_development_dependency 'vcr'
|
23
|
+
|
24
|
+
if RUBY_PLATFORM.match /java/i
|
25
|
+
s.add_development_dependency 'ruby-debug'
|
26
|
+
else
|
27
|
+
s.add_development_dependency 'debugger'
|
28
|
+
end
|
29
|
+
|
30
|
+
s.files = `git ls-files`.split("\n")
|
31
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
32
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map do |f|
|
33
|
+
File.basename(f)
|
34
|
+
end
|
35
|
+
s.require_paths = ["lib"]
|
36
|
+
end
|
data/lib/gom/client.rb
ADDED
@@ -0,0 +1,226 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/http'
|
3
|
+
#require 'active_support'
|
4
|
+
require 'rexml/document'
|
5
|
+
require 'gom/client/version'
|
6
|
+
require 'json' #/pure'
|
7
|
+
|
8
|
+
module Gom
|
9
|
+
|
10
|
+
class HttpError < StandardError
|
11
|
+
attr_reader :response, :description
|
12
|
+
|
13
|
+
def initialize(http_response, description)
|
14
|
+
@response = http_response
|
15
|
+
@description = description
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
"#{@response.code} #{@response.message} #{@description}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Client
|
24
|
+
attr_reader :root
|
25
|
+
|
26
|
+
def initialize(gom_root = 'http://gom')
|
27
|
+
@root = URI.parse(gom_root)
|
28
|
+
end
|
29
|
+
|
30
|
+
# returns nil but NOT raise error on non-existing attributes
|
31
|
+
def retrieve_val(path_or_uri)
|
32
|
+
(a = retrieve(path_or_uri)) && a[:attribute][:value]
|
33
|
+
end
|
34
|
+
|
35
|
+
def retrieve(path_or_uri, redirect_limit=10)
|
36
|
+
self.retrieve! path_or_uri, redirect_limit
|
37
|
+
rescue Gom::HttpError => e
|
38
|
+
return nil if e.response.code == "404"
|
39
|
+
raise e
|
40
|
+
end
|
41
|
+
|
42
|
+
def retrieve!(path_or_uri, redirect_limit=10)
|
43
|
+
uri = path_or_uri.kind_of?(URI) ? path_or_uri : URI.parse("#{@root}#{path_or_uri}")
|
44
|
+
response = Net::HTTP.new(uri.host, uri.port).request_get(uri.path, {"Accept" => "application/json" })
|
45
|
+
|
46
|
+
begin
|
47
|
+
if response.kind_of?(Net::HTTPSuccess)
|
48
|
+
return JSON.parse(response.body, symbolize_names: true)
|
49
|
+
end
|
50
|
+
rescue StandardError => e
|
51
|
+
raise HttpError.new(response, "#{e} -- could not parse body: '#{response.body}'")
|
52
|
+
end
|
53
|
+
|
54
|
+
if (redirect_limit == 0 && response.kind_of?(Net::HTTPRedirection))
|
55
|
+
raise "too many redirects"
|
56
|
+
end
|
57
|
+
|
58
|
+
if response.kind_of?(Net::HTTPRedirection)
|
59
|
+
return Gom::retrieve(response['location'], redirect_limit - 1)
|
60
|
+
end
|
61
|
+
|
62
|
+
raise HttpError.new(response, "while GETting #{uri.path}")
|
63
|
+
end
|
64
|
+
|
65
|
+
# GOM API Change: Destroy will NOT return error on non-existing attributes
|
66
|
+
# or nodes. So no error will every be raised from this methods
|
67
|
+
def destroy!(path_or_uri)
|
68
|
+
uri = path_or_uri.kind_of?(URI) ? path_or_uri : URI.parse("#{@root}#{path_or_uri}")
|
69
|
+
response = Net::HTTP.new(uri.host, uri.port).delete(uri.path)
|
70
|
+
|
71
|
+
return true if response.kind_of?(Net::HTTPSuccess)
|
72
|
+
raise HttpError.new(response, "while DELETEing #{uri.path}")
|
73
|
+
end
|
74
|
+
|
75
|
+
def destroy(uri)
|
76
|
+
self.destroy! uri
|
77
|
+
rescue Gom::HttpError => e
|
78
|
+
return nil if e.response.code == "404"
|
79
|
+
raise e
|
80
|
+
end
|
81
|
+
|
82
|
+
def create!(path_or_uri, attributes={})
|
83
|
+
uri = path_or_uri.kind_of?(URI) ? path_or_uri : URI.parse("#{@root}#{path_or_uri}")
|
84
|
+
request_body = attributes_to_xml attributes
|
85
|
+
headers = { 'Content-Type' => 'application/xml' }
|
86
|
+
response = Net::HTTP.new(uri.host, uri.port).request_post(uri.path, request_body, headers)
|
87
|
+
|
88
|
+
if response.kind_of?(Net::HTTPRedirection)
|
89
|
+
return URI.parse(response['location']).path
|
90
|
+
end
|
91
|
+
|
92
|
+
raise HttpError.new(response, "while CREATEing (#{uri.path})")
|
93
|
+
end
|
94
|
+
|
95
|
+
def update!(path_or_uri, hash_or_text=nil)
|
96
|
+
uri = path_or_uri.kind_of?(URI) ? path_or_uri : URI.parse("#{@root}#{path_or_uri}")
|
97
|
+
|
98
|
+
if is_attribute?(uri.path)
|
99
|
+
raise "update attribute call must include value" if hash_or_text.nil?
|
100
|
+
raise "update attribute value must be a string" unless hash_or_text.kind_of?(String)
|
101
|
+
response_format = "text/plain"
|
102
|
+
if hash_or_text != ""
|
103
|
+
doc = REXML::Document.new
|
104
|
+
attr_node = doc.add_element 'attribute'
|
105
|
+
attr_node.attributes['type'] = 'string';
|
106
|
+
attr_node.text = hash_or_text
|
107
|
+
request_body = doc.to_s
|
108
|
+
headers = { 'Content-Type' => 'application/xml',
|
109
|
+
'Accept' => response_format }
|
110
|
+
else
|
111
|
+
request_body = "attribute=#{hash_or_text}&type=string"
|
112
|
+
headers = { 'Content-Type' => 'application/x-www-form-urlencoded',
|
113
|
+
'Accept' => response_format }
|
114
|
+
end
|
115
|
+
else
|
116
|
+
raise "update node values must be a hash of attributes" unless hash_or_text.nil? or hash_or_text.kind_of?(Hash)
|
117
|
+
request_body = attributes_to_xml hash_or_text || {}
|
118
|
+
response_format = "application/json"
|
119
|
+
headers = { 'Content-Type' => 'application/xml',
|
120
|
+
'Accept' => response_format }
|
121
|
+
end
|
122
|
+
|
123
|
+
#headers = { 'Content-Type' => 'application/xml',
|
124
|
+
# 'Accept' => response_format }
|
125
|
+
|
126
|
+
response = Net::HTTP.new(uri.host, uri.port).request_put(uri.path, request_body, headers)
|
127
|
+
|
128
|
+
return response.body if response.kind_of?(Net::HTTPSuccess) && response_format == "text/plain"
|
129
|
+
if response.kind_of?(Net::HTTPSuccess) && response_format == "application/json"
|
130
|
+
return JSON.parse(response.body, symbolize_names: true)
|
131
|
+
end
|
132
|
+
raise HttpError.new(response, "while PUTting #{uri.path}")
|
133
|
+
end
|
134
|
+
alias update update!
|
135
|
+
|
136
|
+
# supports stored and posted scripts
|
137
|
+
def run_script(options = {})
|
138
|
+
path = options[:path] || nil
|
139
|
+
script = options[:script] || nil
|
140
|
+
params = options[:params] || {}
|
141
|
+
|
142
|
+
if path.nil?
|
143
|
+
script.nil? and (raise ArgumentError, "must provide script OR path")
|
144
|
+
else
|
145
|
+
script and (raise ArgumentError, "must not provide script AND path")
|
146
|
+
params[:_script_path] = path
|
147
|
+
end
|
148
|
+
|
149
|
+
params = params.keys.zip(params.values).map {|k,v| "#{k}=#{v}"}
|
150
|
+
url = URI.parse("#{@root}/gom/script/v8?#{params.join('&')}")
|
151
|
+
|
152
|
+
response = Net::HTTP.start(url.host, url.port) do |http|
|
153
|
+
if script
|
154
|
+
request = Net::HTTP::Post.new(url.to_s)
|
155
|
+
request.set_content_type "text/javascript"
|
156
|
+
http.request(request, script)
|
157
|
+
else
|
158
|
+
request = Net::HTTP::Get.new(url.to_s)
|
159
|
+
http.request(request)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
if response.kind_of?(Net::HTTPSuccess)
|
164
|
+
return response
|
165
|
+
else
|
166
|
+
raise HttpError.new(
|
167
|
+
response, "while executing server-side-script:\n#{response.body}"
|
168
|
+
)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# supports anonymous and named observers
|
173
|
+
def register_observer(options={})
|
174
|
+
name = options[:name] || nil
|
175
|
+
callback_url = options[:callback_url] || nil
|
176
|
+
node = options[:node] || nil
|
177
|
+
filters = options[:filters] || {}
|
178
|
+
format = options[:format] || "application/json"
|
179
|
+
|
180
|
+
callback_url.nil? and (raise ArgumentError, "callback_url must not be nil")
|
181
|
+
node.nil? and (raise ArgumentError, "node must not be nil")
|
182
|
+
unless ['application/json', 'application/xml'].include?(format)
|
183
|
+
raise ArgumentError, "invalid format: '#{format}'"
|
184
|
+
end
|
185
|
+
|
186
|
+
url = URI.parse("#{@root}/gom/observer#{node}")
|
187
|
+
form_data = { 'callback_url' => callback_url,
|
188
|
+
'accept' => format }
|
189
|
+
form_data.merge!(filters)
|
190
|
+
|
191
|
+
if name
|
192
|
+
observer_url = url.path.gsub(/\:/,'/')
|
193
|
+
my_uri = "#{observer_url}/.#{name}"
|
194
|
+
destroy my_uri
|
195
|
+
|
196
|
+
form_data['observed_uri'] = "#{node}"
|
197
|
+
update(my_uri, form_data)
|
198
|
+
else
|
199
|
+
request = Net::HTTP::Post.new(url.path)
|
200
|
+
request.set_form_data(form_data)
|
201
|
+
response = Net::HTTP.new(url.host, url.port).start do |http|
|
202
|
+
http.request(request)
|
203
|
+
end
|
204
|
+
my_uri = URI.parse(response['location']).path
|
205
|
+
end
|
206
|
+
my_uri
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
|
211
|
+
def is_attribute?(the_path)
|
212
|
+
not the_path.index(":").nil?
|
213
|
+
end
|
214
|
+
|
215
|
+
def attributes_to_xml(hash={})
|
216
|
+
doc = REXML::Document.new
|
217
|
+
node = doc.add_element 'node'
|
218
|
+
hash.each do |key, value|
|
219
|
+
attrib = node.add_element 'attribute'
|
220
|
+
attrib.attributes['name'] = key
|
221
|
+
attrib.text = value
|
222
|
+
end
|
223
|
+
doc.to_s
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|