innards 0.1.0.pre → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 78f09776ea90992633e40d2c52ac744da8581a1a
4
+ data.tar.gz: 8e2dff4dfd83d89917b53399ff3053ce2eaedf1d
5
+ SHA512:
6
+ metadata.gz: 567b5f8303e8335ad359a124458702e377ba1bdfc1401442e5890eba95360b6033af2d1d65baa1796f52330edcd80ec1f7e8a1e75bc164f4ea408067fc1571ab
7
+ data.tar.gz: 830e80d3e29b02391809c574921e0dcf206643e6e2fe355e739e3688aa8448757dc14a09d704d5d1941e30d176137ec29250829f89f4685ccb9365c69d6ad803
@@ -0,0 +1,21 @@
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
+ *.komodoproject
21
+ test.rb
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,9 @@
1
+ rvm:
2
+ - 1.9.2
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - rbx-19mode
6
+ - ruby-head
7
+ matrix:
8
+ allow_failures:
9
+ - rvm: ruby-head
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+ gemspec
3
+
4
+ gem 'coveralls', :require => false
@@ -0,0 +1,49 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ innards (0.1.0)
5
+ excon (~> 0.25.1)
6
+ multipart-parser (= 0.1.1)
7
+ ox (~> 2.0.5)
8
+
9
+ GEM
10
+ remote: http://rubygems.org/
11
+ specs:
12
+ colorize (0.5.8)
13
+ coveralls (0.6.7)
14
+ colorize
15
+ multi_json (~> 1.3)
16
+ rest-client
17
+ simplecov (>= 0.7)
18
+ thor
19
+ diff-lcs (1.2.4)
20
+ excon (0.25.3)
21
+ mime-types (1.23)
22
+ multi_json (1.7.7)
23
+ multipart-parser (0.1.1)
24
+ ox (2.0.6)
25
+ rake (10.1.0)
26
+ rest-client (1.6.7)
27
+ mime-types (>= 1.16)
28
+ rspec (2.14.1)
29
+ rspec-core (~> 2.14.0)
30
+ rspec-expectations (~> 2.14.0)
31
+ rspec-mocks (~> 2.14.0)
32
+ rspec-core (2.14.4)
33
+ rspec-expectations (2.14.0)
34
+ diff-lcs (>= 1.1.3, < 2.0)
35
+ rspec-mocks (2.14.1)
36
+ simplecov (0.7.1)
37
+ multi_json (~> 1.0)
38
+ simplecov-html (~> 0.7.1)
39
+ simplecov-html (0.7.1)
40
+ thor (0.18.1)
41
+
42
+ PLATFORMS
43
+ ruby
44
+
45
+ DEPENDENCIES
46
+ coveralls
47
+ innards!
48
+ rake
49
+ rspec
data/README.md CHANGED
@@ -1,25 +1,45 @@
1
+ [![Gem Version](https://badge.fury.io/rb/innards.png)](http://badge.fury.io/rb/innards)
1
2
  [![Dependency Status](https://gemnasium.com/TurboRETS/innards.png)](https://gemnasium.com/TurboRETS/innards)
2
- [![Build Status](https://drone.io/github.com/TurboRETS/innards/status.png)](https://drone.io/github.com/TurboRETS/innards/latest)
3
+ [![Build Status](https://travis-ci.org/TurboRETS/innards.png?branch=master)](https://travis-ci.org/TurboRETS/innards)
3
4
  [![Code Climate](https://codeclimate.com/github/TurboRETS/innards.png)](https://codeclimate.com/github/TurboRETS/innards)
5
+ [![Coverage Status](https://coveralls.io/repos/TurboRETS/innards/badge.png)](https://coveralls.io/r/TurboRETS/innards)
6
+ [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/TurboRETS/innards/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
4
7
 
5
8
  TurboRETS Innards
6
9
  =================
7
10
 
8
11
  Gem for accessing RETS Servers
9
12
 
10
- TODO
13
+ Current Status
11
14
  -
12
15
  - [X] RETS Server Login
13
- - [ ] Fetch Metadata
14
- - [ ] Fetch Resource/Class Data
15
- - [ ] GetObject
16
+ - [X] Fetch Metadata
17
+ - [X] Fetch Resource/Class Data
18
+ - [X] GetObject
19
+ - [ ] RETS 1.7.x User Agent Authentication
16
20
 
17
- Premature Usage Example
21
+ Usage Example
18
22
  -
19
23
  ```ruby
20
24
  require 'innards'
25
+
21
26
  conn = Innards::Connection.new(:login_url => "http://Joe:Schmoe@www.dis.com:6103/rets/login")
22
27
  conn.login!
28
+
29
+ # Fetch metadata
30
+ metadata = conn.get_metadata
31
+
32
+ # Perform a Search
33
+ results = conn.search :SearchType => "Property",
34
+ :Class => "RES",
35
+ :Query => "(ListPrice=0+)"
36
+
37
+ # getObject
38
+ objects = conn.get_object :Type => "Photo",
39
+ :Resource => "Property",
40
+ :ID => "5:*"
41
+
42
+ con.logout!
23
43
  ```
24
44
 
25
45
  License
data/Rakefile CHANGED
@@ -9,4 +9,6 @@ RSpec::Core::RakeTask.new("spec") do |spec|
9
9
  spec.pattern = "spec/**/*_spec.rb"
10
10
  end
11
11
 
12
+ Dir.glob('tasks/*.rake').each {|r| import r}
13
+
12
14
  task :default => :spec
@@ -0,0 +1,26 @@
1
+ require File.expand_path('../lib/innards/version', __FILE__)
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = "innards"
5
+ gem.version = Innards::Version::STRING
6
+ gem.platform = Gem::Platform::RUBY
7
+ gem.authors = ["Paul Trippett"]
8
+ gem.email = ["paul@turborets.com"]
9
+ gem.homepage = "http://github.com/TurboRETS/innards"
10
+ gem.summary = "RETS Library for Ruby"
11
+ gem.description = "Innards; the internal parts especially of a structure or mechanism; in our case it's the core RETS library for TurboRETS"
12
+
13
+ gem.license = "AGPL"
14
+
15
+ gem.required_rubygems_version = ">= 1.3.6"
16
+ gem.rubyforge_project = "innards"
17
+
18
+ gem.add_runtime_dependency "ox", "~>2.0.5"
19
+ gem.add_runtime_dependency "excon", "~>0.25.1"
20
+ gem.add_runtime_dependency "multipart-parser", "0.1.1"
21
+ gem.add_development_dependency "rake"
22
+ gem.add_development_dependency "rspec"
23
+
24
+ gem.files = `git ls-files`.split($\)
25
+ gem.require_path = "lib"
26
+ end
@@ -4,4 +4,12 @@ end
4
4
  path = File.expand_path("../innards", __FILE__)
5
5
  require "#{path}/version"
6
6
  require "#{path}/exceptions"
7
- require "#{path}/connection"
7
+ require "#{path}/digest_handler"
8
+ require "#{path}/cookie_handler"
9
+ require "#{path}/multipart_handler"
10
+ require "#{path}/connection"
11
+ require "#{path}/parsers/parser_base"
12
+ require "#{path}/parsers/login_parser"
13
+ require "#{path}/parsers/logout_parser"
14
+ require "#{path}/parsers/get_metadata_parser"
15
+ require "#{path}/parsers/search_parser"
@@ -1,166 +1,255 @@
1
1
  require 'uri'
2
2
  require 'excon'
3
3
  require 'digest/md5'
4
+ require 'ox'
4
5
 
5
6
  module Innards
7
+
8
+ # Creates a connection to a RETS Server managing any required
9
+ # authentication
6
10
  class Connection
7
-
11
+
8
12
  include Innards::Exceptions
9
-
13
+
10
14
  VALID_RETS_VERSIONS = [
11
15
  "1.5",
12
16
  "1.7",
13
17
  "1.7.2",
14
18
  "1.8"
15
19
  ]
16
-
20
+
17
21
  VALID_AUTH_MODES = [
18
22
  "digest",
19
23
  "basic"
20
24
  ]
21
-
25
+
22
26
  DEFAULT_PARAMS = {
23
27
  :login_url => "",
24
- :login_uri => nil,
25
- :proxy_uri => nil,
26
28
  :user_agent => "innards/v#{Innards::Version::STRING}",
27
29
  :rets_version => "1.7.2",
28
30
  :auth_mode => "digest",
31
+ }
32
+
33
+ MASTER_PARAMS = {
34
+ :request_count => 0,
29
35
  :cookies => [],
30
36
  :digest => {},
31
- :mock => false
37
+ :login_uri => nil,
38
+ :proxy_uri => nil
32
39
  }
33
-
40
+
41
+ DEFAULT_SEARCH_OPTIONS = {
42
+ :SearchType => "",
43
+ :Class => "",
44
+ :Query => "",
45
+ :Format => "COMPACT-DECODED",
46
+ :QueryType => "DMQL2",
47
+ :StandardNames => "0",
48
+ :Limit => "NONE"
49
+ }
50
+
51
+ DEFAULT_GETOBJECT_OPTIONS = {
52
+ :Type => "",
53
+ :Resource => "",
54
+ :ID => ""
55
+ }
56
+
57
+ attr_reader :params
58
+
34
59
  def initialize(params = {})
35
-
36
- @params = DEFAULT_PARAMS.merge(params)
37
-
38
- raise ArgumentError.new("invalid url [:login_url]") unless has_valid_url?
39
- raise ArgumentError.new("invalid rets version [:rets_version]") unless has_valid_rets_version?
40
- raise ArgumentError.new("invalid auth mode [:auth_mode]") unless has_valid_auth_mode?
41
- raise ArgumentError.new("invalid user agent [:user_agent]") unless has_valid_user_agent?
42
-
60
+
61
+ @params = DEFAULT_PARAMS.merge(params).merge(MASTER_PARAMS)
62
+
63
+ @digest_handler = DigestHandler.new @params
64
+ @cookie_handler = CookieHandler.new @params
65
+
66
+ raise InvalidUrlException.new unless has_valid_url?
67
+ raise InvalidRetsVersionException.new unless has_valid_rets_version?
68
+ raise InvalidAuthModeException.new unless has_valid_auth_mode?
69
+ raise InvalidUserAgentException.new unless has_valid_user_agent?
70
+
43
71
  end
44
-
72
+
45
73
  def login!
46
- tries ||= 3
74
+
47
75
  @params[:login_uri] = URI(@params[:login_url])
48
76
  @params[:current_request] = @params[:login_uri].path
49
- make_request_to @params[:current_request]
50
- true
51
- rescue AuthenticationRequiredException => e
52
- retry unless (tries -= 1).zero?
53
- false
77
+
78
+ response = make_request_to(@params[:current_request])
79
+ response_io = StringIO.new(response.body)
80
+
81
+ parser = Innards::Parsers::LoginParser.new
82
+ Ox.sax_parse(parser, response_io)
83
+
84
+ if parser.valid_rets_response_received?
85
+ params[:metadata] = parser.metadata
86
+ true
87
+ else
88
+ false
89
+ end
90
+
91
+ end
92
+
93
+ def logout!
94
+ require_login!
95
+
96
+ response = make_request_to(@params[:metadata][:Logout])
97
+ response_io = StringIO.new(response.body)
98
+
99
+ parser = Innards::Parsers::LogoutParser.new
100
+ Ox.sax_parse(parser, response_io)
101
+
102
+ if parser.valid_rets_response_received?
103
+ params[:metadata] = parser.metadata
104
+ true
105
+ else
106
+ false
107
+ end
108
+ end
109
+
110
+ def get_metadata options = {}
111
+ require_login!
112
+
113
+ options = {
114
+ :Type => "METADATA-SYSTEM",
115
+ :ID => "*",
116
+ :Format => "COMPACT"
117
+ }.merge(options)
118
+
119
+ response = make_request_to(@params[:metadata][:GetMetadata],
120
+ :query => options)
121
+ response_io = StringIO.new(response.body)
122
+
123
+ parser = Innards::Parsers::GetMetadataParser.new
124
+ Ox.sax_parse(parser, response_io)
125
+
126
+ if parser.valid_rets_response_received?
127
+ parser.metadata
128
+ else
129
+ false
130
+ end
131
+ end
132
+
133
+ def search options = {}
134
+ require_login!
135
+
136
+ options = DEFAULT_SEARCH_OPTIONS.merge(options)
137
+
138
+ raise InvalidSearchTypeException.new if options[:SearchType].empty?
139
+ raise InvalidClassException.new if options[:Class].empty?
140
+ raise InvalidQueryException.new if options[:Query].empty?
141
+
142
+ response = make_request_to(@params[:metadata][:Search],
143
+ :query => options)
144
+ response_io = StringIO.new(response.body)
145
+
146
+ parser = Innards::Parsers::SearchParser.new
147
+ Ox.sax_parse(parser, response_io)
148
+
149
+ if parser.valid_rets_response_received?
150
+ parser.results
151
+ else
152
+ false
153
+ end
154
+ end
155
+
156
+ def get_object options = {}
157
+ require_login!
158
+
159
+ options = DEFAULT_GETOBJECT_OPTIONS.merge(options)
160
+
161
+ response = make_request_to(@params[:metadata][:GetObject],
162
+ :query => options)
163
+ response_io = StringIO.new(response.body)
164
+
165
+ reader = MultipartHandler.new response.headers["Content-Type"]
166
+ if reader.is_multipart_response?
167
+ reader.parse response.body
168
+ else
169
+ return response.body
170
+ end
171
+
54
172
  end
55
-
173
+
56
174
  protected
175
+ def require_login!
176
+ if @params[:metadata].empty?
177
+ raise LoginRequiredException.new
178
+ end
179
+ end
180
+
57
181
  def has_valid_url?
58
182
  (@params[:login_url] =~ URI::regexp)
59
183
  end
60
-
184
+
61
185
  def has_valid_rets_version?
62
186
  VALID_RETS_VERSIONS.include? @params[:rets_version]
63
187
  end
64
-
188
+
65
189
  def has_valid_auth_mode?
66
190
  VALID_AUTH_MODES.include? @params[:auth_mode]
67
191
  end
68
-
192
+
69
193
  def has_valid_user_agent?
70
- !(@params[:user_agent].empty? || @params[:user_agent].nil?)
194
+ user_agent = @params[:user_agent]
195
+ !(user_agent.empty? || user_agent.nil?)
71
196
  end
72
-
197
+
73
198
  def connection
74
- @conn = Excon.new("#{@params[:login_uri].scheme}://#{@params[:login_uri].host}:#{@params[:login_uri].port}") if @conn.nil?
199
+ @conn = Excon.new(build_connection_url) if @conn.nil?
75
200
  @conn
76
201
  end
77
-
78
- def make_request_to path
79
- response = connection.get(:path => path, :headers => build_headers_for(path))
80
-
81
- parse_cookie_from(response.headers['Set-Cookie']) if response.headers.has_key?('Set-Cookie')
82
- parse_www_authenticate_from(response.headers['WWW-Authenticate']) if response.headers.has_key?('WWW-Authenticate')
83
-
84
- raise AuthenticationRequiredException.new "Authentication Required" if response.status == 401
85
-
202
+
203
+ def build_connection_url
204
+ login_uri = @params[:login_uri]
205
+ "#{login_uri.scheme}://#{login_uri.host}:#{login_uri.port}"
86
206
  end
87
-
88
- def parse_cookie_from set_cookie_header
89
- set_cookie_header.scan(/(\w+)=(.+?);/) {
90
- @params[:cookies].push([$1, $2])
91
- }
207
+
208
+ def make_request_to path, options = {}
209
+ tries ||= 3
210
+ @params[:request_count] += 1
211
+
212
+ response = connection.get(:path => path,
213
+ :headers => build_headers_for(path),
214
+ :query => options[:query])
215
+
216
+ parse_headers_from response
217
+
218
+ raise AuthenticationRequiredException.new if response.status == 401
219
+
220
+ response
221
+ rescue StaleDigestException, AuthenticationRequiredException => e
222
+ retry unless (tries -= 1).zero?
223
+ raise e
92
224
  end
93
-
94
- def parse_www_authenticate_from www_authenticate_header
95
- www_authenticate_header.scan(/(\w+)="(.*?)"/) {
96
- @params[:digest][$1] = $2
97
- }
225
+
226
+ def parse_headers_from response
227
+ headers = response.headers
228
+ if headers.has_key?('Set-Cookie')
229
+ @cookie_handler.parse(headers['Set-Cookie'])
230
+ end
231
+
232
+ if headers.has_key?('WWW-Authenticate')
233
+ @digest_handler.parse(headers['WWW-Authenticate'])
234
+ end
98
235
  end
99
-
236
+
100
237
  def build_headers_for path
101
238
  headers = {
102
239
  'User-Agent' => @params[:user_agent],
103
- 'Cookie' => build_cookie_header_for(path),
240
+ 'RETS-Version' => "RETS/#{@params[:rets_version]}"
104
241
  }
105
- headers['Authorization'] = build_www_authenticate_header_for(path) unless @params[:digest].empty?
106
- headers
107
- end
108
-
109
- def build_cookie_header_for path
110
- URI.encode_www_form(@params[:cookies]) unless @params[:cookies].empty?
111
- end
112
-
113
- def build_www_authenticate_header_for path
114
- cnonce = md5(random)
115
- header = [
116
- %Q(Digest username="#{@params[:login_uri].user}"),
117
- %Q(realm="#{@params[:digest]['realm']}"),
118
- %Q(nonce="#{@params[:digest]['nonce']}"),
119
- %Q(uri="#{@params[:current_request]}"),
120
- %Q(response="#{request_digest(cnonce)}"),
121
- ]
122
-
123
- if has_qop?
124
- fields = [
125
- %Q(cnonce="#{cnonce}"),
126
- %Q(qop="#{@params[:digest]['qop']}"),
127
- %Q(nc=00000001)
128
- ]
129
- fields.each { |field| header << field }
242
+
243
+ unless @params[:cookies].empty?
244
+ headers['Cookie'] = @cookie_handler.build
130
245
  end
131
-
132
- header << %Q(opaque="#{@params[:digest]['opaque']}") if has_opaque?
133
- header.join(", ")
134
- end
135
-
136
- def request_digest cnonce
137
- a = [md5(a1), @params[:digest]['nonce'], md5(a2)]
138
- a.insert(2, "00000001", cnonce, @params[:digest]['qop']) if has_qop?
139
- md5(a.join(":"))
140
- end
141
-
142
- def has_opaque?
143
- @params[:digest].has_key?('opaque') and !@params[:digest]['opaque'].empty?
144
- end
145
-
146
- def has_qop?
147
- @params[:digest].has_key?('qop') and !@params[:digest]['qop'].empty?
148
- end
149
-
150
- def a1
151
- [@params[:login_uri].user, @params[:digest]['realm'], @params[:login_uri].password].join(":")
152
- end
153
246
 
154
- def a2
155
- ["GET", @params[:current_request]].join(":")
156
- end
157
-
158
- def random
159
- "%x" % (Time.now.to_i + rand(65535))
160
- end
161
-
162
- def md5(str)
163
- Digest::MD5.hexdigest(str)
247
+ unless @params[:digest].empty?
248
+ headers['Authorization'] = @digest_handler.build_for(path)
249
+ end
250
+
251
+ headers
164
252
  end
253
+
165
254
  end
166
- end
255
+ end