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