frodo 0.10.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.
- checksums.yaml +7 -0
- data/.autotest +2 -0
- data/.circleci/config.yml +54 -0
- data/.gitignore +24 -0
- data/.gitlab-ci.yml +9 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +75 -0
- data/CHANGELOG.md +163 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +23 -0
- data/README.md +479 -0
- data/Rakefile +7 -0
- data/TODO.md +55 -0
- data/frodo.gemspec +39 -0
- data/images/frodo.jpg +0 -0
- data/lib/frodo/abstract_client.rb +11 -0
- data/lib/frodo/client.rb +6 -0
- data/lib/frodo/concerns/api.rb +292 -0
- data/lib/frodo/concerns/authentication.rb +32 -0
- data/lib/frodo/concerns/base.rb +84 -0
- data/lib/frodo/concerns/caching.rb +26 -0
- data/lib/frodo/concerns/connection.rb +79 -0
- data/lib/frodo/concerns/verbs.rb +68 -0
- data/lib/frodo/config.rb +143 -0
- data/lib/frodo/entity.rb +335 -0
- data/lib/frodo/entity_container.rb +75 -0
- data/lib/frodo/entity_set.rb +131 -0
- data/lib/frodo/errors.rb +70 -0
- data/lib/frodo/middleware/authentication/token.rb +13 -0
- data/lib/frodo/middleware/authentication.rb +87 -0
- data/lib/frodo/middleware/authorization.rb +18 -0
- data/lib/frodo/middleware/caching.rb +30 -0
- data/lib/frodo/middleware/custom_headers.rb +14 -0
- data/lib/frodo/middleware/gzip.rb +33 -0
- data/lib/frodo/middleware/instance_url.rb +20 -0
- data/lib/frodo/middleware/logger.rb +42 -0
- data/lib/frodo/middleware/multipart.rb +64 -0
- data/lib/frodo/middleware/odata_headers.rb +13 -0
- data/lib/frodo/middleware/raise_error.rb +47 -0
- data/lib/frodo/middleware.rb +33 -0
- data/lib/frodo/navigation_property/proxy.rb +80 -0
- data/lib/frodo/navigation_property.rb +29 -0
- data/lib/frodo/properties/binary.rb +50 -0
- data/lib/frodo/properties/boolean.rb +37 -0
- data/lib/frodo/properties/collection.rb +50 -0
- data/lib/frodo/properties/complex.rb +114 -0
- data/lib/frodo/properties/date.rb +27 -0
- data/lib/frodo/properties/date_time.rb +83 -0
- data/lib/frodo/properties/date_time_offset.rb +17 -0
- data/lib/frodo/properties/decimal.rb +54 -0
- data/lib/frodo/properties/enum.rb +62 -0
- data/lib/frodo/properties/float.rb +67 -0
- data/lib/frodo/properties/geography/base.rb +162 -0
- data/lib/frodo/properties/geography/line_string.rb +33 -0
- data/lib/frodo/properties/geography/point.rb +31 -0
- data/lib/frodo/properties/geography/polygon.rb +38 -0
- data/lib/frodo/properties/geography.rb +13 -0
- data/lib/frodo/properties/guid.rb +17 -0
- data/lib/frodo/properties/integer.rb +107 -0
- data/lib/frodo/properties/number.rb +14 -0
- data/lib/frodo/properties/string.rb +72 -0
- data/lib/frodo/properties/time.rb +40 -0
- data/lib/frodo/properties/time_of_day.rb +27 -0
- data/lib/frodo/properties.rb +32 -0
- data/lib/frodo/property.rb +139 -0
- data/lib/frodo/property_registry.rb +41 -0
- data/lib/frodo/query/criteria/comparison_operators.rb +49 -0
- data/lib/frodo/query/criteria/date_functions.rb +61 -0
- data/lib/frodo/query/criteria/geography_functions.rb +21 -0
- data/lib/frodo/query/criteria/lambda_operators.rb +27 -0
- data/lib/frodo/query/criteria/string_functions.rb +40 -0
- data/lib/frodo/query/criteria.rb +92 -0
- data/lib/frodo/query/in_batches.rb +58 -0
- data/lib/frodo/query.rb +221 -0
- data/lib/frodo/railtie.rb +19 -0
- data/lib/frodo/schema/complex_type.rb +79 -0
- data/lib/frodo/schema/enum_type.rb +95 -0
- data/lib/frodo/schema.rb +164 -0
- data/lib/frodo/service.rb +199 -0
- data/lib/frodo/service_registry.rb +52 -0
- data/lib/frodo/version.rb +3 -0
- data/lib/frodo.rb +67 -0
- data/spec/fixtures/auth_success_response.json +11 -0
- data/spec/fixtures/error.json +11 -0
- data/spec/fixtures/files/entity_to_xml.xml +18 -0
- data/spec/fixtures/files/error.xml +5 -0
- data/spec/fixtures/files/metadata.xml +150 -0
- data/spec/fixtures/files/metadata_with_error.xml +157 -0
- data/spec/fixtures/files/product_0.json +10 -0
- data/spec/fixtures/files/product_0.xml +28 -0
- data/spec/fixtures/files/products.json +106 -0
- data/spec/fixtures/files/products.xml +308 -0
- data/spec/fixtures/files/supplier_0.json +26 -0
- data/spec/fixtures/files/supplier_0.xml +32 -0
- data/spec/fixtures/leads.json +923 -0
- data/spec/fixtures/refresh_error_response.json +8 -0
- data/spec/frodo/abstract_client_spec.rb +13 -0
- data/spec/frodo/client_spec.rb +57 -0
- data/spec/frodo/concerns/authentication_spec.rb +79 -0
- data/spec/frodo/concerns/base_spec.rb +68 -0
- data/spec/frodo/concerns/caching_spec.rb +40 -0
- data/spec/frodo/concerns/connection_spec.rb +65 -0
- data/spec/frodo/config_spec.rb +127 -0
- data/spec/frodo/entity/shared_examples.rb +83 -0
- data/spec/frodo/entity_container_spec.rb +38 -0
- data/spec/frodo/entity_set_spec.rb +169 -0
- data/spec/frodo/entity_spec.rb +153 -0
- data/spec/frodo/errors_spec.rb +48 -0
- data/spec/frodo/middleware/authentication/token_spec.rb +87 -0
- data/spec/frodo/middleware/authentication_spec.rb +83 -0
- data/spec/frodo/middleware/authorization_spec.rb +17 -0
- data/spec/frodo/middleware/custom_headers_spec.rb +21 -0
- data/spec/frodo/middleware/gzip_spec.rb +68 -0
- data/spec/frodo/middleware/instance_url_spec.rb +27 -0
- data/spec/frodo/middleware/logger_spec.rb +21 -0
- data/spec/frodo/middleware/odata_headers_spec.rb +15 -0
- data/spec/frodo/middleware/raise_error_spec.rb +66 -0
- data/spec/frodo/navigation_property/proxy_spec.rb +46 -0
- data/spec/frodo/navigation_property_spec.rb +55 -0
- data/spec/frodo/properties/binary_spec.rb +50 -0
- data/spec/frodo/properties/boolean_spec.rb +72 -0
- data/spec/frodo/properties/collection_spec.rb +44 -0
- data/spec/frodo/properties/date_spec.rb +23 -0
- data/spec/frodo/properties/date_time_offset_spec.rb +30 -0
- data/spec/frodo/properties/date_time_spec.rb +23 -0
- data/spec/frodo/properties/decimal_spec.rb +50 -0
- data/spec/frodo/properties/float_spec.rb +45 -0
- data/spec/frodo/properties/geography/line_string_spec.rb +33 -0
- data/spec/frodo/properties/geography/point_spec.rb +29 -0
- data/spec/frodo/properties/geography/polygon_spec.rb +55 -0
- data/spec/frodo/properties/geography/shared_examples.rb +72 -0
- data/spec/frodo/properties/guid_spec.rb +17 -0
- data/spec/frodo/properties/integer_spec.rb +58 -0
- data/spec/frodo/properties/string_spec.rb +46 -0
- data/spec/frodo/properties/time_of_day_spec.rb +23 -0
- data/spec/frodo/properties/time_spec.rb +15 -0
- data/spec/frodo/property_registry_spec.rb +16 -0
- data/spec/frodo/property_spec.rb +71 -0
- data/spec/frodo/query/criteria_spec.rb +229 -0
- data/spec/frodo/query_spec.rb +156 -0
- data/spec/frodo/schema/complex_type_spec.rb +97 -0
- data/spec/frodo/schema/enum_type_spec.rb +112 -0
- data/spec/frodo/schema_spec.rb +113 -0
- data/spec/frodo/service_registry_spec.rb +19 -0
- data/spec/frodo/service_spec.rb +153 -0
- data/spec/frodo/usage_example_spec.rb +161 -0
- data/spec/spec_helper.rb +35 -0
- data/spec/support/coverage.rb +2 -0
- data/spec/support/fixture_helpers.rb +14 -0
- data/spec/support/middleware.rb +19 -0
- metadata +479 -0
data/lib/frodo/config.rb
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
|
|
5
|
+
module Frodo
|
|
6
|
+
class << self
|
|
7
|
+
attr_writer :log
|
|
8
|
+
|
|
9
|
+
# Returns the current Configuration
|
|
10
|
+
#
|
|
11
|
+
# Example
|
|
12
|
+
#
|
|
13
|
+
# Frodo.configuration.username = "username"
|
|
14
|
+
# Frodo.configuration.password = "password"
|
|
15
|
+
def configuration
|
|
16
|
+
@configuration ||= Configuration.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Yields the Configuration
|
|
20
|
+
#
|
|
21
|
+
# Example
|
|
22
|
+
#
|
|
23
|
+
# Frodo.configure do |config|
|
|
24
|
+
# config.username = "username"
|
|
25
|
+
# config.password = "password"
|
|
26
|
+
# end
|
|
27
|
+
def configure
|
|
28
|
+
yield configuration
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def log?
|
|
32
|
+
@log ||= false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def log(message)
|
|
36
|
+
return unless Frodo.log?
|
|
37
|
+
configuration.logger.send(configuration.log_level, message)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class Configuration
|
|
42
|
+
class Option
|
|
43
|
+
attr_reader :configuration, :name, :options
|
|
44
|
+
|
|
45
|
+
def self.define(*args)
|
|
46
|
+
new(*args).define
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def initialize(configuration, name, options = {})
|
|
50
|
+
@configuration = configuration
|
|
51
|
+
@name = name
|
|
52
|
+
@options = options
|
|
53
|
+
@default = options.fetch(:default, nil)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def define
|
|
57
|
+
write_attribute
|
|
58
|
+
define_method if default_provided?
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
attr_reader :default
|
|
65
|
+
alias default_provided? default
|
|
66
|
+
|
|
67
|
+
def write_attribute
|
|
68
|
+
configuration.send :attr_accessor, name
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def define_method
|
|
72
|
+
our_default = default
|
|
73
|
+
our_name = name
|
|
74
|
+
configuration.send :define_method, our_name do
|
|
75
|
+
instance_variable_get(:"@#{our_name}") ||
|
|
76
|
+
instance_variable_set(
|
|
77
|
+
:"@#{our_name}",
|
|
78
|
+
our_default.respond_to?(:call) ? our_default.call : our_default
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
class << self
|
|
85
|
+
attr_accessor :options
|
|
86
|
+
|
|
87
|
+
def option(*args)
|
|
88
|
+
option = Option.define(self, *args)
|
|
89
|
+
(self.options ||= []) << option.name
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The OAuth client id
|
|
94
|
+
option :client_id
|
|
95
|
+
|
|
96
|
+
# The OAuth client secret
|
|
97
|
+
option :client_secret
|
|
98
|
+
|
|
99
|
+
option :host, default: 'login.microsoftonline.com'
|
|
100
|
+
|
|
101
|
+
option :oauth_token
|
|
102
|
+
option :refresh_token
|
|
103
|
+
option :instance_url
|
|
104
|
+
option :base_path
|
|
105
|
+
|
|
106
|
+
# Set this to an object that responds to read, write and fetch and all GET
|
|
107
|
+
# requests will be cached.
|
|
108
|
+
option :cache
|
|
109
|
+
|
|
110
|
+
# The number of times reauthentication should be tried before failing.
|
|
111
|
+
option :authentication_retries, default: 3
|
|
112
|
+
|
|
113
|
+
# Set to true if you want responses from Dynamics to be gzip compressed.
|
|
114
|
+
option :compress
|
|
115
|
+
|
|
116
|
+
# Faraday request read/open timeout.
|
|
117
|
+
option :timeout
|
|
118
|
+
|
|
119
|
+
# Faraday adapter to use. Defaults to Faraday.default_adapter.
|
|
120
|
+
option :adapter, default: lambda { Faraday.default_adapter }
|
|
121
|
+
|
|
122
|
+
option :proxy_uri, default: lambda { ENV['FRODATA_PROXY_URI'] }
|
|
123
|
+
|
|
124
|
+
# A Proc that is called with the response body after a successful authentication.
|
|
125
|
+
option :authentication_callback
|
|
126
|
+
|
|
127
|
+
# Set SSL options
|
|
128
|
+
option :ssl, default: {}
|
|
129
|
+
|
|
130
|
+
# A Hash that is converted to HTTP headers
|
|
131
|
+
option :request_headers
|
|
132
|
+
|
|
133
|
+
# Set a logger for when Frodo.log is set to true, defaulting to STDOUT
|
|
134
|
+
option :logger, default: ::Logger.new(STDOUT)
|
|
135
|
+
|
|
136
|
+
# Set a log level for logging when Frodo.log is set to true, defaulting to :debug
|
|
137
|
+
option :log_level, default: :debug
|
|
138
|
+
|
|
139
|
+
def options
|
|
140
|
+
self.class.options
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
data/lib/frodo/entity.rb
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
module Frodo
|
|
2
|
+
# An Frodo::Entity represents a single record returned by the service. All
|
|
3
|
+
# Entities have a type and belong to a specific namespace. They are written
|
|
4
|
+
# back to the service via the EntitySet they came from. Frodo::Entity
|
|
5
|
+
# instances should not be instantiated directly; instead, they should either
|
|
6
|
+
# be read or instantiated from their respective Frodo::EntitySet.
|
|
7
|
+
class Entity
|
|
8
|
+
# The Entity type name
|
|
9
|
+
attr_reader :type
|
|
10
|
+
# The Frodo::Service's identifying name
|
|
11
|
+
attr_reader :service_name
|
|
12
|
+
# The entity set this entity belongs to
|
|
13
|
+
attr_reader :entity_set
|
|
14
|
+
# List of errors on entity
|
|
15
|
+
attr_reader :errors
|
|
16
|
+
|
|
17
|
+
PROPERTY_NOT_LOADED = :not_loaded
|
|
18
|
+
|
|
19
|
+
XML_NAMESPACES = {
|
|
20
|
+
'xmlns' => 'http://www.w3.org/2005/Atom',
|
|
21
|
+
'xmlns:data' => 'http://docs.oasis-open.org/odata/ns/data',
|
|
22
|
+
'xmlns:metadata' => 'http://docs.oasis-open.org/odata/ns/metadata',
|
|
23
|
+
'xmlns:georss' => 'http://www.georss.org/georss',
|
|
24
|
+
'xmlns:gml' => 'http://www.opengis.net/gml',
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
# Initializes a bare Entity
|
|
28
|
+
# @param options [Hash]
|
|
29
|
+
def initialize(options = {})
|
|
30
|
+
@id = options[:id]
|
|
31
|
+
@type = options[:type]
|
|
32
|
+
@service_name = options[:service_name]
|
|
33
|
+
@entity_set = options[:entity_set]
|
|
34
|
+
@context = options[:context]
|
|
35
|
+
@links = options[:links]
|
|
36
|
+
@errors = []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def namespace
|
|
40
|
+
@namespace ||= type.rpartition('.').first
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns name of Entity from Service specified type.
|
|
44
|
+
# @return [String]
|
|
45
|
+
def name
|
|
46
|
+
@name ||= type.split('.').last
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns context URL for this entity
|
|
50
|
+
# @return [String]
|
|
51
|
+
def context
|
|
52
|
+
@context ||= context_url
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Get property value
|
|
56
|
+
# @param property_name [to_s]
|
|
57
|
+
# @return [*]
|
|
58
|
+
def [](property_name)
|
|
59
|
+
if get_property(property_name).is_a?(::Frodo::Properties::Complex)
|
|
60
|
+
get_property(property_name)
|
|
61
|
+
else
|
|
62
|
+
get_property(property_name).value
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Set property value
|
|
67
|
+
# @param property_name [to_s]
|
|
68
|
+
# @param value [*]
|
|
69
|
+
def []=(property_name, value)
|
|
70
|
+
get_property(property_name).value = value
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def get_property(property_name)
|
|
74
|
+
prop_name = property_name.to_s
|
|
75
|
+
# Property is lazy loaded
|
|
76
|
+
if properties_xml_value.has_key?(prop_name)
|
|
77
|
+
property = instantiate_property(prop_name, properties_xml_value[prop_name])
|
|
78
|
+
set_property(prop_name, property.dup)
|
|
79
|
+
properties_xml_value.delete(prop_name)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
if properties.has_key? prop_name
|
|
83
|
+
properties[prop_name]
|
|
84
|
+
elsif navigation_properties.has_key? prop_name
|
|
85
|
+
navigation_properties[prop_name]
|
|
86
|
+
else
|
|
87
|
+
raise ArgumentError, "Unknown property: #{property_name}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def property_names
|
|
92
|
+
[
|
|
93
|
+
@properties_xml_value.andand.keys,
|
|
94
|
+
@properties.andand.keys
|
|
95
|
+
].compact.flatten
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def navigation_property_names
|
|
99
|
+
navigation_properties.keys
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def navigation_properties
|
|
103
|
+
@navigation_properties ||= links.keys.map do |nav_name|
|
|
104
|
+
[
|
|
105
|
+
nav_name,
|
|
106
|
+
Frodo::NavigationProperty::Proxy.new(self, nav_name)
|
|
107
|
+
]
|
|
108
|
+
end.to_h
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Links to other Frodo entitites
|
|
112
|
+
# @return [Hash]
|
|
113
|
+
def links
|
|
114
|
+
@links ||= schema.navigation_properties[name].map do |nav_name, details|
|
|
115
|
+
[
|
|
116
|
+
nav_name,
|
|
117
|
+
{ type: details.nav_type, href: "#{id}/#{nav_name}" }
|
|
118
|
+
]
|
|
119
|
+
end.to_h
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Create Entity with provided properties and options.
|
|
123
|
+
# @param new_properties [Hash]
|
|
124
|
+
# @param options [Hash]
|
|
125
|
+
# @param [Frodo::Entity]
|
|
126
|
+
def self.with_properties(new_properties = {}, options = {})
|
|
127
|
+
entity = Frodo::Entity.new(options)
|
|
128
|
+
entity.instance_eval do
|
|
129
|
+
service.properties_for_entity(type).each do |property_name, instance|
|
|
130
|
+
set_property(property_name, instance)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
new_properties.each do |property_name, property_value|
|
|
134
|
+
self[property_name] = property_value
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
entity
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Create Entity from JSON document with provided options.
|
|
141
|
+
# @param json [Hash|to_s]
|
|
142
|
+
# @param options [Hash]
|
|
143
|
+
# @return [Frodo::Entity]
|
|
144
|
+
def self.from_json(json, options = {})
|
|
145
|
+
return nil if json.nil?
|
|
146
|
+
json = JSON.parse(json.to_s) unless json.is_a?(Hash)
|
|
147
|
+
metadata = extract_metadata(json)
|
|
148
|
+
options.merge!(context: metadata['@odata.context'])
|
|
149
|
+
p json
|
|
150
|
+
p options
|
|
151
|
+
entity = with_properties(json, options)
|
|
152
|
+
process_metadata(entity, metadata)
|
|
153
|
+
entity
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Create Entity from XML document with provided options.
|
|
157
|
+
# @param xml_doc [Nokogiri::XML]
|
|
158
|
+
# @param options [Hash]
|
|
159
|
+
# @return [Frodo::Entity]
|
|
160
|
+
def self.from_xml(xml_doc, options = {})
|
|
161
|
+
return nil if xml_doc.nil?
|
|
162
|
+
entity = Frodo::Entity.new(options)
|
|
163
|
+
process_properties(entity, xml_doc)
|
|
164
|
+
process_links(entity, xml_doc)
|
|
165
|
+
entity
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Converts Entity to its XML representation.
|
|
169
|
+
# @return [String]
|
|
170
|
+
def to_xml
|
|
171
|
+
namespaces = XML_NAMESPACES.merge('xml:base' => service.service_url)
|
|
172
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
|
173
|
+
xml.entry(namespaces) do
|
|
174
|
+
xml.category(term: type,
|
|
175
|
+
scheme: 'http://docs.oasis-open.org/odata/ns/scheme')
|
|
176
|
+
xml.author { xml.name }
|
|
177
|
+
|
|
178
|
+
xml.content(type: 'application/xml') do
|
|
179
|
+
xml['metadata'].properties do
|
|
180
|
+
property_names.each do |name|
|
|
181
|
+
next if name == primary_key
|
|
182
|
+
get_property(name).to_xml(xml)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
builder.to_xml
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Converts Entity to its JSON representation.
|
|
192
|
+
# @return [String]
|
|
193
|
+
def to_json
|
|
194
|
+
# TODO: add @odata.context
|
|
195
|
+
to_hash.to_json
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Converts Entity to a hash.
|
|
199
|
+
# @return [Hash]
|
|
200
|
+
def to_hash
|
|
201
|
+
property_names.map do |name|
|
|
202
|
+
[name, get_property(name).json_value]
|
|
203
|
+
end.to_h
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Returns the canonical URL for this entity
|
|
207
|
+
# @return [String]
|
|
208
|
+
def id
|
|
209
|
+
@id ||= lambda {
|
|
210
|
+
entity_set = self.entity_set.andand.name
|
|
211
|
+
entity_set ||= context.split('#').last.split('/').first
|
|
212
|
+
"#{entity_set}(#{self[primary_key]})"
|
|
213
|
+
}.call
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Returns the primary key for the Entity.
|
|
217
|
+
# @return [String]
|
|
218
|
+
def primary_key
|
|
219
|
+
schema.primary_key_for(name)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def is_new?
|
|
223
|
+
self[primary_key].nil?
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def any_errors?
|
|
227
|
+
!errors.empty?
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def service
|
|
231
|
+
@service ||= Frodo::ServiceRegistry[service_name]
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def schema
|
|
235
|
+
@schema ||= service.schemas[namespace]
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
private
|
|
239
|
+
|
|
240
|
+
def instantiate_property(property_name, value_xml)
|
|
241
|
+
prop_type = schema.get_property_type(name, property_name)
|
|
242
|
+
prop_type, value_type = prop_type.split(/\(|\)/)
|
|
243
|
+
|
|
244
|
+
if prop_type == 'Collection'
|
|
245
|
+
klass = ::Frodo::Properties::Collection
|
|
246
|
+
options = { value_type: value_type }
|
|
247
|
+
else
|
|
248
|
+
klass = ::Frodo::PropertyRegistry[prop_type]
|
|
249
|
+
options = {}
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
if klass.nil?
|
|
253
|
+
raise RuntimeError, "Unknown property type: #{prop_type}"
|
|
254
|
+
else
|
|
255
|
+
klass.from_xml(value_xml, options.merge(service: service))
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def properties
|
|
260
|
+
@properties ||= {}
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def properties_xml_value
|
|
264
|
+
@properties_xml_value ||= {}
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Computes the entity's canonical context URL
|
|
268
|
+
def context_url
|
|
269
|
+
"#{service.service_url}/$metadata##{entity_set.name}/$entity"
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def set_property(name, property)
|
|
273
|
+
properties[name.to_s] = property
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Instantiating properties takes time, so we can lazy load properties by passing xml_value and lookup when needed
|
|
277
|
+
def set_property_lazy_load(name, xml_value )
|
|
278
|
+
properties_xml_value[name.to_s] = xml_value
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def self.process_properties(entity, xml_doc)
|
|
282
|
+
entity.instance_eval do
|
|
283
|
+
unless instance_variable_get(:@context)
|
|
284
|
+
context = xml_doc.xpath('/entry').first.andand['context']
|
|
285
|
+
instance_variable_set(:@context, context)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
xml_doc.xpath('./content/properties/*').each do |property_xml|
|
|
289
|
+
# Doing lazy loading here because instantiating each object takes a long time
|
|
290
|
+
set_property_lazy_load(property_xml.name, property_xml)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def self.process_links(entity, xml_doc)
|
|
296
|
+
entity.instance_eval do
|
|
297
|
+
new_links = instance_variable_get(:@links) || {}
|
|
298
|
+
schema.navigation_properties[name].each do |nav_name, details|
|
|
299
|
+
p nav_name
|
|
300
|
+
xml_doc.xpath("./link[@title='#{nav_name}']").each do |node|
|
|
301
|
+
next if node.attributes['type'].nil?
|
|
302
|
+
next unless node.attributes['type'].value =~ /^application\/atom\+xml;type=(feed|entry)$/i
|
|
303
|
+
link_type = node.attributes['type'].value =~ /type=entry$/i ? :entity : :collection
|
|
304
|
+
new_links[nav_name] = {
|
|
305
|
+
type: link_type,
|
|
306
|
+
href: node.attributes['href'].value
|
|
307
|
+
}
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
instance_variable_set(:@links, new_links)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def self.extract_metadata(json)
|
|
315
|
+
metadata = json.select { |key, val| key =~ /@odata/ }
|
|
316
|
+
json.delete_if { |key, val| key =~ /@odata/ }
|
|
317
|
+
metadata
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def self.process_metadata(entity, metadata)
|
|
321
|
+
entity.instance_eval do
|
|
322
|
+
new_links = instance_variable_get(:@links) || {}
|
|
323
|
+
schema.navigation_properties[name].each do |nav_name, details|
|
|
324
|
+
href = metadata["#{nav_name}@odata.navigationLink"]
|
|
325
|
+
next if href.nil?
|
|
326
|
+
new_links[nav_name] = {
|
|
327
|
+
type: details.nav_type,
|
|
328
|
+
href: href
|
|
329
|
+
}
|
|
330
|
+
end
|
|
331
|
+
instance_variable_set(:@links, new_links) unless new_links.empty?
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Frodo
|
|
2
|
+
#
|
|
3
|
+
class EntityContainer
|
|
4
|
+
# The EntityContainer's parent service
|
|
5
|
+
attr_reader :service
|
|
6
|
+
# The EntityContainer's metadata
|
|
7
|
+
attr_reader :metadata
|
|
8
|
+
|
|
9
|
+
# Creates a new EntityContainer
|
|
10
|
+
# @param service [Frodo::Service] The entity container's parent service
|
|
11
|
+
def initialize(service)
|
|
12
|
+
@metadata = service.metadata.xpath('//EntityContainer').first
|
|
13
|
+
@service = service
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# The EntityContainer's surrounding Schema
|
|
17
|
+
# @return [Nokogiri::XML]
|
|
18
|
+
def schema
|
|
19
|
+
@schema ||= metadata.ancestors('Schema').first
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns the EntityContainer's namespace.
|
|
23
|
+
# @return [String]
|
|
24
|
+
def namespace
|
|
25
|
+
@namespace ||= schema.attributes['Namespace'].value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns the EntityContainer's name.
|
|
29
|
+
# @return [String]
|
|
30
|
+
def name
|
|
31
|
+
@name ||= metadata.attributes['Name'].value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns a hash of EntitySet names and their respective EntityTypes.
|
|
35
|
+
# @return [Hash<String, String>]
|
|
36
|
+
def entity_sets
|
|
37
|
+
@entity_sets ||= metadata.xpath('//EntitySet').map do |entity|
|
|
38
|
+
[
|
|
39
|
+
entity.attributes['Name'].value,
|
|
40
|
+
entity.attributes['EntityType'].value
|
|
41
|
+
]
|
|
42
|
+
end.to_h
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Retrieves the EntitySet associated with a specific EntityType by name
|
|
46
|
+
#
|
|
47
|
+
# @param entity_set_name [to_s] the name of the EntitySet desired
|
|
48
|
+
# @return [Frodo::EntitySet] an Frodo::EntitySet to query
|
|
49
|
+
def [](entity_set_name)
|
|
50
|
+
xpath_query = "//EntitySet[@Name='#{entity_set_name}']"
|
|
51
|
+
entity_set_node = metadata.xpath(xpath_query).first
|
|
52
|
+
raise ArgumentError, "Unknown Entity Set: #{entity_set_name}" if entity_set_node.nil?
|
|
53
|
+
entity_type = entity_set_node.attributes['EntityType'].value
|
|
54
|
+
Frodo::EntitySet.new(
|
|
55
|
+
name: entity_set_name,
|
|
56
|
+
namespace: namespace,
|
|
57
|
+
type: entity_type,
|
|
58
|
+
service_name: service.name,
|
|
59
|
+
container: name
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def singletons
|
|
64
|
+
# TODO return singletons exposed by this EntityContainer
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def actions
|
|
68
|
+
# TODO return action imports exposed by this EntityContainer
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def functions
|
|
72
|
+
# TODO return function imports exposed by this EntityContainer
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
module Frodo
|
|
2
|
+
# This class represents a set of entities within an Frodo service. It is
|
|
3
|
+
# instantiated whenever an Frodo::Service is asked for an EntitySet via the
|
|
4
|
+
# Frodo::Service#[] method call. It also provides Enumerable behavior so that
|
|
5
|
+
# you can interact with the entities within a set in a very comfortable way.
|
|
6
|
+
#
|
|
7
|
+
# This class also implements a query interface for finding certain entities
|
|
8
|
+
# based on query criteria or limiting the result set returned by the set. This
|
|
9
|
+
# functionality is implemented through transparent proxy objects.
|
|
10
|
+
class EntitySet
|
|
11
|
+
include Enumerable
|
|
12
|
+
|
|
13
|
+
# The name of the EntitySet
|
|
14
|
+
attr_reader :name
|
|
15
|
+
# The Entity type for the EntitySet
|
|
16
|
+
attr_reader :type
|
|
17
|
+
# The Frodo::Service's namespace
|
|
18
|
+
attr_reader :namespace
|
|
19
|
+
# The Frodo::Service's identifiable name
|
|
20
|
+
attr_reader :service_name
|
|
21
|
+
# The EntitySet's container name
|
|
22
|
+
attr_reader :container
|
|
23
|
+
|
|
24
|
+
# Sets up the EntitySet to permit querying for the resources in the set.
|
|
25
|
+
#
|
|
26
|
+
# @param options [Hash] the options to setup the EntitySet
|
|
27
|
+
# @return [Frodo::EntitySet] an instance of the EntitySet
|
|
28
|
+
def initialize(options = {})
|
|
29
|
+
@name = options[:name]
|
|
30
|
+
@type = options[:type]
|
|
31
|
+
@namespace = options[:namespace]
|
|
32
|
+
@service_name = options[:service_name]
|
|
33
|
+
@container = options[:container]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Provided for Enumerable functionality
|
|
37
|
+
#
|
|
38
|
+
# @param block [block] a block to evaluate
|
|
39
|
+
# @return [Frodo::Entity] each entity in turn from this set
|
|
40
|
+
# def each(&block)
|
|
41
|
+
# query.execute.each(&block)
|
|
42
|
+
# end
|
|
43
|
+
|
|
44
|
+
# # Return the first `n` Entities for the set.
|
|
45
|
+
# # If count is 1 it returns the single entity, otherwise its an array of entities
|
|
46
|
+
# # @return [Frodo::EntitySet]
|
|
47
|
+
# def first(count = 1)
|
|
48
|
+
# result = query.limit(count).execute
|
|
49
|
+
# count == 1 ? result.first : result.to_a
|
|
50
|
+
# end
|
|
51
|
+
|
|
52
|
+
# # Returns the number of entities within the set.
|
|
53
|
+
# # Not supported in Microsoft CRM2011
|
|
54
|
+
# # @return [Integer]
|
|
55
|
+
# def count
|
|
56
|
+
# query.count
|
|
57
|
+
# end
|
|
58
|
+
|
|
59
|
+
# Create a new Entity for this set with the given properties.
|
|
60
|
+
# @param properties [Hash] property name as key and it's initial value
|
|
61
|
+
# @return [Frodo::Entity]
|
|
62
|
+
def new_entity(properties = {})
|
|
63
|
+
Frodo::Entity.with_properties(properties, entity_options)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Returns a query targetted at the current EntitySet.
|
|
67
|
+
# @param options [Hash] query options
|
|
68
|
+
# @return [Frodo::Query]
|
|
69
|
+
def query(options = {})
|
|
70
|
+
Frodo::Query.new(self, options)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def entity_primary_key()
|
|
75
|
+
new_entity.primary_key
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Find the Entity with the supplied key value.
|
|
79
|
+
# @param key [to_s] primary key to lookup
|
|
80
|
+
# @return [Frodo::Entity,nil]
|
|
81
|
+
# def [](key, options={})
|
|
82
|
+
# properties_to_expand = if options[:expand] == :all
|
|
83
|
+
# new_entity.navigation_property_names
|
|
84
|
+
# else
|
|
85
|
+
# [ options[:expand] ].compact.flatten
|
|
86
|
+
# end
|
|
87
|
+
|
|
88
|
+
# query.expand(*properties_to_expand).find(key)
|
|
89
|
+
# end
|
|
90
|
+
|
|
91
|
+
# Write supplied entity back to the service.
|
|
92
|
+
# TODO Test this more with CRM2011
|
|
93
|
+
# @param entity [Frodo::Entity] entity to save or update in the service
|
|
94
|
+
# @return [Frodo::Entity]
|
|
95
|
+
# def <<(entity)
|
|
96
|
+
# url_chunk, options = setup_entity_post_request(entity)
|
|
97
|
+
|
|
98
|
+
# result = execute_entity_post_request(options, url_chunk)
|
|
99
|
+
# if entity.is_new?
|
|
100
|
+
# doc = ::Nokogiri::XML(result.body).remove_namespaces!
|
|
101
|
+
# primary_key_node = doc.xpath("//content/properties/#{entity.primary_key}").first
|
|
102
|
+
# entity[entity.primary_key] = primary_key_node.content unless primary_key_node.nil?
|
|
103
|
+
# end
|
|
104
|
+
|
|
105
|
+
# unless result.status.to_s =~ /^2[0-9][0-9]$/
|
|
106
|
+
# entity.errors << ['could not commit entity']
|
|
107
|
+
# end
|
|
108
|
+
|
|
109
|
+
# entity
|
|
110
|
+
# end
|
|
111
|
+
|
|
112
|
+
# The Frodo::Service this EntitySet is associated with.
|
|
113
|
+
# @return [Frodo::Service]
|
|
114
|
+
# @api private
|
|
115
|
+
def service
|
|
116
|
+
@service ||= Frodo::ServiceRegistry[service_name]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Options used for instantiating a new Frodo::Entity for this set.
|
|
120
|
+
# @return [Hash]
|
|
121
|
+
# @api private
|
|
122
|
+
def entity_options
|
|
123
|
+
{
|
|
124
|
+
service_name: service_name,
|
|
125
|
+
type: type,
|
|
126
|
+
entity_set: self
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
end
|
|
131
|
+
end
|