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 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
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
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,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -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
@@ -0,0 +1,6 @@
1
+ module Gom
2
+ class Client
3
+ VERSION = '0.0.2'
4
+ DATE = '2013-08-15'
5
+ end
6
+ 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