reso_transport 1.0.0

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.
@@ -0,0 +1,47 @@
1
+ module ResoTransport
2
+ EntityType = Struct.new(:name, :base_type, :primary_key, :schema) do
3
+
4
+ def self.from_stream(args)
5
+ new(args["Name"], args["BaseType"])
6
+ end
7
+
8
+ def parse(record)
9
+ record.each_pair do |k,v|
10
+ next if v.nil?
11
+ if property = (property_map[k] || navigation_property_map[k])
12
+ record[k] = property.parse(v)
13
+ end
14
+ end
15
+ end
16
+
17
+ def parse_value(record)
18
+ record.each_pair do |k,v|
19
+ next if v.nil?
20
+ if property = (property_map[k] || navigation_property_map[k])
21
+ record[k] = property.parse(v)
22
+ end
23
+ end
24
+ end
25
+
26
+ def property_map
27
+ @property_map ||= properties.inject({}) {|hsh, p| hsh[p.name] = p; hsh }
28
+ end
29
+
30
+ def properties
31
+ @properties ||= []
32
+ end
33
+
34
+ def navigation_property_map
35
+ @navigation_property_map ||= navigation_properties.inject({}) {|hsh, p| hsh[p.name] = p; hsh }
36
+ end
37
+
38
+ def navigation_properties
39
+ @navigation_properties ||= []
40
+ end
41
+
42
+ def enumerations
43
+ @enumerations ||= []
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,37 @@
1
+ module ResoTransport
2
+ Member = Struct.new(:name, :value, :annotation) do
3
+ def self.from_stream(args)
4
+ new(args["Name"], args["Value"])
5
+ end
6
+ end
7
+
8
+ Enum = Struct.new(:name, :type, :is_flags) do
9
+ def self.from_stream(args)
10
+ new("#{args[:schema].namespace}.#{args["Name"]}", args["UnderlyingType"], args["IsFlags"])
11
+ end
12
+
13
+ def members
14
+ @members ||= []
15
+ end
16
+
17
+ def parse_value(value)
18
+ mapping.fetch(value, value)
19
+ end
20
+
21
+ def encode_value(value)
22
+ "'#{mapping.invert.fetch(value, value)}'"
23
+ end
24
+
25
+ def mapping
26
+ @mapping ||= generate_member_map || {}
27
+ end
28
+
29
+ def generate_member_map
30
+ members.map {|mem|
31
+ { mem.name => mem.annotation || mem.name }
32
+ }.reduce(:merge!)
33
+ end
34
+
35
+ end
36
+ end
37
+
@@ -0,0 +1,48 @@
1
+ module ResoTransport
2
+ Metadata = Struct.new(:client) do
3
+
4
+ MIME_TYPES = {
5
+ xml: "application/xml",
6
+ json: "application/json"
7
+ }
8
+
9
+ def entity_sets
10
+ parser.entity_sets
11
+ end
12
+
13
+ def schemas
14
+ parser.schemas
15
+ end
16
+
17
+ def parser
18
+ @parser ||= MetadataParser.new.parse(get_data)
19
+ end
20
+
21
+ def get_data
22
+ if client.md_file
23
+ if File.exist?(client.md_file) && File.size(client.md_file) > 0
24
+ File.new(client.md_file)
25
+ else
26
+ File.open(client.md_file, "w") {|f| f.write(raw) }
27
+ File.new(client.md_file)
28
+ end
29
+ else
30
+ raw
31
+ end
32
+ end
33
+
34
+ def raw
35
+ resp = client.connection.get("$metadata") do |req|
36
+ req.headers['Accept'] = MIME_TYPES[client.vendor.fetch(:metadata_format, :xml).to_sym]
37
+ end
38
+
39
+ if resp.success?
40
+ resp.body
41
+ else
42
+ puts resp.body
43
+ raise "Error getting metadata!"
44
+ end
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,104 @@
1
+ module ResoTransport
2
+ class MetadataParser
3
+ include REXML::StreamListener
4
+
5
+ attr_reader :schemas, :entity_sets, :enumerations
6
+
7
+ def initialize
8
+ @schemas = []
9
+ @entity_sets = []
10
+ @entity_types = []
11
+ @enumerations = []
12
+
13
+ @current_entity_type = nil
14
+ @current_complex_type = nil
15
+ @current_enum_type = nil
16
+ @current_member = nil
17
+ end
18
+
19
+ def parse(doc)
20
+ REXML::Document.parse_stream(doc, self)
21
+ finalize
22
+ return self
23
+ end
24
+
25
+ def finalize
26
+ schemas.each do |s|
27
+ s.entity_types.each do |et|
28
+ et.properties.each do |p|
29
+ p.finalize_type(self)
30
+ end
31
+
32
+ et.navigation_properties.each do |p|
33
+ p.finalize_type(self)
34
+ end
35
+ end
36
+
37
+ s.complex_types.each do |et|
38
+ et.properties.each do |p|
39
+ p.finalize_type(self)
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ # Schema ->
46
+ # EnumType ->
47
+ # Members ->
48
+ # Annotation
49
+ # EntityType ->
50
+ # Key
51
+ # Properties ->
52
+ # enumerations
53
+ #
54
+
55
+ def tag_start(name, args)
56
+ case name
57
+ when "Schema"
58
+ @schemas << ResoTransport::Schema.from_stream(args)
59
+ when "EntitySet"
60
+ @entity_sets << ResoTransport::EntitySet.from_stream(args)
61
+ when "EntityType"
62
+ @current_entity_type = ResoTransport::EntityType.from_stream(args)
63
+ when "ComplexType"
64
+ @current_complex_type = ResoTransport::EntityType.from_stream(args)
65
+ when "PropertyRef"
66
+ @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"
71
+ @current_entity_type.navigation_properties << ResoTransport::Property.from_stream(args)
72
+ when "EnumType"
73
+ @current_enum_type = ResoTransport::Enum.from_stream(args.merge(schema: @schemas.last))
74
+ when "Member"
75
+ @current_member = ResoTransport::Member.from_stream(args)
76
+ when "Annotation"
77
+ if @current_enum_type && @current_member
78
+ @current_member.annotation = args['String']
79
+ end
80
+ end
81
+ rescue => e
82
+ puts e.inspect
83
+ puts "Error processing Tag: #{[name, args].inspect}"
84
+ end
85
+
86
+ def tag_end(name)
87
+ case name
88
+ when "EntityType"
89
+ @current_entity_type.schema = @schemas.last.namespace
90
+ @schemas.last.entity_types << @current_entity_type
91
+ when "ComplexType"
92
+ @current_complex_type.schema = @schemas.last.namespace
93
+ @schemas.last.complex_types << @current_complex_type
94
+ when "EnumType"
95
+ @enumerations << @current_enum_type
96
+ @current_enum_type = nil
97
+ when "Member"
98
+ @current_enum_type.members << @current_member
99
+ @current_member = nil
100
+ end
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,101 @@
1
+ module ResoTransport
2
+ Property = Struct.new(:name, :data_type, :attrs, :multi, :enum, :complex_type, :entity_type) do
3
+
4
+ def self.from_stream(args)
5
+ new(args["Name"], args["Type"], args)
6
+ end
7
+
8
+ def parse(value)
9
+ case value
10
+ when Array
11
+ value.map {|v| parser_object.parse_value(v) }
12
+ else
13
+ if multi
14
+ value.split(',').map(&:strip).map {|v| parser_object.parse_value(v) }
15
+ else
16
+ parser_object.parse_value(value)
17
+ end
18
+ end
19
+ end
20
+
21
+ def parse_value(value)
22
+ case data_type
23
+ when "Edm.DateTimeOffset"
24
+ DateTime.parse(value)
25
+ when "Edm.Date"
26
+ Date.parse(value)
27
+ else
28
+ value
29
+ end
30
+ end
31
+
32
+ def encode(value)
33
+ case value
34
+ when Array
35
+ value.map {|v| parser_object.encode_value(v) }
36
+ else
37
+ parser_object.encode_value(value)
38
+ end
39
+ end
40
+
41
+ def encode_value(value)
42
+ case data_type
43
+ when "Edm.String"
44
+ "'#{value}'"
45
+ when "Edm.DateTimeOffset"
46
+ if value.respond_to?(:to_datetime)
47
+ value.to_datetime.strftime(ODATA_TIME_FORMAT)
48
+ else
49
+ DateTime.parse(value).strftime(ODATA_TIME_FORMAT)
50
+ end
51
+ else
52
+ value
53
+ end
54
+ end
55
+
56
+ def parser_object
57
+ enum || complex_type || entity_type || self
58
+ end
59
+
60
+ def finalize_type(parser)
61
+ type_name, is_collection = case self.data_type
62
+ when /^Collection\((.*)\)$/
63
+ [$1, true]
64
+ when /^Edm\.(.*)$/
65
+ [$1, false]
66
+ else
67
+ [self.data_type, false]
68
+ end
69
+
70
+ if enum = parser.enumerations.detect {|e| e.name == type_name }
71
+ self.multi = is_collection || enum.is_flags
72
+ self.enum = enum
73
+ end
74
+
75
+ schema_name, collection_name = ResoTransport.split_schema_and_class_name(type_name)
76
+ if schema = parser.schemas.detect {|e| e.namespace == schema_name }
77
+ if complex_type = schema.complex_types.detect {|c| c.name == collection_name }
78
+ self.multi = is_collection
79
+ self.complex_type = complex_type
80
+ end
81
+
82
+ if entity_type = schema.entity_types.detect {|et| et.name == collection_name }
83
+ self.multi = is_collection
84
+ self.entity_type = entity_type
85
+ end
86
+ end
87
+
88
+ end
89
+
90
+ def method_missing(name, *args, &block)
91
+ self.attrs[name] || self.attrs[camelize(name)] || super
92
+ end
93
+
94
+ private
95
+
96
+ def camelize(name)
97
+ name.to_s.split("_").map(&:capitalize).join
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,147 @@
1
+ module ResoTransport
2
+ Query = Struct.new(:resource) do
3
+
4
+ def all(&block)
5
+ new_query_context('and')
6
+ instance_eval(&block)
7
+ clear_query_context
8
+ return self
9
+ end
10
+
11
+ def any(&block)
12
+ new_query_context('or')
13
+ instance_eval(&block)
14
+ clear_query_context
15
+ return self
16
+ end
17
+
18
+ [:eq, :ne, :gt, :ge, :lt, :le].each do |op|
19
+ define_method(op) do |conditions|
20
+ conditions.each_pair do |k,v|
21
+ current_query_context << "#{k} #{op} #{encode_value(k, v)}"
22
+ end
23
+ return self
24
+ end
25
+ end
26
+
27
+ def limit(size)
28
+ options[:top] = size
29
+ return self
30
+ end
31
+
32
+ def offset(size)
33
+ options[:skip] = size
34
+ return self
35
+ end
36
+
37
+ def order(field, dir=nil)
38
+ options[:orderby] = [field, dir].join(" ").strip
39
+ return self
40
+ end
41
+
42
+ def include_count
43
+ options[:count] = true
44
+ return self
45
+ end
46
+
47
+ def select(*fields)
48
+ os = options.fetch(:select, "").split(",")
49
+ options[:select] = (os + Array(fields)).uniq.join(",")
50
+
51
+ return self
52
+ end
53
+
54
+ def expand(*names)
55
+ ex = options.fetch(:expand, "").split(",")
56
+ options[:expand] = (ex + Array(names)).uniq.join(",")
57
+
58
+ return self
59
+ end
60
+
61
+ def results
62
+ resp = execute
63
+
64
+ if resp[:success]
65
+ resp[:results]
66
+ else
67
+ puts resp[:meta]
68
+ raise "Request Failed"
69
+ end
70
+ end
71
+
72
+ def execute
73
+ resp = resource.get(compile_params)
74
+ parsed_body = JSON.parse(resp.body)
75
+ results = Array(parsed_body.delete("value"))
76
+
77
+ {
78
+ success: resp.success? && !parsed_body.has_key?("error"),
79
+ meta: parsed_body,
80
+ results: resource.parse(results)
81
+ }
82
+ end
83
+
84
+ def new_query_context(context)
85
+ @last_query_context ||= 0
86
+ @current_query_context = @last_query_context + 1
87
+ sub_queries[@current_query_context][:context] = context
88
+ end
89
+
90
+ def clear_query_context
91
+ @last_query_context = @current_query_context
92
+ @current_query_context = nil
93
+ end
94
+
95
+ def current_query_context
96
+ @current_query_context ||= nil
97
+ sub_queries[@current_query_context || :global][:criteria]
98
+ end
99
+
100
+ def options
101
+ @options ||= {}
102
+ end
103
+
104
+ def sub_queries
105
+ @sub_queries ||= Hash.new {|h,k| h[k] = { context: 'and', criteria: [] } }
106
+ end
107
+
108
+ def compile_filters
109
+ groups = sub_queries.dup
110
+ global = groups.delete(:global)
111
+ filter_groups = groups.values
112
+
113
+ filter_string = ""
114
+
115
+ if global && global[:criteria]&.any?
116
+ filter_string << global[:criteria].join(" #{global[:context]} ")
117
+ end
118
+
119
+ filter_string << filter_groups.map do |g|
120
+ "(#{g[:criteria].join(" #{g[:context]} ")})"
121
+ end.join(" and ")
122
+
123
+ filter_string
124
+ end
125
+
126
+ def compile_params
127
+ params = {}
128
+
129
+ options.each_pair do |k,v|
130
+ params["$#{k}"] = v
131
+ end
132
+
133
+ if !sub_queries.empty?
134
+ params["$filter"] = compile_filters
135
+ end
136
+
137
+ params
138
+ end
139
+
140
+ def encode_value(key, v)
141
+ field = resource.property(key.to_s)
142
+ raise "Couldn't find property #{key} for #{resource.name}" if field.nil?
143
+ field.encode(v)
144
+ end
145
+
146
+ end
147
+ end
@@ -0,0 +1,55 @@
1
+ module ResoTransport
2
+ Resource = Struct.new(:client, :entity_set) do
3
+
4
+ def query
5
+ Query.new(self)
6
+ end
7
+
8
+ def name
9
+ entity_set.name
10
+ end
11
+
12
+ def property(name)
13
+ properties.detect {|p| p.name == name }
14
+ end
15
+
16
+ def properties
17
+ entity_type.properties
18
+ end
19
+
20
+ def expandable
21
+ entity_type.navigation_properties
22
+ end
23
+
24
+ def entity_type
25
+ @entity_type ||= schema.entity_types.detect {|et| et.name == entity_set.entity_type }
26
+ end
27
+
28
+ def schema
29
+ @schema ||= md.schemas.detect {|s| s.namespace == entity_set.schema }
30
+ end
31
+
32
+ def md
33
+ client.metadata
34
+ end
35
+
36
+ def parse(results)
37
+ results.map {|r| entity_type.parse(r) }
38
+ end
39
+
40
+ def get(params)
41
+ client.connection.get(name, params) do |req|
42
+ req.headers['Accept'] = 'application/json'
43
+ end
44
+ end
45
+
46
+ def to_s
47
+ %(#<ResoTransport::Resource entity_set="#{name}", schema="#{schema&.namespace}">)
48
+ end
49
+
50
+ def inspect
51
+ to_s
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,21 @@
1
+ module ResoTransport
2
+ Schema = Struct.new(:namespace) do
3
+
4
+ def self.from_stream(args)
5
+ new(args["Namespace"])
6
+ end
7
+
8
+ def entity_types
9
+ @entity_types ||= []
10
+ end
11
+
12
+ def complex_types
13
+ @complex_types ||= []
14
+ end
15
+
16
+ def enumerations
17
+ @enumerations ||= []
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module ResoTransport
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,57 @@
1
+ require 'rexml/document'
2
+ require 'rexml/streamlistener'
3
+ require 'logger'
4
+ require 'faraday'
5
+ require 'json'
6
+ require 'time'
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_parser"
15
+ require "reso_transport/schema"
16
+ require "reso_transport/entity_set"
17
+ require "reso_transport/entity_type"
18
+ require "reso_transport/enum"
19
+ require "reso_transport/property"
20
+ require "reso_transport/query"
21
+
22
+
23
+
24
+ module Faraday
25
+ module Utils
26
+
27
+ def escape(str)
28
+ str.to_s.gsub(ESCAPE_RE) do |match|
29
+ '%' + match.unpack('H2' * match.bytesize).join('%').upcase
30
+ end.gsub(" ","%20")
31
+
32
+ end
33
+ end
34
+ end
35
+
36
+ module ResoTransport
37
+ class Error < StandardError; end
38
+ class AccessDenied < StandardError; end
39
+ ODATA_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S%Z"
40
+
41
+ class << self
42
+ attr_writer :configuration
43
+ end
44
+
45
+ def self.configuration
46
+ @configuration ||= Configuration.new
47
+ end
48
+
49
+ def self.configure
50
+ yield(configuration)
51
+ end
52
+
53
+ def self.split_schema_and_class_name(s)
54
+ s.partition(/(\w+)$/).first(2).map {|s| s.sub(/\.$/, '') }
55
+ end
56
+
57
+ end
@@ -0,0 +1,35 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "reso_transport/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "reso_transport"
8
+ spec.version = ResoTransport::VERSION
9
+ spec.authors = ["Jon Druse"]
10
+ spec.email = ["jon@wrstudios.com"]
11
+
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"
16
+
17
+
18
+ # Specify which files should be added to the gem when it is released.
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
21
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
+ end
23
+ spec.bindir = "exe"
24
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
+ spec.require_paths = ["lib"]
26
+
27
+ spec.add_dependency "faraday", "~> 0.17.0"
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.17"
30
+ spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "minitest", "~> 5.0"
32
+ spec.add_development_dependency "minitest-rg", "~> 5.0"
33
+ spec.add_development_dependency "vcr", "~> 5.0"
34
+ spec.add_development_dependency "byebug"
35
+ end
@@ -0,0 +1,26 @@
1
+ :bridge:
2
+ :md_file: PATH_TO_METADATA_FILE
3
+ :vendor:
4
+ :name: bridge_interactive
5
+ :endpoint: ENDPOINT_URL_HERE
6
+ :authentication:
7
+ :access_token: ACCESS_TOKEN_HERE
8
+
9
+ :trestle:
10
+ :md_file: PATH_TO_METADATA_FILE
11
+ :vendor:
12
+ :name: trestle
13
+ :endpoint: ENDPOINT_URL_HERE
14
+ :authentication:
15
+ :endpoint: AUTH_ENDPOINT_HERE
16
+ :client_id: CLIENT_ID_HERE
17
+ :client_secret: CLIENT_SECRET_HERE
18
+
19
+ :spark:
20
+ :md_file: PATH_TO_METADATA_FILE
21
+ :vendor:
22
+ :name: fbs
23
+ :endpoint: ENDPOINT_URL_HERE
24
+ :authentication:
25
+ :access_token: ACCESS_TOKEN_HERE
26
+