gom-client 0.0.2
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/.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
|