acts_as_sdata 1.0.0

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