reso_transport 1.5.5 → 1.5.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e936cab60989d16c28cef1265638fee97c6d10e94b9d2814de93f69f237c5ffd
4
- data.tar.gz: 0e597dea0da08a2aada6b54cc4a5243e8352b442538d78d584555bb1be05dfcb
3
+ metadata.gz: 49293478c0827ccb4bdca1bb6c9974900f8c0c16853b247487120a0ed3bb79df
4
+ data.tar.gz: c9d8c93979377ed18d22a179bae44175961b44e8641eb091930f99542d1e610f
5
5
  SHA512:
6
- metadata.gz: a4d69fc29b8dde541d9665e80ab60973df81b1f794dbee2e9fffe1877b3fbff3fec62ebe2ff69899c4cc7d4287e7e5f4570d02f66338d8c84d8a4341f0a9aed1
7
- data.tar.gz: ad04f1a60eeecf6731d619be5c16830fd6887d8783f10cbaa44c29a70e6c28491dae55bf49ce29d0f415c6e58b458d73e7bc7161f99767c7e4b402c7f74077d8
6
+ metadata.gz: 4464fe0a9aa0006f25c5eab29dab8422401ac2ebf435b712e65d5727e43606bb0df4fdf895e5a1d6a1e3e44d4937d19c42389577d28568351832ccae64d55ebb
7
+ data.tar.gz: 729d643455ddd8e7a827ddc6aea2a8512f93bc65a3e469993da5d2cd5085ecdc5eed4befee4f376cd9986f51a81d363fdeb042a1f8f51e2e51d7230f265c2d7a
data/.gitignore CHANGED
@@ -13,5 +13,7 @@
13
13
  /log/**
14
14
  /test/vcr_cassettes/**
15
15
  md_cache/**
16
+ ds_cache/**
16
17
  .byebug_history
17
18
  secrets.yml
19
+ /.history/
data/.gitpod.yml ADDED
@@ -0,0 +1,2 @@
1
+ tasks:
2
+ - init: bundle install
data/.rubocop.yml ADDED
@@ -0,0 +1,33 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.6
3
+
4
+ Layout/FirstHashElementIndentation:
5
+ Enabled: false
6
+
7
+ Layout/MultilineMethodCallIndentation:
8
+ Enabled: false
9
+
10
+ Metrics/AbcSize:
11
+ Max: 20
12
+
13
+ Metrics/BlockLength:
14
+ Exclude:
15
+ - test/**/*
16
+
17
+ Metrics/ClassLength:
18
+ Max: 150
19
+
20
+ Metrics/MethodLength:
21
+ Max: 15
22
+
23
+ Metrics/ModuleLength:
24
+ Max: 150
25
+
26
+ Style/ColonMethodCall:
27
+ Enabled: false
28
+
29
+ Style/Documentation:
30
+ Enabled: false
31
+
32
+ Style/FrozenStringLiteralComment:
33
+ Enabled: false
data/README.md CHANGED
@@ -1,3 +1,6 @@
1
+ [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/wrstudios/reso_transport)
2
+ [![Gem Version](https://badge.fury.io/rb/reso_transport.svg)](https://badge.fury.io/rb/reso_transport)
3
+
1
4
  # ResoTransport
2
5
 
3
6
  A Ruby gem for connecting to and interacting with RESO WebAPI services. Learn more about what that is by checking out the [RESO WebAPI](https://www.reso.org/reso-web-api/) Documentation.
@@ -51,7 +54,7 @@ Or you can set a logger for each specific instance of a client which can be usef
51
54
 
52
55
  ### Getting Connected
53
56
 
54
- There are 2 strategies for authentication.
57
+ There are 2 strategies for authentication.
55
58
 
56
59
  **Bearer Token**
57
60
 
@@ -60,7 +63,8 @@ It's simple to use a static access token if your token never expires:
60
63
  ```ruby
61
64
  @client = ResoTransport::Client.new({
62
65
  md_file: METADATA_CACHE,
63
- endpoint: ENDPOINT_URL
66
+ endpoint: ENDPOINT_URL,
67
+ use_replication_endpoint: false # this is the default and can be ommitted
64
68
  authentication: {
65
69
  access_token: TOKEN,
66
70
  token_type: "Bearer" # this is the default and can be ommitted
@@ -89,7 +93,8 @@ If the connection requires requesting a new token periodically, it's easy to pro
89
93
 
90
94
  This will pre-fetch a token from the provided endpoint when the current token is either non-existent or has expired.
91
95
 
92
-
96
+ The `use_replication_endpoint` flag will append `/replication` to all resource queries if set to `true`. This is required
97
+ by some data sources to query resources beyond 10,000 records.
93
98
 
94
99
  ### Caching Metadata
95
100
 
@@ -108,9 +113,9 @@ If you don't have access to the file system, like on Heroku, or you just don't w
108
113
 
109
114
  ```ruby
110
115
  class MyCacheStore < ResoTransport::MetadataCache
111
-
116
+
112
117
  def read
113
- # read `name` from somewhere
118
+ # read `name` from somewhere
114
119
  end
115
120
 
116
121
  def write(data)
@@ -158,12 +163,25 @@ Once you have a successful connection you can explore what resources are availab
158
163
  #=> {"Property"=>#<ResoTransport::Resource entity_set="Property", schema="ODataService">, "Office"=>#<ResoTransport::Resource entity_set="Office", schema="ODataService">, "Member"=>#<ResoTransport::Resource entity_set="Member", schema="ODataService">}
159
164
 
160
165
  @client.resources["Property"]
161
- #=> #<ResoTransport::Resource entity_set="Property", schema="ODataService">
166
+ #=> #<ResoTransport::Resource entity_set="Property", schema="ODataService">
162
167
 
163
168
  @client.resources["Property"].query.limit(1).results
164
169
  #=> Results Array
165
170
  ```
166
171
 
172
+ If the resource contains localizations you can access those as well.
173
+
174
+ ```ruby
175
+ @client.resources["Property"].localizations
176
+ #=> {"CommercialSale"=>{"Name"=>"CommercialSale", "ResourcePath"=>"/Property?Class=CommercialSale", "Description"=>"Contains data for Commercial searches.", "DateTimeStamp"=>"2021-05-03T18:13:20.643-07:00"}, "Residential"=>{"Name"=>"Residential", "ResourcePath"=>"/Property?Class=Residential", "Description"=>"Contains data for Residential searches.", "DateTimeStamp"=>"2021-05-03T18:13:20.643-07:00"}}
177
+ ```
178
+
179
+ If a resource contains localizations you must select one by name, before querying, like so:
180
+
181
+ ```ruby
182
+ @client.resources["Property"].localization('Residential').query.limit(1).results
183
+ ```
184
+
167
185
  #### Querying
168
186
 
169
187
  ResoTransport provides powerful querying capabilities:
@@ -196,7 +214,7 @@ To see what child records can be expanded look at `expandable`:
196
214
 
197
215
  ```ruby
198
216
  @resource.expandable
199
- #=> [#<struct ResoTransport::Property name="Media", data_type="Collection(RESO.Media)", attrs={"Name"=>"Media", "Type"=>"Collection(RESO.Media)"}, multi=true, enum=nil, complex_type=nil, entity_type=#<struct ResoTransport::EntityType name="Media", base_type=nil, primary_key="MediaKey", schema="CoreLogic.DataStandard.RESO.DD">> ...]
217
+ #=> [#<struct ResoTransport::Property name="Media", data_type="Collection(RESO.Media)", attrs={"Name"=>"Media", "Type"=>"Collection(RESO.Media)"}, multi=true, enum=nil, complex_type=nil, entity_type=#<struct ResoTransport::EntityType name="Media", base_type=nil, primary_key="MediaKey", schema="CoreLogic.DataStandard.RESO.DD">> ...]
200
218
  ```
201
219
 
202
220
  Use `expand` to expand child records with the top level results.
@@ -210,7 +228,7 @@ You have several options to expand multiple child record sets. Each of these wil
210
228
 
211
229
  ```ruby
212
230
  @resource.query.expand("Media", "Office").limit(10).results
213
-
231
+
214
232
  @resource.query.expand(["Media", "Office"]).limit(10).results
215
233
 
216
234
  @resource.query.expand("Media").expand("Office").limit(10).results
@@ -257,7 +275,7 @@ When querying for an enumeration value, you can provide either the system name,
257
275
 
258
276
  ```ruby
259
277
  @resource.query.eq(StandardStatus: "Active Under Contract").limit(1).compile_params
260
- #=> {"$top"=>1, "$filter"=>"StandardStatus eq 'ActiveUnderContract'"}
278
+ #=> {"$top"=>1, "$filter"=>"StandardStatus eq 'ActiveUnderContract'"}
261
279
  ```
262
280
 
263
281
  ## Development
data/bin/console CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "bundler/setup"
4
- require "reso_transport"
3
+ require 'bundler/setup'
4
+ require 'reso_transport'
5
5
 
6
6
  # You can add fixtures and/or initialization code here to make experimenting
7
7
  # with your gem easier. You can also use a different console, if you like.
@@ -12,19 +12,19 @@ require "reso_transport"
12
12
  #
13
13
 
14
14
  ResoTransport.configure do |c|
15
- c.logger = Logger.new("log/console.log")
15
+ c.logger = Logger.new('log/console.log')
16
16
  end
17
17
 
18
-
19
- require "irb"
18
+ require 'irb'
20
19
  require 'yaml'
21
20
  require 'byebug'
22
21
 
23
- SECRETS = YAML.load_file("secrets.yml")
22
+ SECRETS = YAML.load_file('secrets.yml')
24
23
 
25
- @trestle = ResoTransport::Client.new(SECRETS[:trestle].merge(logger: Logger.new($stdout)))
26
- #@bridge = ResoTransport::Client.new(SECRETS[:bridge].merge(logger: Logger.new($stdout)))
27
- # @spark = ResoTransport::Client.new(SECRETS[:spark])
28
- #@crmls = ResoTransport::Client.new(SECRETS[:crmls])
24
+ SECRETS.each do |name, data|
25
+ data[:logger] = Logger.new($stdout)
26
+ client = ResoTransport::Client.new(data)
27
+ instance_variable_set("@#{name}", client)
28
+ end
29
29
 
30
30
  IRB.start(__FILE__)
data/bin/rake ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rake", "rake")
@@ -5,41 +5,30 @@ require 'faraday'
5
5
  require 'json'
6
6
  require 'time'
7
7
 
8
- require "reso_transport/version"
9
- require "reso_transport/configuration"
10
- require "reso_transport/authentication"
11
- require "reso_transport/client"
12
- require "reso_transport/resource"
13
- require "reso_transport/metadata"
14
- require "reso_transport/metadata_cache"
15
- require "reso_transport/metadata_parser"
16
- require "reso_transport/schema"
17
- require "reso_transport/entity_set"
18
- require "reso_transport/entity_type"
19
- require "reso_transport/enum"
20
- require "reso_transport/property"
21
- require "reso_transport/query"
22
-
23
-
24
-
25
- # module Faraday
26
- # module Utils
27
-
28
- # def escape(str)
29
- # str.to_s.gsub(ESCAPE_RE) do |match|
30
- # '%' + match.unpack('H2' * match.bytesize).join('%').upcase
31
- # end.gsub(" ","%20")
32
-
33
- # end
34
- # end
35
- # end
36
-
37
- Faraday::Utils.default_space_encoding = "%20"
8
+ require 'reso_transport/version'
9
+ require 'reso_transport/configuration'
10
+ require 'reso_transport/authentication'
11
+ require 'reso_transport/client'
12
+ require 'reso_transport/resource'
13
+ require 'reso_transport/metadata'
14
+ require 'reso_transport/metadata_cache'
15
+ require 'reso_transport/metadata_parser'
16
+ require 'reso_transport/datasystem'
17
+ require 'reso_transport/datasystem_parser'
18
+ require 'reso_transport/schema'
19
+ require 'reso_transport/entity_set'
20
+ require 'reso_transport/entity_type'
21
+ require 'reso_transport/enum'
22
+ require 'reso_transport/property'
23
+ require 'reso_transport/query'
24
+
25
+ Faraday::Utils.default_space_encoding = '%20'
38
26
 
39
27
  module ResoTransport
40
28
  class Error < StandardError; end
29
+
41
30
  class AccessDenied < StandardError; end
42
- ODATA_TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
31
+ ODATA_TIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'.freeze
43
32
 
44
33
  class << self
45
34
  attr_writer :configuration
@@ -53,8 +42,7 @@ module ResoTransport
53
42
  yield(configuration)
54
43
  end
55
44
 
56
- def self.split_schema_and_class_name(s)
57
- s.to_s.partition(/(\w+)$/).first(2).map {|s| s.sub(/\.$/, '') }
45
+ def self.split_schema_and_class_name(text)
46
+ text.to_s.partition(/(\w+)$/).first(2).map { |s| s.sub(/\.$/, '') }
58
47
  end
59
-
60
48
  end
@@ -1,20 +1,32 @@
1
1
  module ResoTransport
2
2
  module Authentication
3
3
  class FetchTokenAuth < AuthStrategy
4
- attr_reader :connection, :endpoint, :client_id, :client_secret, :grant_type, :scope
5
-
4
+ attr_reader :endpoint,
5
+ :client_id,
6
+ :client_secret,
7
+ :grant_type,
8
+ :scope,
9
+ :username,
10
+ :password
11
+
6
12
  def initialize(options)
7
- @grant_type = options.fetch(:grant_type, "client_credentials")
8
- @scope = options.fetch(:scope, "api")
13
+ super()
14
+
15
+ @grant_type = options.fetch(:grant_type, 'client_credentials')
16
+ @scope = options.fetch(:scope, 'api')
9
17
  @client_id = options.fetch(:client_id)
10
18
  @client_secret = options.fetch(:client_secret)
11
19
  @endpoint = options.fetch(:endpoint)
20
+ @username = options.fetch(:username, nil)
21
+ @password = options.fetch(:password, nil)
22
+ end
12
23
 
13
- @connection = Faraday.new(@endpoint) do |faraday|
24
+ def connection
25
+ @connection ||= Faraday.new(@endpoint) do |faraday|
14
26
  faraday.request :url_encoded
15
27
  faraday.response :logger, ResoTransport.configuration.logger if ResoTransport.configuration.logger
16
28
  faraday.adapter Faraday.default_adapter
17
- faraday.basic_auth @client_id, @client_secret
29
+ faraday.basic_auth client_id, client_secret
18
30
  end
19
31
  end
20
32
 
@@ -30,19 +42,26 @@ module ResoTransport
30
42
  Access.new({
31
43
  access_token: json.fetch('access_token'),
32
44
  expires_in: json.fetch('expires_in', 1 << (1.size * 8 - 2) - 1),
33
- token_type: json.fetch('token_type', "Bearer")
45
+ token_type: json.fetch('token_type', 'Bearer')
34
46
  })
35
47
  end
36
48
 
37
49
  private
38
50
 
39
51
  def auth_params
40
- {
41
- client_id: client_id,
52
+ params = {
53
+ client_id: client_id,
42
54
  client_secret: client_secret,
43
- grant_type: grant_type,
44
- scope: scope
55
+ grant_type: grant_type,
56
+ scope: scope
45
57
  }
58
+
59
+ if grant_type == 'password'
60
+ params[:username] = username
61
+ params[:password] = password
62
+ end
63
+
64
+ params
46
65
  end
47
66
  end
48
67
  end
@@ -0,0 +1,61 @@
1
+ module ResoTransport
2
+ class BaseMetadata
3
+ MIME_TYPES = {
4
+ xml: 'application/xml',
5
+ json: 'application/json'
6
+ }.freeze
7
+
8
+ attr_reader :client
9
+
10
+ def initialize(client)
11
+ @client = client
12
+ @prefix = nil
13
+ @classname = nil
14
+ end
15
+
16
+ def prefix
17
+ raise 'prefix not set' unless @prefix
18
+
19
+ @prefix
20
+ end
21
+
22
+ def classname
23
+ raise 'classname not set' unless @classname
24
+
25
+ @classname
26
+ end
27
+
28
+ def parser
29
+ @parser ||= Object::const_get("#{classname}Parser").new.parse(data)
30
+ end
31
+
32
+ def data
33
+ if cache_file
34
+ cache.read || cache.write(raw)
35
+ else
36
+ raw
37
+ end
38
+ end
39
+
40
+ def cache
41
+ @cache ||= client.send("#{prefix}_cache").new(cache_file)
42
+ end
43
+
44
+ def cache_file
45
+ @cache_file ||= client.send "#{prefix}_file"
46
+ end
47
+
48
+ def raw
49
+ if response.success?
50
+ response.body
51
+ else
52
+ puts response.body
53
+ raise "Error getting #{classname}!"
54
+ end
55
+ end
56
+
57
+ def response
58
+ raise 'Must implement response method'
59
+ end
60
+ end
61
+ end
@@ -1,35 +1,52 @@
1
1
  module ResoTransport
2
2
  class Client
3
- attr_reader :connection, :uid, :vendor, :endpoint, :auth, :md_file, :md_cache
4
-
3
+ attr_reader :connection, :uid, :vendor, :endpoint, :authentication, :md_file, :md_cache, :ds_file, :ds_cache,
4
+ :use_replication_endpoint
5
+
5
6
  def initialize(options)
6
- @endpoint = options.fetch(:endpoint)
7
- @md_file = options.fetch(:md_file, nil)
8
- @authentication = ensure_valid_auth_strategy(options.fetch(:authentication))
9
- @vendor = options.fetch(:vendor, {})
10
- @faraday_options = options.fetch(:faraday_options, {})
11
- @logger = options.fetch(:logger, nil)
12
- @md_cache = options.fetch(:md_cache, ResoTransport::MetadataCache)
13
-
14
- @connection = Faraday.new(@endpoint, @faraday_options) do |faraday|
7
+ @use_replication_endpoint = options.fetch(:use_replication_endpoint, false)
8
+ @endpoint = options.fetch(:endpoint)
9
+ @md_file = options.fetch(:md_file, nil)
10
+ @ds_file = options.fetch(:ds_file, nil)
11
+ @authentication = ensure_valid_auth_strategy(options.fetch(:authentication))
12
+ @vendor = options.fetch(:vendor, {})
13
+ @faraday_options = options.fetch(:faraday_options, {})
14
+ @logger = options.fetch(:logger, nil)
15
+ @md_cache = options.fetch(:md_cache, ResoTransport::MetadataCache)
16
+ @ds_cache = options.fetch(:ds_cache, ResoTransport::MetadataCache)
17
+ @connection = establish_connection
18
+ end
19
+
20
+ def establish_connection
21
+ Faraday.new(@endpoint, @faraday_options) do |faraday|
15
22
  faraday.request :url_encoded
16
23
  faraday.response :logger, @logger || ResoTransport.configuration.logger
17
- #yield faraday if block_given?
18
24
  faraday.use Authentication::Middleware, @authentication
19
- faraday.adapter Faraday.default_adapter #unless faraday.builder.send(:adapter_set?)
25
+ faraday.adapter Faraday.default_adapter # unless faraday.builder.send(:adapter_set?)
20
26
  end
21
27
  end
22
28
 
23
29
  def resources
24
- @resources ||= metadata.entity_sets.map {|es| {es.name => Resource.new(self, es)} }.reduce(:merge!)
30
+ @resources ||= metadata.entity_sets.map { |es| { es.name => resource_for(es) } }.reduce(:merge!)
31
+ end
32
+
33
+ def resource_for(entity_set)
34
+ localizations = {}
35
+ localizations = datasystem.localizations_for(entity_set.entity_type) if metadata.datasystem?
36
+
37
+ Resource.new(self, entity_set, localizations)
25
38
  end
26
39
 
27
40
  def metadata
28
41
  @metadata ||= Metadata.new(self)
29
42
  end
30
43
 
44
+ def datasystem
45
+ @datasystem ||= Datasystem.new(self)
46
+ end
47
+
31
48
  def to_s
32
- %(#<ResoTransport::Client endpoint="#{endpoint}", md_file="#{md_file}">)
49
+ %(#<ResoTransport::Client endpoint="#{endpoint}", md_file="#{md_file}", ds_file="#{ds_file}">)
33
50
  end
34
51
 
35
52
  def inspect
@@ -41,7 +58,7 @@ module ResoTransport
41
58
  def ensure_valid_auth_strategy(options)
42
59
  case options
43
60
  when Hash
44
- if options.has_key?(:endpoint)
61
+ if options.key?(:endpoint)
45
62
  Authentication::FetchTokenAuth.new(options)
46
63
  else
47
64
  Authentication::StaticTokenAuth.new(options)
@@ -50,6 +67,5 @@ module ResoTransport
50
67
  raise ArgumentError, "#{options.inspect} invalid: cannot determine strategy"
51
68
  end
52
69
  end
53
-
54
70
  end
55
71
  end
@@ -0,0 +1,22 @@
1
+ require_relative 'base_metadata'
2
+
3
+ module ResoTransport
4
+ class Datasystem < BaseMetadata
5
+ def initialize(client)
6
+ super client
7
+ @prefix = 'ds'
8
+ @classname = self.class.name
9
+ end
10
+
11
+ def localizations_for(resource_name)
12
+ localizations = parser.resources.dig(resource_name, 'Localizations') || []
13
+ localizations.map { |l| [l['Name'], l] }.to_h
14
+ end
15
+
16
+ def response
17
+ @response ||= client.connection.get('DataSystem') do |req|
18
+ req.headers['Accept'] = 'application/json'
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,26 @@
1
+ module ResoTransport
2
+ class DatasystemParser
3
+ def parse(doc)
4
+ begin
5
+ data = doc.is_a?(File) ? doc.read : doc
6
+ @json = JSON.parse data
7
+ rescue JSON::ParserError => e
8
+ @json = {}
9
+ puts e.message
10
+ end
11
+ self
12
+ end
13
+
14
+ # value ->
15
+ # Resources ->
16
+ # Name ->
17
+ # ResourcePath ->
18
+ # Localizations ->
19
+ # Name ->
20
+ # ResourcePath ->
21
+
22
+ def resources
23
+ @resources ||= @json['value'].map { |v| v['Resources'] }.flatten.compact.map { |r| [r['Name'], r] }.to_h
24
+ end
25
+ end
26
+ end
@@ -1,11 +1,9 @@
1
1
  module ResoTransport
2
2
  EntitySet = Struct.new(:name, :schema, :entity_type) do
3
-
4
3
  def self.from_stream(args)
5
- schema, entity_type = ResoTransport.split_schema_and_class_name(args["EntityType"])
4
+ schema, entity_type = ResoTransport.split_schema_and_class_name(args['EntityType'])
6
5
 
7
- new(args["Name"], schema, entity_type)
6
+ new(args['Name'], schema, entity_type)
8
7
  end
9
-
10
8
  end
11
9
  end
@@ -1,30 +1,29 @@
1
1
  module ResoTransport
2
2
  EntityType = Struct.new(:name, :base_type, :primary_key, :schema) do
3
-
4
3
  def self.from_stream(args)
5
- new(args["Name"], args["BaseType"])
4
+ new(args['Name'], args['BaseType'])
6
5
  end
7
6
 
8
7
  def parse(record)
9
- record.each_pair do |k,v|
8
+ record.each_pair do |k, v|
10
9
  next if v.nil?
11
- if property = (property_map[k] || navigation_property_map[k])
12
- record[k] = property.parse(v)
13
- end
10
+
11
+ property = property_map[k] || navigation_property_map[k]
12
+ record[k] = property.parse(v) if property
14
13
  end
15
14
  end
16
15
 
17
16
  def parse_value(record)
18
- record.each_pair do |k,v|
17
+ record.each_pair do |k, v|
19
18
  next if v.nil?
20
- if property = (property_map[k] || navigation_property_map[k])
21
- record[k] = property.parse(v)
22
- end
19
+
20
+ property = property_map[k] || navigation_property_map[k]
21
+ record[k] = property.parse(v) if property
23
22
  end
24
23
  end
25
24
 
26
25
  def property_map
27
- @property_map ||= properties.inject({}) {|hsh, p| hsh[p.name] = p; hsh }
26
+ @property_map ||= properties.each_with_object({}) { |p, hsh| hsh[p.name] = p; }
28
27
  end
29
28
 
30
29
  def properties
@@ -32,7 +31,7 @@ module ResoTransport
32
31
  end
33
32
 
34
33
  def navigation_property_map
35
- @navigation_property_map ||= navigation_properties.inject({}) {|hsh, p| hsh[p.name] = p; hsh }
34
+ @navigation_property_map ||= navigation_properties.each_with_object({}) { |p, hsh| hsh[p.name] = p; }
36
35
  end
37
36
 
38
37
  def navigation_properties
@@ -42,6 +41,5 @@ module ResoTransport
42
41
  def enumerations
43
42
  @enumerations ||= []
44
43
  end
45
-
46
44
  end
47
45
  end
@@ -1,10 +1,12 @@
1
- module ResoTransport
2
- Metadata = Struct.new(:client) do
1
+ require_relative 'base_metadata'
3
2
 
4
- MIME_TYPES = {
5
- xml: "application/xml",
6
- json: "application/json"
7
- }
3
+ module ResoTransport
4
+ class Metadata < BaseMetadata
5
+ def initialize(client)
6
+ super client
7
+ @prefix = 'md'
8
+ @classname = self.class.name
9
+ end
8
10
 
9
11
  def entity_sets
10
12
  parser.entity_sets
@@ -14,34 +16,14 @@ module ResoTransport
14
16
  parser.schemas
15
17
  end
16
18
 
17
- def parser
18
- @parser ||= MetadataParser.new.parse(get_data)
19
+ def datasystem?
20
+ parser.datasystem?
19
21
  end
20
22
 
21
- def md_cache
22
- @md_cache ||= client.md_cache.new(client.md_file)
23
- end
24
-
25
- def get_data
26
- if client.md_file
27
- md_cache.read || md_cache.write(raw)
28
- else
29
- raw
30
- end
31
- end
32
-
33
- def raw
34
- resp = client.connection.get("$metadata") do |req|
23
+ def response
24
+ @response ||= client.connection.get('$metadata') do |req|
35
25
  req.headers['Accept'] = MIME_TYPES[client.vendor.fetch(:metadata_format, :xml).to_sym]
36
26
  end
37
-
38
- if resp.success?
39
- resp.body
40
- else
41
- puts resp.body
42
- raise "Error getting metadata!"
43
- end
44
27
  end
45
-
46
28
  end
47
29
  end
@@ -7,15 +7,14 @@ module ResoTransport
7
7
  end
8
8
 
9
9
  def read
10
- if File.exist?(name) && File.size(name) > 0
11
- File.new(name)
12
- end
10
+ return nil if !File.exist?(name) || File.size(name).zero?
11
+
12
+ File.new(name)
13
13
  end
14
14
 
15
15
  def write(raw)
16
- File.open(name, "w") {|f| f.write(raw.force_encoding("UTF-8")) } unless raw.length == 0
16
+ File.open(name, 'w') { |f| f.write(raw.force_encoding('UTF-8')) } if raw.length.positive?
17
17
  File.new(name)
18
18
  end
19
-
20
19
  end
21
20
  end
@@ -14,12 +14,14 @@ module ResoTransport
14
14
  @current_complex_type = nil
15
15
  @current_enum_type = nil
16
16
  @current_member = nil
17
+
18
+ @datasystem = nil
17
19
  end
18
20
 
19
21
  def parse(doc)
20
22
  REXML::Document.parse_stream(doc, self)
21
23
  finalize
22
- return self
24
+ self
23
25
  end
24
26
 
25
27
  def finalize
@@ -54,55 +56,62 @@ module ResoTransport
54
56
 
55
57
  def tag_start(name, args)
56
58
  case name
57
- when "Schema"
59
+ when 'Schema'
58
60
  @schemas << ResoTransport::Schema.from_stream(args)
59
- when "EntitySet"
61
+ when 'EntitySet'
60
62
  @entity_sets << ResoTransport::EntitySet.from_stream(args)
61
- when "EntityType"
63
+ when 'EntityType'
62
64
  @current_entity_type = ResoTransport::EntityType.from_stream(args)
63
- when "ComplexType"
65
+ when 'ComplexType'
64
66
  @current_complex_type = ResoTransport::EntityType.from_stream(args)
65
- when "PropertyRef"
67
+ when 'PropertyRef'
66
68
  @current_entity_type.primary_key = args['Name']
67
- when "Property"
68
- @current_entity_type.properties << ResoTransport::Property.from_stream(args.merge(schema: @schemas.last)) if @current_entity_type
69
- @current_complex_type.properties << ResoTransport::Property.from_stream(args.merge(schema: @schemas.last)) if @current_complex_type
70
- when "NavigationProperty"
69
+ when 'Property'
70
+ if @current_entity_type
71
+ @current_entity_type.properties << ResoTransport::Property.from_stream(args.merge(schema: @schemas.last))
72
+ end
73
+ if @current_complex_type
74
+ @current_complex_type.properties << ResoTransport::Property.from_stream(args.merge(schema: @schemas.last))
75
+ end
76
+ when 'NavigationProperty'
71
77
  @current_entity_type.navigation_properties << ResoTransport::Property.from_stream(args)
72
- when "EnumType"
78
+ when 'EnumType'
73
79
  @current_enum_type = ResoTransport::Enum.from_stream(args.merge(schema: @schemas.last))
74
- when "Member"
80
+ when 'Member'
75
81
  @current_member = ResoTransport::Member.from_stream(args)
76
- when "Annotation"
82
+ when 'Annotation'
77
83
  if @current_enum_type && @current_member
78
84
  @current_member.annotation = args['String']
79
- else
80
- if @current_entity_type || @current_complex_type
81
- #raise args.inspect
82
- end
85
+ elsif @current_entity_type || @current_complex_type
86
+ # raise args.inspect
83
87
  end
84
88
  end
85
- rescue => e
89
+ rescue StandardError => e
86
90
  puts e.inspect
87
91
  puts "Error processing Tag: #{[name, args].inspect}"
88
92
  end
89
93
 
90
94
  def tag_end(name)
91
95
  case name
92
- when "EntityType"
96
+ when 'EntityType'
93
97
  @current_entity_type.schema = @schemas.last.namespace
94
98
  @schemas.last.entity_types << @current_entity_type
95
- when "ComplexType"
99
+ when 'ComplexType'
96
100
  @current_complex_type.schema = @schemas.last.namespace
97
101
  @schemas.last.complex_types << @current_complex_type
98
- when "EnumType"
102
+ when 'EnumType'
99
103
  @enumerations << @current_enum_type
100
104
  @current_enum_type = nil
101
- when "Member"
105
+ when 'Member'
102
106
  @current_enum_type.members << @current_member
103
107
  @current_member = nil
104
108
  end
105
109
  end
106
110
 
111
+ def datasystem?
112
+ return @datasystem unless @datasystem.nil?
113
+
114
+ @datasystem = @schemas.any? { |s| s.entity_types.any? { |t| t.name == 'DataSystem' } }
115
+ end
107
116
  end
108
117
  end
@@ -1,23 +1,22 @@
1
1
  module ResoTransport
2
2
  Query = Struct.new(:resource) do
3
-
4
- def all(*contexts, &block)
3
+ def all(*_contexts, &block)
5
4
  new_query_context('and')
6
5
  instance_eval(&block)
7
6
  clear_query_context
8
- return self
7
+ self
9
8
  end
10
9
 
11
10
  def any(&block)
12
11
  new_query_context('or')
13
12
  instance_eval(&block)
14
13
  clear_query_context
15
- return self
14
+ self
16
15
  end
17
16
 
18
- [:eq, :ne, :gt, :ge, :lt, :le].each do |op|
17
+ %i[eq ne gt ge lt le].each do |op|
19
18
  define_method(op) do |conditions|
20
- conditions.each_pair do |k,v|
19
+ conditions.each_pair do |k, v|
21
20
  current_query_context << "#{k} #{op} #{encode_value(k, v)}"
22
21
  end
23
22
  return self
@@ -26,36 +25,36 @@ module ResoTransport
26
25
 
27
26
  def limit(size)
28
27
  options[:top] = size
29
- return self
28
+ self
30
29
  end
31
30
 
32
31
  def offset(size)
33
32
  options[:skip] = size
34
- return self
33
+ self
35
34
  end
36
35
 
37
- def order(field, dir=nil)
38
- options[:orderby] = [field, dir].join(" ").strip
39
- return self
36
+ def order(field, dir = nil)
37
+ options[:orderby] = [field, dir].join(' ').strip
38
+ self
40
39
  end
41
40
 
42
41
  def include_count
43
42
  options[:count] = true
44
- return self
43
+ self
45
44
  end
46
45
 
47
46
  def select(*fields)
48
- os = options.fetch(:select, "").split(",")
49
- options[:select] = (os + Array(fields)).uniq.join(",")
47
+ os = options.fetch(:select, '').split(',')
48
+ options[:select] = (os + Array(fields)).uniq.join(',')
50
49
 
51
- return self
50
+ self
52
51
  end
53
52
 
54
53
  def expand(*names)
55
- ex = options.fetch(:expand, "").split(",")
56
- options[:expand] = (ex + Array(names)).uniq.join(",")
54
+ ex = options.fetch(:expand, '').split(',')
55
+ options[:expand] = (ex + Array(names)).uniq.join(',')
57
56
 
58
- return self
57
+ self
59
58
  end
60
59
 
61
60
  def count
@@ -63,7 +62,7 @@ module ResoTransport
63
62
  limit(1).include_count
64
63
  resp = resource.get(compile_params)
65
64
  parsed_body = JSON.parse(resp.body)
66
- parsed_body.fetch("@odata.count", 0)
65
+ parsed_body.fetch('@odata.count', 0)
67
66
  end
68
67
 
69
68
  def results
@@ -72,21 +71,28 @@ module ResoTransport
72
71
  if resp[:success]
73
72
  resp[:results]
74
73
  else
75
- puts resp.inspect
76
- raise "Request Failed"
74
+ puts resp[:meta]
75
+ raise 'Request Failed'
77
76
  end
78
77
  end
79
78
 
80
79
  def execute
81
80
  resp = resource.get(compile_params)
82
- parsed_body = JSON.parse(resp.body)
83
- results = Array(parsed_body.delete("value"))
84
-
85
- {
86
- success: resp.success? && !parsed_body.has_key?("error"),
87
- meta: parsed_body,
88
- results: resource.parse(results)
89
- }
81
+ if resp.success?
82
+ parsed_body = JSON.parse(resp.body)
83
+ results = Array(parsed_body.delete('value'))
84
+
85
+ {
86
+ success: resp.success? && !parsed_body.key?('error'),
87
+ meta: parsed_body,
88
+ results: resource.parse(results)
89
+ }
90
+ else
91
+ {
92
+ success: false,
93
+ meta: resp.body
94
+ }
95
+ end
90
96
  end
91
97
 
92
98
  def new_query_context(context)
@@ -110,7 +116,7 @@ module ResoTransport
110
116
  end
111
117
 
112
118
  def sub_queries
113
- @sub_queries ||= Hash.new {|h,k| h[k] = { context: 'and', criteria: [] } }
119
+ @sub_queries ||= Hash.new { |h, k| h[k] = { context: 'and', criteria: [] } }
114
120
  end
115
121
 
116
122
  def compile_filters
@@ -120,36 +126,32 @@ module ResoTransport
120
126
 
121
127
  filter_chunks = []
122
128
 
123
- if global && global[:criteria]&.any?
124
- filter_chunks << global[:criteria].join(" #{global[:context]} ")
125
- end
129
+ filter_chunks << global[:criteria].join(" #{global[:context]} ") if global && global[:criteria]&.any?
126
130
 
127
131
  filter_chunks << filter_groups.map do |g|
128
132
  "(#{g[:criteria].join(" #{g[:context]} ")})"
129
- end.join(" and ")
133
+ end.join(' and ')
130
134
 
131
- filter_chunks.reject {|c| c == ""}.join(" and ")
135
+ filter_chunks.reject { |c| c == '' }.join(' and ')
132
136
  end
133
137
 
134
138
  def compile_params
135
139
  params = {}
136
140
 
137
- options.each_pair do |k,v|
141
+ options.each_pair do |k, v|
138
142
  params["$#{k}"] = v
139
143
  end
140
144
 
141
- if !sub_queries.empty?
142
- params["$filter"] = compile_filters
143
- end
145
+ params['$filter'] = compile_filters unless sub_queries.empty?
144
146
 
145
147
  params
146
148
  end
147
149
 
148
- def encode_value(key, v)
150
+ def encode_value(key, val)
149
151
  field = resource.property(key.to_s)
150
152
  raise "Couldn't find property #{key} for #{resource.name}" if field.nil?
151
- field.encode(v)
152
- end
153
153
 
154
+ field.encode(val)
155
+ end
154
156
  end
155
157
  end
@@ -1,6 +1,5 @@
1
1
  module ResoTransport
2
- Resource = Struct.new(:client, :entity_set) do
3
-
2
+ Resource = Struct.new(:client, :entity_set, :localizations, :local) do
4
3
  def query
5
4
  Query.new(self)
6
5
  end
@@ -10,7 +9,7 @@ module ResoTransport
10
9
  end
11
10
 
12
11
  def property(name)
13
- properties.detect {|p| p.name == name }
12
+ properties.detect { |p| p.name == name }
14
13
  end
15
14
 
16
15
  def properties
@@ -20,13 +19,13 @@ module ResoTransport
20
19
  def expandable
21
20
  entity_type.navigation_properties
22
21
  end
23
-
22
+
24
23
  def entity_type
25
- @entity_type ||= schema.entity_types.detect {|et| et.name == entity_set.entity_type }
24
+ @entity_type ||= schema.entity_types.detect { |et| et.name == entity_set.entity_type }
26
25
  end
27
26
 
28
27
  def schema
29
- @schema ||= md.schemas.detect {|s| s.namespace == entity_set.schema }
28
+ @schema ||= md.schemas.detect { |s| s.namespace == entity_set.schema }
30
29
  end
31
30
 
32
31
  def md
@@ -34,15 +33,30 @@ module ResoTransport
34
33
  end
35
34
 
36
35
  def parse(results)
37
- results.map {|r| entity_type.parse(r) }
36
+ results.map { |r| entity_type.parse(r) }
38
37
  end
39
38
 
40
39
  def get(params)
41
- client.connection.get(name, params) do |req|
40
+ client.connection.get(url, params) do |req|
42
41
  req.headers['Accept'] = 'application/json'
43
42
  end
44
43
  end
45
44
 
45
+ def url
46
+ return local['ResourcePath'].gsub(%r{^/}, '') if local
47
+
48
+ raise 'Localization required' if localizations.any? && local.nil?
49
+
50
+ return "#{name}/replication" if client.use_replication_endpoint
51
+
52
+ name
53
+ end
54
+
55
+ def localization(name)
56
+ self.local = localizations[name] if localizations.key?(name)
57
+ self
58
+ end
59
+
46
60
  def to_s
47
61
  %(#<ResoTransport::Resource entity_set="#{name}", schema="#{schema&.namespace}">)
48
62
  end
@@ -50,6 +64,5 @@ module ResoTransport
50
64
  def inspect
51
65
  to_s
52
66
  end
53
-
54
67
  end
55
68
  end
@@ -1,3 +1,3 @@
1
1
  module ResoTransport
2
- VERSION = "1.5.5"
2
+ VERSION = '1.5.7'.freeze
3
3
  end
@@ -1,35 +1,35 @@
1
-
2
- lib = File.expand_path("../lib", __FILE__)
1
+ lib = File.expand_path('lib', __dir__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "reso_transport/version"
3
+ require 'reso_transport/version'
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = "reso_transport"
6
+ spec.name = 'reso_transport'
8
7
  spec.version = ResoTransport::VERSION
9
- spec.authors = ["Jon Druse"]
10
- spec.email = ["jon@wrstudios.com"]
8
+ spec.authors = ['Jon Druse']
9
+ spec.email = ['jon@wrstudios.com']
11
10
 
12
- spec.summary = "A utility for consuming RESO Web API connections"
13
- spec.description = "Supports Trestle, Spark, Bridge Interactive, MLS Grid"
14
- spec.homepage = "http://github.com/wrstudios/reso_transport"
15
- spec.license = "MIT"
11
+ spec.summary = 'A utility for consuming RESO Web API connections'
12
+ spec.description = 'Supports Trestle, Spark, Bridge Interactive, MLS Grid'
13
+ spec.homepage = 'http://github.com/wrstudios/reso_transport'
14
+ spec.license = 'MIT'
16
15
 
16
+ spec.required_ruby_version = '>= 2.6'
17
17
 
18
18
  # Specify which files should be added to the gem when it is released.
19
19
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
21
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
22
  end
23
- spec.bindir = "exe"
23
+ spec.bindir = 'exe'
24
24
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
- spec.require_paths = ["lib"]
25
+ spec.require_paths = ['lib']
26
26
 
27
- spec.add_dependency "faraday", "~> 1.0.1"
27
+ spec.add_dependency 'faraday', '~> 1.0.1'
28
28
 
29
- spec.add_development_dependency "bundler", "~> 2"
30
- spec.add_development_dependency "rake", "~> 13"
31
- spec.add_development_dependency "minitest", "~> 5.0"
32
- spec.add_development_dependency "minitest-rg", "~> 5.0"
33
- spec.add_development_dependency "vcr", "~> 6.0"
34
- spec.add_development_dependency "byebug"
29
+ spec.add_development_dependency 'bundler', '~> 2'
30
+ spec.add_development_dependency 'byebug', '~> 11'
31
+ spec.add_development_dependency 'minitest', '~> 5.0'
32
+ spec.add_development_dependency 'minitest-rg', '~> 5.0'
33
+ spec.add_development_dependency 'rake', '~> 13'
34
+ spec.add_development_dependency 'vcr', '~> 6.0'
35
35
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reso_transport
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.5
4
+ version: 1.5.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Druse
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-01-25 00:00:00.000000000 Z
11
+ date: 2021-05-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -39,19 +39,19 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '2'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rake
42
+ name: byebug
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '13'
47
+ version: '11'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '13'
54
+ version: '11'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: minitest
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -81,33 +81,33 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '5.0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: vcr
84
+ name: rake
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '6.0'
89
+ version: '13'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '6.0'
96
+ version: '13'
97
97
  - !ruby/object:Gem::Dependency
98
- name: byebug
98
+ name: vcr
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - ">="
101
+ - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '0'
103
+ version: '6.0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - ">="
108
+ - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '0'
110
+ version: '6.0'
111
111
  description: Supports Trestle, Spark, Bridge Interactive, MLS Grid
112
112
  email:
113
113
  - jon@wrstudios.com
@@ -116,6 +116,8 @@ extensions: []
116
116
  extra_rdoc_files: []
117
117
  files:
118
118
  - ".gitignore"
119
+ - ".gitpod.yml"
120
+ - ".rubocop.yml"
119
121
  - ".ruby-version"
120
122
  - ".travis.yml"
121
123
  - CODE_OF_CONDUCT.md
@@ -125,6 +127,7 @@ files:
125
127
  - README.md
126
128
  - Rakefile
127
129
  - bin/console
130
+ - bin/rake
128
131
  - bin/setup
129
132
  - lib/reso_transport.rb
130
133
  - lib/reso_transport/authentication.rb
@@ -133,8 +136,11 @@ files:
133
136
  - lib/reso_transport/authentication/fetch_token_auth.rb
134
137
  - lib/reso_transport/authentication/middleware.rb
135
138
  - lib/reso_transport/authentication/static_token_auth.rb
139
+ - lib/reso_transport/base_metadata.rb
136
140
  - lib/reso_transport/client.rb
137
141
  - lib/reso_transport/configuration.rb
142
+ - lib/reso_transport/datasystem.rb
143
+ - lib/reso_transport/datasystem_parser.rb
138
144
  - lib/reso_transport/entity_set.rb
139
145
  - lib/reso_transport/entity_type.rb
140
146
  - lib/reso_transport/enum.rb
@@ -160,7 +166,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
160
166
  requirements:
161
167
  - - ">="
162
168
  - !ruby/object:Gem::Version
163
- version: '0'
169
+ version: '2.6'
164
170
  required_rubygems_version: !ruby/object:Gem::Requirement
165
171
  requirements:
166
172
  - - ">="