acts_as_sdata 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.
- data/.gitignore +2 -0
- data/MIT-LICENSE +20 -0
- data/README.textile +200 -0
- data/Rakefile +20 -0
- data/VERSION +1 -0
- data/config/sdata.yml +13 -0
- data/config/sdata.yml.example +20 -0
- data/config/sdata.yml.tmpl.staging +14 -0
- data/generators/acts_as_sdata/acts_as_sdata_generator.rb +9 -0
- data/generators/acts_as_sdata/templates/migration.rb +69 -0
- data/init.rb +36 -0
- data/lib/s_data/active_record_extensions/base.rb +7 -0
- data/lib/s_data/active_record_extensions/mixin.rb +157 -0
- data/lib/s_data/active_record_extensions/sdata_uuid_mixin.rb +133 -0
- data/lib/s_data/atom_extensions/content_mixin.rb +14 -0
- data/lib/s_data/atom_extensions/entry_mixin.rb +41 -0
- data/lib/s_data/atom_extensions/nodes/digest.rb +48 -0
- data/lib/s_data/atom_extensions/nodes/payload.rb +34 -0
- data/lib/s_data/atom_extensions/nodes/sync_state.rb +14 -0
- data/lib/s_data/conditions_builder.rb +59 -0
- data/lib/s_data/controller_mixin.rb +11 -0
- data/lib/s_data/controller_mixin/actions.rb +87 -0
- data/lib/s_data/controller_mixin/collection_scope.rb +57 -0
- data/lib/s_data/controller_mixin/s_data_feed.rb +87 -0
- data/lib/s_data/controller_mixin/s_data_instance.rb +35 -0
- data/lib/s_data/diagnosis/application_controller_mixin.rb +16 -0
- data/lib/s_data/diagnosis/diagnosis.rb +130 -0
- data/lib/s_data/diagnosis/diagnosis_mapper.rb +39 -0
- data/lib/s_data/exceptions.rb +10 -0
- data/lib/s_data/formatting.rb +13 -0
- data/lib/s_data/namespace_definitions.rb +19 -0
- data/lib/s_data/payload.rb +158 -0
- data/lib/s_data/payload_map.rb +0 -0
- data/lib/s_data/payload_map/payload_map.rb +136 -0
- data/lib/s_data/payload_map/payload_map_hash.rb +39 -0
- data/lib/s_data/predicate.rb +31 -0
- data/lib/s_data/route_mapper.rb +143 -0
- data/lib/s_data/router_mixin.rb +10 -0
- data/lib/s_data/sync/controller_mixin.rb +122 -0
- data/lib/s_data/sync/sdata_syncing_mixin.rb +17 -0
- data/lib/s_data/virtual_base.rb +114 -0
- data/test/functional/Rakefile +0 -0
- data/test/unit/active_record_mixin/active_record_mixin_spec.rb +20 -0
- data/test/unit/active_record_mixin/acts_as_sdata_spec.rb +41 -0
- data/test/unit/active_record_mixin/find_by_sdata_instance_id_spec.rb +34 -0
- data/test/unit/active_record_mixin/payload_spec.rb +622 -0
- data/test/unit/active_record_mixin/to_atom_spec.rb +85 -0
- data/test/unit/atom_entry_mixin/atom_entry_mixin_spec.rb +11 -0
- data/test/unit/atom_entry_mixin/to_attributes_spec.rb +30 -0
- data/test/unit/class_stubs/address.rb +19 -0
- data/test/unit/class_stubs/contact.rb +25 -0
- data/test/unit/class_stubs/customer.rb +70 -0
- data/test/unit/class_stubs/model_base.rb +17 -0
- data/test/unit/class_stubs/payload.rb +15 -0
- data/test/unit/class_stubs/sd_uuid.rb +28 -0
- data/test/unit/class_stubs/user.rb +40 -0
- data/test/unit/conditions_builder_spec.rb +54 -0
- data/test/unit/controller_mixin/acts_as_sdata_spec.rb +29 -0
- data/test/unit/controller_mixin/build_sdata_feed_spec.rb +50 -0
- data/test/unit/controller_mixin/controller_mixin_spec.rb +22 -0
- data/test/unit/controller_mixin/diagnosis_spec.rb +232 -0
- data/test/unit/controller_mixin/sdata_collection_spec.rb +78 -0
- data/test/unit/controller_mixin/sdata_create_instance_spec.rb +173 -0
- data/test/unit/controller_mixin/sdata_opensearch_and_links_spec.rb +382 -0
- data/test/unit/controller_mixin/sdata_scope/linked_model_spec.rb +58 -0
- data/test/unit/controller_mixin/sdata_scope/non_linked_model_spec.rb +66 -0
- data/test/unit/controller_mixin/sdata_scope/scoping_in_config_spec.rb +64 -0
- data/test/unit/controller_mixin/sdata_show_instance_spec.rb +98 -0
- data/test/unit/controller_mixin/sdata_update_instance_spec.rb +65 -0
- data/test/unit/payload_map/payload_map_hash_spec.rb +84 -0
- data/test/unit/payload_map/payload_map_spec.rb +144 -0
- data/test/unit/predicate_spec.rb +59 -0
- data/test/unit/router_mixin/routes_spec.rb +138 -0
- data/test/unit/spec.opts +4 -0
- data/test/unit/spec_helper.rb +47 -0
- data/test/unit/spec_helpers/nokogiri_extensions.rb +16 -0
- data/test/unit/sync_controller_mixin/controller_mixin_spec.rb +22 -0
- data/test/unit/sync_controller_mixin/sdata_collection_sync_feed_spec.rb +69 -0
- metadata +175 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
module SData
|
2
|
+
module ControllerMixin
|
3
|
+
module CollectionScope
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def where_clause?
|
8
|
+
!! where_clause
|
9
|
+
end
|
10
|
+
|
11
|
+
def where_clause
|
12
|
+
expression = params.to_a.select{ |pair| pair[1].nil? }.flatten.compact.first
|
13
|
+
expression.nil? ? nil : expression.match(/where\s(.*)/)[0]
|
14
|
+
end
|
15
|
+
|
16
|
+
#(name eq 'asdf') -> options[:conditions] = ['"name" eq ?', 'asdf']
|
17
|
+
def sdata_scope
|
18
|
+
options = {}
|
19
|
+
|
20
|
+
if where_clause?
|
21
|
+
predicate = SData::Predicate.parse(model_class.payload_map.baze_fields, where_clause)
|
22
|
+
options[:conditions] = predicate.to_conditions
|
23
|
+
end
|
24
|
+
|
25
|
+
if sdata_options[:scoping]
|
26
|
+
options[:conditions] ||= []
|
27
|
+
sdata_options[:scoping].each do |scope|
|
28
|
+
options[:conditions][0] = [options[:conditions].to_a[0], scope].compact.join(' and ')
|
29
|
+
1.upto(sdata_options[:scope_param_size] || 1) do
|
30
|
+
options[:conditions] << target_user.id.to_s
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
if params.key? :condition
|
36
|
+
options[:conditions] ||= []
|
37
|
+
if params[:condition] == "$linked"
|
38
|
+
virtual_class = sdata_options[:model].to_s.demodulize
|
39
|
+
baze_class = sdata_options[:model].baze_class.name.demodulize
|
40
|
+
condition = "id IN (SELECT bb_model_id FROM sd_uuids WHERE bb_model_type = '#{baze_class}' and sd_class = '#{virtual_class}')"
|
41
|
+
options[:conditions][0] = [options[:conditions].to_a[0], condition].compact.join(' and ')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
#FIXME: this is an unoptimized solution that may be a bottleneck for large number of matches
|
46
|
+
#if user has hundreds of records but requests first 10, we shouldnt load them all into memory
|
47
|
+
#but use sql query to count how many exist in total, and then load the first 10 only
|
48
|
+
|
49
|
+
#FIXME: do not return records deleted through acts_as_paranoid!
|
50
|
+
results = sdata_options[:model].all(options)
|
51
|
+
@total_results = results.count
|
52
|
+
paginated_results = results[zero_based_start_index, records_to_return]
|
53
|
+
paginated_results.to_a
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module SData
|
2
|
+
module ControllerMixin
|
3
|
+
module SDataFeed
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def resource_url
|
8
|
+
sdata_options[:model].sdata_resource_kind_url(params[:dataset])
|
9
|
+
end
|
10
|
+
|
11
|
+
def build_feed_links_for(feed)
|
12
|
+
feed.links << Atom::Link.new(
|
13
|
+
:rel => 'self',
|
14
|
+
:href => (resource_url + "?#{request.query_parameters.to_param}".chomp('?')),
|
15
|
+
:type => 'application/atom+xml; type=feed',
|
16
|
+
:title => 'Refresh')
|
17
|
+
if (records_to_return > 0) && (@total_results > records_to_return)
|
18
|
+
feed.links << Atom::Link.new(
|
19
|
+
:rel => 'first',
|
20
|
+
:href => (resource_url + "?#{request.query_parameters.merge(:startIndex => '1').to_param}"),
|
21
|
+
:type => 'application/atom+xml; type=feed',
|
22
|
+
:title => 'First Page')
|
23
|
+
feed.links << Atom::Link.new(
|
24
|
+
:rel => 'last',
|
25
|
+
:href => (resource_url + "?#{request.query_parameters.merge(:startIndex => [1,(@last=(((@total_results-zero_based_start_index - 1) / records_to_return * records_to_return) + zero_based_start_index + 1))].max).to_param}"),
|
26
|
+
:type => 'application/atom+xml; type=feed',
|
27
|
+
:title => 'Last Page')
|
28
|
+
if (one_based_start_index+records_to_return) <= @total_results
|
29
|
+
feed.links << Atom::Link.new(
|
30
|
+
:rel => 'next',
|
31
|
+
:href => (resource_url + "?#{request.query_parameters.merge(:startIndex => [1,[@last, (one_based_start_index+records_to_return)].min].max.to_s).to_param}"),
|
32
|
+
:type => 'application/atom+xml; type=feed',
|
33
|
+
:title => 'Next Page')
|
34
|
+
end
|
35
|
+
if (one_based_start_index > 1)
|
36
|
+
feed.links << Atom::Link.new(
|
37
|
+
:rel => 'previous',
|
38
|
+
:href => (resource_url + "?#{request.query_parameters.merge(:startIndex => [1,[@last, (one_based_start_index-records_to_return)].min].max.to_s).to_param}"),
|
39
|
+
:type => 'application/atom+xml; type=feed',
|
40
|
+
:title => 'Previous Page')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def build_sdata_feed(opts={})
|
46
|
+
opts = sdata_options.deep_merge(opts)
|
47
|
+
Atom::Feed.new do |f|
|
48
|
+
f.title = opts[:feed][:title]
|
49
|
+
f.updated = Time.now
|
50
|
+
f.authors << Atom::Person.new(:name => opts[:feed][:author])
|
51
|
+
f.id = resource_url
|
52
|
+
f.categories << Atom::Category.new(:scheme => 'http://schemas.sage.com/sdata/categories',
|
53
|
+
:term => self.category_term,
|
54
|
+
:label => self.category_term.underscore.humanize.titleize)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def records_to_return
|
59
|
+
default_items_per_page = sdata_options[:feed][:default_items_per_page] || 10
|
60
|
+
maximum_items_per_page = sdata_options[:feed][:maximum_items_per_page] || 100
|
61
|
+
#check whether the count param is castable into integer
|
62
|
+
return default_items_per_page if params[:count].blank? or (params[:count].to_i.to_s != params[:count])
|
63
|
+
items_per_page = [params[:count].to_i, maximum_items_per_page].min
|
64
|
+
items_per_page = default_items_per_page if (items_per_page < 0)
|
65
|
+
items_per_page
|
66
|
+
end
|
67
|
+
|
68
|
+
def one_based_start_index
|
69
|
+
[(params[:startIndex].to_i), 1].max
|
70
|
+
end
|
71
|
+
|
72
|
+
def zero_based_start_index
|
73
|
+
[(one_based_start_index - 1), 0].max
|
74
|
+
end
|
75
|
+
|
76
|
+
def populate_open_search_for(feed)
|
77
|
+
feed[SData.config[:schemas]['opensearch'], 'totalResults'] << @total_results
|
78
|
+
feed[SData.config[:schemas]['opensearch'], 'startIndex'] << one_based_start_index
|
79
|
+
feed[SData.config[:schemas]['opensearch'], 'itemsPerPage'] << records_to_return
|
80
|
+
end
|
81
|
+
|
82
|
+
def category_term
|
83
|
+
self.sdata_options[:model].name.demodulize.camelize(:lower).pluralize
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module SData
|
2
|
+
module ControllerMixin
|
3
|
+
module SDataInstance
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def sdata_instance
|
8
|
+
if params[:condition] == "$linked"
|
9
|
+
linked_sdata_instance
|
10
|
+
elsif params.key?(:predicate)
|
11
|
+
sdata_instance_by_predicate
|
12
|
+
else
|
13
|
+
non_linked_sdata_instance
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def sdata_instance_by_predicate
|
18
|
+
predicate = SData::Predicate.parse(model_class.payload_map.baze_fields, params[:predicate])
|
19
|
+
scope = model_class.all(:conditions => predicate.to_conditions)
|
20
|
+
if scope.count != 1
|
21
|
+
raise Sage::BusinessLogic::Exception::IncompatibleDataException, "Conditions scope must contain exactly one entry"
|
22
|
+
end
|
23
|
+
scope.first
|
24
|
+
end
|
25
|
+
|
26
|
+
def linked_sdata_instance
|
27
|
+
SData::SdUuid.find_by_virtual_model_and_uuid(model_class, Predicate.strip_quotes(params[:instance_id]))
|
28
|
+
end
|
29
|
+
|
30
|
+
def non_linked_sdata_instance
|
31
|
+
model_class.find_by_sdata_instance_id(Predicate.strip_quotes(params[:instance_id]))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module SData
|
2
|
+
module ApplicationControllerMixin
|
3
|
+
def sdata_rescue_support
|
4
|
+
self.__send__ :include, SDataRescue
|
5
|
+
end
|
6
|
+
|
7
|
+
module SDataRescue
|
8
|
+
def sdata_global_rescue(exception, request_path)
|
9
|
+
RAILS_DEFAULT_LOGGER.debug("sdata_global_rescue. exception: #{exception.inspect} request_path: #{request_path.inspect}")
|
10
|
+
error_payload = SData::Diagnosis::DiagnosisMapper.map(exception, request_path)
|
11
|
+
render :xml => error_payload.to_xml(:root), :status => (error_payload.send('http_status_code') || '500')
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
ActionController::Base.extend SData::ApplicationControllerMixin
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module SData
|
2
|
+
class Diagnosis
|
3
|
+
|
4
|
+
@@sdata_attributes = [:severity, :sdata_code, :application_code, :message, :stack_trace, :payload_path, :exception, :http_status_code]
|
5
|
+
@@sdata_attributes.each {|attr| attr_accessor attr}
|
6
|
+
|
7
|
+
def initialize(params={})
|
8
|
+
raise "SData::Diagnosis is an abstract class; instantiate a subclass instead" if self.class == SData::Diagnosis
|
9
|
+
params.each_pair do |key,value|
|
10
|
+
if @@sdata_attributes.include?(key)
|
11
|
+
self.send("#{key}=", value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
if self.exception && !self.message
|
15
|
+
self.message = self.exception.message
|
16
|
+
end
|
17
|
+
self.sdata_code ||= self.class.name.demodulize
|
18
|
+
self.severity ||= "error"
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_xml(mode=:root)
|
22
|
+
case mode
|
23
|
+
when :root
|
24
|
+
return Diagnosis.construct_header_for(self.diagnosis_payload)
|
25
|
+
when :feed
|
26
|
+
return self.diagnosis_payload[0]
|
27
|
+
when :entry
|
28
|
+
return self.diagnosis_payload
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
#Can be called from outside to build a single header for multiple diagnoses, each of which has been
|
33
|
+
#constructed with (header=false) option. Useful when generating multiple diagnoses inside a single Feed.
|
34
|
+
#Currently won't work inside a signle Entry due to parsing problems in rAtom.
|
35
|
+
#Solving this problem is complex, so won't try to implement unless we confirm it's required. -Eugene
|
36
|
+
def self.construct_header_for(diagnosis_payloads)
|
37
|
+
document = XML::Document.new
|
38
|
+
root_node = XML::Node.new("sdata:diagnoses")
|
39
|
+
#TODO FIXME: SData spec says root node must be just 'xmlns=' and not 'xmlns:sdata', but this fails W3
|
40
|
+
#XML validation. Confirm which way is correct -- if former, change below line to root_node['xmlns']...
|
41
|
+
root_node['xmlns:sdata'] = "#{SData.config[:schemas]['sdata']}"
|
42
|
+
diagnosis_payloads.each do |diagnosis_payload|
|
43
|
+
root_node << diagnosis_payload
|
44
|
+
end
|
45
|
+
document.root = root_node
|
46
|
+
document
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def diagnosis_payload
|
52
|
+
node = XML::Node.new("sdata:diagnosis")
|
53
|
+
@@sdata_attributes.each do |attribute|
|
54
|
+
value = self.send(attribute) unless [:http_status_code].include?(attribute)
|
55
|
+
if value
|
56
|
+
if value.is_a?(Exception)
|
57
|
+
node << (XML::Node.new("sdata:stackTrace") << value.backtrace.join("\n") ) if SData.config[:show_stack_trace]
|
58
|
+
else
|
59
|
+
node << (XML::Node.new("sdata:#{attribute.to_s.camelize(:lower)}") << value)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
#nesting node in [] because of strange quirk in Atom::Content's mixin in AtomExtensions::ContentMixin,
|
64
|
+
#in which .self becomes the return value of this method, but doesn't escape properly in the Atom Entry
|
65
|
+
#making .self be [node] and then calling self[0] works fine.
|
66
|
+
[node]
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
#potential TODO: write a static class-generating method which diagnoses exception and decides what kind of
|
72
|
+
#error payload class to return (caller will then instantiate it)
|
73
|
+
|
74
|
+
#customize as needed
|
75
|
+
class BadUrlSyntax < Diagnosis
|
76
|
+
def initialize(params={})
|
77
|
+
self.http_status_code = '400'
|
78
|
+
super(params)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
class BadQueryParameter < Diagnosis
|
82
|
+
def initialize(params={})
|
83
|
+
self.http_status_code = '400'
|
84
|
+
super(params)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
class ApplicationNotFound < Diagnosis
|
88
|
+
def initialize(params={})
|
89
|
+
self.http_status_code = '404'
|
90
|
+
super(params)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
class ApplicationUnavailable < Diagnosis
|
94
|
+
def initialize(params={})
|
95
|
+
self.http_status_code = '503'
|
96
|
+
super(params)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
class DatasetNotFound < Diagnosis
|
100
|
+
def initialize(params={})
|
101
|
+
self.http_status_code = '404'
|
102
|
+
super(params)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
class ContractNotFound < Diagnosis
|
106
|
+
def initialize(params={})
|
107
|
+
self.http_status_code = '404'
|
108
|
+
super(params)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
class ResourceKindNotFound < Diagnosis
|
112
|
+
def initialize(params={})
|
113
|
+
self.http_status_code = '404'
|
114
|
+
super(params)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
class BadWhereSyntax < Diagnosis
|
118
|
+
def initialize(params={})
|
119
|
+
self.http_status_code = '400'
|
120
|
+
super(params)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
class ApplicationDiagnosis < Diagnosis
|
124
|
+
def initialize(params={})
|
125
|
+
self.http_status_code = '500'
|
126
|
+
super(params)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module SData
|
2
|
+
class Diagnosis
|
3
|
+
class DiagnosisMapper
|
4
|
+
def self.map(exception, request_path='')
|
5
|
+
error_payload = case exception.class.name.demodulize
|
6
|
+
when 'NoMethodError'
|
7
|
+
if request_path.match /^\/#{SData.store_path}\/[^\/]+\/[A-z]+\/.*/
|
8
|
+
SData::ApplicationDiagnosis.new(:exception => exception, :http_status_code => '500')
|
9
|
+
elsif request_path.match /^\/#{SData.store_path}\/[^\/]+\/[A-z]+/
|
10
|
+
SData::ResourceKindNotFound.new(:exception => exception, :http_status_code => '404')
|
11
|
+
elsif request_path.match /^\/#{SData.store_path}\/[^\/]+$/
|
12
|
+
SData::ApplicationDiagnosis.new(:exception => exception, :http_status_code => '501')
|
13
|
+
elsif request_path.match /^\/#{SData.store_path}.+/
|
14
|
+
SData::DatasetNotFound.new(:exception => exception, :http_status_code => '404')
|
15
|
+
elsif request_path.match /^\/#{SData.store_path}$/
|
16
|
+
SData::ApplicationDiagnosis.new(:exception => exception, :http_status_code => '501')
|
17
|
+
elsif request_path.match /^\/sdata\/#{SData.config[:application]}.+/
|
18
|
+
SData::ContractNotFound.new(:exception => exception, :http_status_code => '404')
|
19
|
+
elsif request_path.match /^\/sdata\/#{SData.config[:application]}$/
|
20
|
+
SData::ApplicationDiagnosis.new(:exception => exception, :http_status_code => '501')
|
21
|
+
else
|
22
|
+
SData::ApplicationNotFound.new(:exception => exception, :http_status_code => '404')
|
23
|
+
end
|
24
|
+
when 'AccessDeniedException'
|
25
|
+
SData::ApplicationDiagnosis.new(:exception => exception, :http_status_code => '403')
|
26
|
+
when 'ExpiredSubscriptionException'
|
27
|
+
SData::ApplicationDiagnosis.new(:exception => exception, :http_status_code => '402')
|
28
|
+
when 'UnauthenticatedException'
|
29
|
+
SData::ApplicationDiagnosis.new(:exception => exception, :http_status_code => '401')
|
30
|
+
when 'IncompatibleDataException'
|
31
|
+
SData::BadWhereSyntax.new(:exception => exception, :http_status_code => '409')
|
32
|
+
else
|
33
|
+
SData::ApplicationDiagnosis.new(:exception => exception, :http_status_code => '500')
|
34
|
+
end
|
35
|
+
error_payload
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module SData
|
2
|
+
class Formatting
|
3
|
+
def self.format_date_time(date_time)
|
4
|
+
return date_time unless date_time
|
5
|
+
case date_time.class.name
|
6
|
+
when 'ActiveSupport::TimeWithZone', 'Time'
|
7
|
+
date_time.strftime("%Y-%m-%dT%H:%M:%S%z").insert(-3,':')
|
8
|
+
when 'Date'
|
9
|
+
date_time.strftime("%Y-%m-%d")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# The following pre-sets the namespace prefix for simple extensions. It is done this way because
|
2
|
+
# the namespace map is lazily created in the Atom::Feed.to_xml method. Could also alias to_xml,
|
3
|
+
# and add all the namespaces in extensions_namespaces to this map
|
4
|
+
module SData
|
5
|
+
module NamespaceMapMixin
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
alias_method :initialize, :initialize_with_map
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize_with_map(default=Atom::NAMESPACE)
|
13
|
+
@default = default
|
14
|
+
@i = 0
|
15
|
+
@map = SData.config[:schemas].invert
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
Atom::Xml::NamespaceMap.__send__ :include, SData::NamespaceMapMixin
|
@@ -0,0 +1,158 @@
|
|
1
|
+
module SData
|
2
|
+
class Payload
|
3
|
+
|
4
|
+
attr_accessor :builder, :root_node_name, :included, :selected, :maximum_precedence, :sync, :contract
|
5
|
+
attr_accessor :xml_node, :entity, :expand, :dataset
|
6
|
+
|
7
|
+
def initialize(params)
|
8
|
+
self.builder = Builder::XmlMarkup.new
|
9
|
+
self.included = params[:included]
|
10
|
+
self.selected = params[:selected]
|
11
|
+
self.maximum_precedence = params[:maximum_precedence]
|
12
|
+
self.sync = params[:sync]
|
13
|
+
self.contract = params[:contract]
|
14
|
+
self.expand = params[:expand]
|
15
|
+
self.entity = params[:entity]
|
16
|
+
self.dataset = params[:dataset]
|
17
|
+
end
|
18
|
+
|
19
|
+
def ==(other)
|
20
|
+
return false unless other.is_a?(SData::Payload)
|
21
|
+
[:root_node_name, :included, :selected, :maximum_precedence, :sync, :contract, :entity, :expand].all?{|attr| self.send(attr) == other.send(attr)}
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.is_sync?
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
28
|
+
def is_sync?
|
29
|
+
!sync.nil? ? sync : Payload.is_sync?
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.parse(xml)
|
33
|
+
returning new do |payload|
|
34
|
+
payload.xml_node = xml
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_xml(*params)
|
39
|
+
node = XML::Node.new("sdata:payload")
|
40
|
+
generate! if @xml_node.nil?
|
41
|
+
node << @xml_node
|
42
|
+
return node
|
43
|
+
end
|
44
|
+
|
45
|
+
def generate!
|
46
|
+
@xml_node = generate(entity.sdata_node_name, entity, expand, 1, nil)
|
47
|
+
end
|
48
|
+
|
49
|
+
def generate(node_name, node_value, expand, element_precedence, resource_collection)
|
50
|
+
self.root_node_name ||= node_name
|
51
|
+
return "" if element_precedence > maximum_precedence
|
52
|
+
return "" if excluded_in_select?(node_name)
|
53
|
+
if node_value.respond_to?(:sdata_options)
|
54
|
+
construct_from_sdata_model(node_name, node_value, expand, element_precedence, resource_collection)
|
55
|
+
elsif node_value.is_a?(Array)
|
56
|
+
construct_from_array(node_name, node_value, expand, element_precedence, resource_collection)
|
57
|
+
elsif node_value.is_a?(Hash)
|
58
|
+
construct_from_hash(node_name, node_value, expand, element_precedence, resource_collection)
|
59
|
+
else
|
60
|
+
construct_from_string(node_name, node_value, expand, element_precedence, resource_collection)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def construct_from_sdata_model(node_name, node_value, expand, element_precedence, resource_collection)
|
65
|
+
node = XML::Node.new(qualified(node_name))
|
66
|
+
attributes = node_value.resource_header_attributes(dataset, included)
|
67
|
+
attributes.each_pair do |key,value|
|
68
|
+
node[key] = value.to_s
|
69
|
+
end
|
70
|
+
if (node_name == self.root_node_name) || (expand != :none) || included.include?(formatted(node_name))
|
71
|
+
expand = :none if (expand == :immediate_children)
|
72
|
+
node_value.payload_map.each_pair do |child_node_name, child_node_data|
|
73
|
+
if child_node_data[:type] == :association
|
74
|
+
child_expand = :none
|
75
|
+
else
|
76
|
+
child_expand = (is_sync? ? :all_children : expand)
|
77
|
+
end
|
78
|
+
collection = ({:parent => node_value, :url => formatted(child_node_name), :type => node_value.payload_map[child_node_name][:type]})
|
79
|
+
attribute_method_name = sdata_attribute_method(child_node_data)
|
80
|
+
node << generate(formatted(child_node_name), node_value.send(attribute_method_name), child_expand, child_node_data[:precedence], collection)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
node
|
84
|
+
end
|
85
|
+
|
86
|
+
# this doesn't belong here. sdata attribute definitions should be real objects not hashes, so they can figure this stuff out themselves
|
87
|
+
def sdata_attribute_method(attribute_definition)
|
88
|
+
is_sync? ? attribute_definition[:method_name_with_deleted] : attribute_definition[:method_name]
|
89
|
+
end
|
90
|
+
|
91
|
+
def construct_from_array(node_name, node_value, expand, element_precedence, resource_collection)
|
92
|
+
if resource_collection && resource_collection[:type]
|
93
|
+
construct_from_sdata_array(node_name, node_value, expand, element_precedence, resource_collection)
|
94
|
+
else
|
95
|
+
construct_from_non_sdata_array(node_name, node_value, expand, element_precedence, resource_collection)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def construct_from_sdata_array(node_name, node_value, expand, element_precedence, resource_collection)
|
100
|
+
expand = :none if (expand == :immediate_children)
|
101
|
+
node = XML::Node.new(qualified(node_name))
|
102
|
+
scoped_children_collection = sdata_collection_url(resource_collection[:parent], resource_collection[:url])
|
103
|
+
node['sdata:url'] = scoped_children_collection
|
104
|
+
node_value.each do |item|
|
105
|
+
node << generate(item.sdata_node_name, item, expand, element_precedence, nil)
|
106
|
+
end
|
107
|
+
node
|
108
|
+
end
|
109
|
+
|
110
|
+
def construct_from_non_sdata_array(node_name, node_value, expand, element_precedence, resource_collection)
|
111
|
+
expand = :none if (expand == :immediate_children)
|
112
|
+
node = XML::Node.new(qualified(node_name))
|
113
|
+
node_value.each do |item|
|
114
|
+
node << generate(formatted(node_name).singularize, item, expand, element_precedence, (item.is_a?(Hash) ? item[:resource_collection] : nil))
|
115
|
+
end
|
116
|
+
node
|
117
|
+
end
|
118
|
+
|
119
|
+
def construct_from_hash(node_name, node_value, expand, element_precedence, resource_collection)
|
120
|
+
expand = :none if (expand == :immediate_children)
|
121
|
+
node = XML::Node.new(qualified(node_name))
|
122
|
+
node_value.each_pair do |child_node_name, child_node_data|
|
123
|
+
node << generate(formatted(child_node_name), child_node_data, expand, element_precedence, (child_node_data.is_a?(Hash) ? child_node_data[:resource_collection] : nil))
|
124
|
+
end
|
125
|
+
node
|
126
|
+
end
|
127
|
+
|
128
|
+
def construct_from_string(node_name, node_value, expand, element_precedence, resource_collection)
|
129
|
+
node = XML::Node.new(qualified(node_name))
|
130
|
+
if !node_value.to_s.blank?
|
131
|
+
node << node_value
|
132
|
+
else
|
133
|
+
node['xsi:nil'] = 'true'
|
134
|
+
end
|
135
|
+
node
|
136
|
+
end
|
137
|
+
|
138
|
+
def qualified(node_name)
|
139
|
+
"#{contract}:#{(formatted(node_name))}"
|
140
|
+
end
|
141
|
+
|
142
|
+
def formatted(node_name)
|
143
|
+
node_name.to_s.camelize(:lower)
|
144
|
+
end
|
145
|
+
|
146
|
+
def sdata_collection_url(parent, child)
|
147
|
+
#FIXME: adjust for bookkeeper support
|
148
|
+
SData.endpoint + "/#{dataset}/" + parent.sdata_node_name + "('#{parent.id}')/" + child
|
149
|
+
end
|
150
|
+
|
151
|
+
def excluded_in_select?(node_name)
|
152
|
+
return false if selected.empty?
|
153
|
+
return false if node_name == self.root_node_name
|
154
|
+
return !selected.include?(node_name.to_s.camelize(:lower))
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
end
|