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.
Files changed (79) hide show
  1. data/.gitignore +2 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.textile +200 -0
  4. data/Rakefile +20 -0
  5. data/VERSION +1 -0
  6. data/config/sdata.yml +13 -0
  7. data/config/sdata.yml.example +20 -0
  8. data/config/sdata.yml.tmpl.staging +14 -0
  9. data/generators/acts_as_sdata/acts_as_sdata_generator.rb +9 -0
  10. data/generators/acts_as_sdata/templates/migration.rb +69 -0
  11. data/init.rb +36 -0
  12. data/lib/s_data/active_record_extensions/base.rb +7 -0
  13. data/lib/s_data/active_record_extensions/mixin.rb +157 -0
  14. data/lib/s_data/active_record_extensions/sdata_uuid_mixin.rb +133 -0
  15. data/lib/s_data/atom_extensions/content_mixin.rb +14 -0
  16. data/lib/s_data/atom_extensions/entry_mixin.rb +41 -0
  17. data/lib/s_data/atom_extensions/nodes/digest.rb +48 -0
  18. data/lib/s_data/atom_extensions/nodes/payload.rb +34 -0
  19. data/lib/s_data/atom_extensions/nodes/sync_state.rb +14 -0
  20. data/lib/s_data/conditions_builder.rb +59 -0
  21. data/lib/s_data/controller_mixin.rb +11 -0
  22. data/lib/s_data/controller_mixin/actions.rb +87 -0
  23. data/lib/s_data/controller_mixin/collection_scope.rb +57 -0
  24. data/lib/s_data/controller_mixin/s_data_feed.rb +87 -0
  25. data/lib/s_data/controller_mixin/s_data_instance.rb +35 -0
  26. data/lib/s_data/diagnosis/application_controller_mixin.rb +16 -0
  27. data/lib/s_data/diagnosis/diagnosis.rb +130 -0
  28. data/lib/s_data/diagnosis/diagnosis_mapper.rb +39 -0
  29. data/lib/s_data/exceptions.rb +10 -0
  30. data/lib/s_data/formatting.rb +13 -0
  31. data/lib/s_data/namespace_definitions.rb +19 -0
  32. data/lib/s_data/payload.rb +158 -0
  33. data/lib/s_data/payload_map.rb +0 -0
  34. data/lib/s_data/payload_map/payload_map.rb +136 -0
  35. data/lib/s_data/payload_map/payload_map_hash.rb +39 -0
  36. data/lib/s_data/predicate.rb +31 -0
  37. data/lib/s_data/route_mapper.rb +143 -0
  38. data/lib/s_data/router_mixin.rb +10 -0
  39. data/lib/s_data/sync/controller_mixin.rb +122 -0
  40. data/lib/s_data/sync/sdata_syncing_mixin.rb +17 -0
  41. data/lib/s_data/virtual_base.rb +114 -0
  42. data/test/functional/Rakefile +0 -0
  43. data/test/unit/active_record_mixin/active_record_mixin_spec.rb +20 -0
  44. data/test/unit/active_record_mixin/acts_as_sdata_spec.rb +41 -0
  45. data/test/unit/active_record_mixin/find_by_sdata_instance_id_spec.rb +34 -0
  46. data/test/unit/active_record_mixin/payload_spec.rb +622 -0
  47. data/test/unit/active_record_mixin/to_atom_spec.rb +85 -0
  48. data/test/unit/atom_entry_mixin/atom_entry_mixin_spec.rb +11 -0
  49. data/test/unit/atom_entry_mixin/to_attributes_spec.rb +30 -0
  50. data/test/unit/class_stubs/address.rb +19 -0
  51. data/test/unit/class_stubs/contact.rb +25 -0
  52. data/test/unit/class_stubs/customer.rb +70 -0
  53. data/test/unit/class_stubs/model_base.rb +17 -0
  54. data/test/unit/class_stubs/payload.rb +15 -0
  55. data/test/unit/class_stubs/sd_uuid.rb +28 -0
  56. data/test/unit/class_stubs/user.rb +40 -0
  57. data/test/unit/conditions_builder_spec.rb +54 -0
  58. data/test/unit/controller_mixin/acts_as_sdata_spec.rb +29 -0
  59. data/test/unit/controller_mixin/build_sdata_feed_spec.rb +50 -0
  60. data/test/unit/controller_mixin/controller_mixin_spec.rb +22 -0
  61. data/test/unit/controller_mixin/diagnosis_spec.rb +232 -0
  62. data/test/unit/controller_mixin/sdata_collection_spec.rb +78 -0
  63. data/test/unit/controller_mixin/sdata_create_instance_spec.rb +173 -0
  64. data/test/unit/controller_mixin/sdata_opensearch_and_links_spec.rb +382 -0
  65. data/test/unit/controller_mixin/sdata_scope/linked_model_spec.rb +58 -0
  66. data/test/unit/controller_mixin/sdata_scope/non_linked_model_spec.rb +66 -0
  67. data/test/unit/controller_mixin/sdata_scope/scoping_in_config_spec.rb +64 -0
  68. data/test/unit/controller_mixin/sdata_show_instance_spec.rb +98 -0
  69. data/test/unit/controller_mixin/sdata_update_instance_spec.rb +65 -0
  70. data/test/unit/payload_map/payload_map_hash_spec.rb +84 -0
  71. data/test/unit/payload_map/payload_map_spec.rb +144 -0
  72. data/test/unit/predicate_spec.rb +59 -0
  73. data/test/unit/router_mixin/routes_spec.rb +138 -0
  74. data/test/unit/spec.opts +4 -0
  75. data/test/unit/spec_helper.rb +47 -0
  76. data/test/unit/spec_helpers/nokogiri_extensions.rb +16 -0
  77. data/test/unit/sync_controller_mixin/controller_mixin_spec.rb +22 -0
  78. data/test/unit/sync_controller_mixin/sdata_collection_sync_feed_spec.rb +69 -0
  79. 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,10 @@
1
+ module SData
2
+ module Exceptions
3
+ module VirtualBase
4
+ class InvalidSDataAttribute < ArgumentError
5
+ end
6
+ class InvalidSDataAssociation < InvalidSDataAttribute
7
+ end
8
+ end
9
+ end
10
+ 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