reso_transport 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+