odata_server 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +12 -0
- data/MIT-LICENSE +20 -0
- data/README +14 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/app/controllers/o_data_controller.rb +145 -0
- data/app/helpers/o_data_helper.rb +203 -0
- data/app/views/o_data/metadata.xml.builder +62 -0
- data/app/views/o_data/resource.atom.builder +8 -0
- data/app/views/o_data/resource.json.erb +1 -0
- data/app/views/o_data/service.xml.builder +11 -0
- data/config/routes.rb +11 -0
- data/init.rb +3 -0
- data/install.rb +1 -0
- data/lib/o_data/abstract_query.rb +27 -0
- data/lib/o_data/abstract_query/base.rb +133 -0
- data/lib/o_data/abstract_query/countable.rb +22 -0
- data/lib/o_data/abstract_query/errors.rb +117 -0
- data/lib/o_data/abstract_query/option.rb +58 -0
- data/lib/o_data/abstract_query/options/enumerated_option.rb +39 -0
- data/lib/o_data/abstract_query/options/expand_option.rb +81 -0
- data/lib/o_data/abstract_query/options/format_option.rb +19 -0
- data/lib/o_data/abstract_query/options/inlinecount_option.rb +20 -0
- data/lib/o_data/abstract_query/options/orderby_option.rb +79 -0
- data/lib/o_data/abstract_query/options/select_option.rb +74 -0
- data/lib/o_data/abstract_query/options/skip_option.rb +32 -0
- data/lib/o_data/abstract_query/options/top_option.rb +32 -0
- data/lib/o_data/abstract_query/parser.rb +77 -0
- data/lib/o_data/abstract_query/segment.rb +43 -0
- data/lib/o_data/abstract_query/segments/collection_segment.rb +24 -0
- data/lib/o_data/abstract_query/segments/count_segment.rb +38 -0
- data/lib/o_data/abstract_query/segments/entity_type_and_key_values_segment.rb +116 -0
- data/lib/o_data/abstract_query/segments/entity_type_segment.rb +31 -0
- data/lib/o_data/abstract_query/segments/links_segment.rb +49 -0
- data/lib/o_data/abstract_query/segments/navigation_property_segment.rb +82 -0
- data/lib/o_data/abstract_query/segments/property_segment.rb +50 -0
- data/lib/o_data/abstract_query/segments/value_segment.rb +40 -0
- data/lib/o_data/abstract_schema.rb +9 -0
- data/lib/o_data/abstract_schema/association.rb +29 -0
- data/lib/o_data/abstract_schema/base.rb +48 -0
- data/lib/o_data/abstract_schema/comparable.rb +42 -0
- data/lib/o_data/abstract_schema/end.rb +50 -0
- data/lib/o_data/abstract_schema/entity_type.rb +64 -0
- data/lib/o_data/abstract_schema/navigation_property.rb +37 -0
- data/lib/o_data/abstract_schema/property.rb +35 -0
- data/lib/o_data/abstract_schema/schema_object.rb +37 -0
- data/lib/o_data/abstract_schema/serializable.rb +79 -0
- data/lib/o_data/active_record_schema.rb +8 -0
- data/lib/o_data/active_record_schema/association.rb +130 -0
- data/lib/o_data/active_record_schema/base.rb +37 -0
- data/lib/o_data/active_record_schema/entity_type.rb +128 -0
- data/lib/o_data/active_record_schema/navigation_property.rb +45 -0
- data/lib/o_data/active_record_schema/property.rb +45 -0
- data/lib/o_data/active_record_schema/serializable.rb +36 -0
- data/lib/odata_server.rb +12 -0
- data/public/clientaccesspolicy.xml +13 -0
- data/tasks/o_data_server_tasks.rake +4 -0
- data/test/helper.rb +17 -0
- data/uninstall.rb +1 -0
- metadata +171 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
module OData
|
2
|
+
module AbstractQuery
|
3
|
+
module Options
|
4
|
+
class EnumeratedOption < OData::AbstractQuery::Option
|
5
|
+
def self.valid_values
|
6
|
+
%w{}
|
7
|
+
end
|
8
|
+
|
9
|
+
def valid_values
|
10
|
+
self.class.valid_values
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(query, key, value = nil)
|
14
|
+
super(query, key, value)
|
15
|
+
end
|
16
|
+
|
17
|
+
# def self.applies_to?(query)
|
18
|
+
# false
|
19
|
+
# end
|
20
|
+
|
21
|
+
def self.parse!(query, key, value = nil)
|
22
|
+
return nil unless key == self.option_name
|
23
|
+
return nil if valid_values.empty?
|
24
|
+
|
25
|
+
if value.blank?
|
26
|
+
query.Option(self, key, valid_values.first)
|
27
|
+
else
|
28
|
+
query.Option(self, key, value)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def valid?
|
33
|
+
return false if self.value.blank? || self.valid_values.empty?
|
34
|
+
self.valid_values.collect(&:to_s).include?(self.value.to_s)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module OData
|
2
|
+
module AbstractQuery
|
3
|
+
module Options
|
4
|
+
class ExpandOption < OData::AbstractQuery::Option
|
5
|
+
def self.option_name
|
6
|
+
'$expand'
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :navigation_property_paths, :navigation_property_paths_str
|
10
|
+
|
11
|
+
# TODO: remove navigation_property_paths_str
|
12
|
+
def initialize(query, navigation_property_paths = {}, navigation_property_paths_str = nil)
|
13
|
+
@navigation_property_paths = navigation_property_paths
|
14
|
+
@navigation_property_paths_str = navigation_property_paths_str
|
15
|
+
|
16
|
+
super(query, self.class.option_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.applies_to?(query)
|
20
|
+
return false if query.segments.empty?
|
21
|
+
(query.segments.last.is_a?(OData::AbstractQuery::Segments::CollectionSegment) || query.segments.last.is_a?(OData::AbstractQuery::Segments::NavigationPropertySegment))
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.parse!(query, key, value = nil)
|
25
|
+
return nil unless key == self.option_name
|
26
|
+
|
27
|
+
if query.segments.last.respond_to?(:navigation_property)
|
28
|
+
navigation_property = query.segments.last.navigation_property
|
29
|
+
|
30
|
+
raise OData::AbstractQuery::Errors::InvalidOptionValue.new(query, self.option_name) if navigation_property.to_end.polymorphic?
|
31
|
+
end
|
32
|
+
|
33
|
+
if query.segments.last.respond_to?(:entity_type)
|
34
|
+
entity_type = query.segments.last.entity_type
|
35
|
+
|
36
|
+
navigation_property_paths = value.to_s.split(/\s*,\s*/).inject({}) { |acc, path|
|
37
|
+
segments = path.split('/')
|
38
|
+
reflect_on_navigation_property_path(query, acc, entity_type, segments.shift, segments)
|
39
|
+
acc
|
40
|
+
}
|
41
|
+
|
42
|
+
query.Option(self, navigation_property_paths, value.to_s)
|
43
|
+
else
|
44
|
+
raise OData::AbstractQuery::Errors::InvalidOptionContext.new(query, self.option_name) unless value.blank?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def valid?
|
49
|
+
# TODO: replace with validation
|
50
|
+
true
|
51
|
+
end
|
52
|
+
|
53
|
+
def value
|
54
|
+
"'" + @navigation_property_paths_str.gsub(/\s+/, '') + "'"
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def self.reflect_on_navigation_property_path(query, acc, entity_type, head, rest)
|
60
|
+
if head.blank?
|
61
|
+
acc
|
62
|
+
elsif entity_type.blank?
|
63
|
+
raise OData::AbstractQuery::Errors::NavigationPropertyNotFound.new(nil, head)
|
64
|
+
elsif navigation_property = entity_type.navigation_properties.find { |np| np.name == head }
|
65
|
+
acc[navigation_property] ||= {}
|
66
|
+
|
67
|
+
if navigation_property.to_end.polymorphic?
|
68
|
+
raise OData::AbstractQuery::Errors::InvalidOptionValue.new(query, head) unless rest.empty?
|
69
|
+
else
|
70
|
+
reflect_on_navigation_property_path(query, acc[navigation_property], navigation_property.to_end.entity_type, rest.shift, rest)
|
71
|
+
end
|
72
|
+
|
73
|
+
acc
|
74
|
+
else
|
75
|
+
raise OData::AbstractQuery::Errors::NavigationPropertyNotFound.new(nil, head)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module OData
|
2
|
+
module AbstractQuery
|
3
|
+
module Options
|
4
|
+
class FormatOption < EnumeratedOption
|
5
|
+
def self.option_name
|
6
|
+
'$format'
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.valid_values
|
10
|
+
%w{atom json}
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.applies_to?(query)
|
14
|
+
true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module OData
|
2
|
+
module AbstractQuery
|
3
|
+
module Options
|
4
|
+
class InlinecountOption < EnumeratedOption
|
5
|
+
def self.option_name
|
6
|
+
'$inlinecount'
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.valid_values
|
10
|
+
%w{none allpages}
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.applies_to?(query)
|
14
|
+
return false if query.segments.empty?
|
15
|
+
query.segments.last.is_a?(OData::AbstractQuery::Segments::CollectionSegment) || query.segments.last.is_a?(OData::AbstractQuery::Segments::NavigationPropertySegment)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module OData
|
2
|
+
module AbstractQuery
|
3
|
+
module Options
|
4
|
+
class OrderbyOption < OData::AbstractQuery::Option
|
5
|
+
def self.option_name
|
6
|
+
'$orderby'
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :pairs
|
10
|
+
|
11
|
+
def initialize(query, pairs = [])
|
12
|
+
@pairs = pairs
|
13
|
+
|
14
|
+
super(query, self.class.option_name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.applies_to?(query)
|
18
|
+
return false if query.segments.empty?
|
19
|
+
(query.segments.last.is_a?(OData::AbstractQuery::Segments::CollectionSegment) || query.segments.last.is_a?(OData::AbstractQuery::Segments::NavigationPropertySegment))
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.parse!(query, key, value = nil)
|
23
|
+
return nil unless key == self.option_name
|
24
|
+
|
25
|
+
if query.segments.last.respond_to?(:navigation_property)
|
26
|
+
navigation_property = query.segments.last.navigation_property
|
27
|
+
|
28
|
+
raise OData::AbstractQuery::Errors::InvalidOptionValue.new(query, self.option_name) if navigation_property.to_end.polymorphic?
|
29
|
+
end
|
30
|
+
|
31
|
+
raise OData::AbstractQuery::Errors::InvalidOptionContext.new(query, self) unless query.segments.last.respond_to?(:countable?) && query.segments.last.countable?
|
32
|
+
|
33
|
+
if query.segments.last.respond_to?(:entity_type)
|
34
|
+
entity_type = query.segments.last.entity_type
|
35
|
+
|
36
|
+
pairs = value.to_s.split(/\s*,\s*/).collect { |path|
|
37
|
+
if md = path.match(/^([A-Za-z_]+(?:\/[A-Za-z_]+)*)(?:\s+(asc|desc))?$/)
|
38
|
+
property_name = md[1]
|
39
|
+
order = md[2].blank? ? :asc : md[2].to_sym
|
40
|
+
|
41
|
+
property = entity_type.properties.find { |p| p.name == property_name }
|
42
|
+
raise OData::AbstractQuery::Errors::PropertyNotFound.new(query, property_name) if property.blank?
|
43
|
+
|
44
|
+
[property, order]
|
45
|
+
else
|
46
|
+
raise OData::AbstractQuery::Errors::PropertyNotFound.new(query, path)
|
47
|
+
end
|
48
|
+
}
|
49
|
+
|
50
|
+
query.Option(self, pairs.empty? ? [[entity_type.key_property, :asc]] : pairs)
|
51
|
+
else
|
52
|
+
raise OData::AbstractQuery::Errors::InvalidOptionContext.new(query, self.option_name) unless value.blank?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def entity_type
|
57
|
+
return nil if self.query.segments.empty?
|
58
|
+
return nil unless self.query.segments.last.respond_to?(:entity_type)
|
59
|
+
@entity_type ||= self.query.segments.last.entity_type
|
60
|
+
end
|
61
|
+
|
62
|
+
def valid?
|
63
|
+
entity_type = self.entity_type
|
64
|
+
return false if entity_type.blank?
|
65
|
+
|
66
|
+
@pairs.is_a?(Array) && @pairs.all? { |pair|
|
67
|
+
property, value = pair
|
68
|
+
|
69
|
+
property.is_a?(OData::AbstractSchema::Property) && !!entity_type.properties.find { |p| p.name == property.name }
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
def value
|
74
|
+
"'" + @pairs.collect { |p| "#{p.first.name} #{p.last}" }.join(',') + "'"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module OData
|
2
|
+
module AbstractQuery
|
3
|
+
module Options
|
4
|
+
class SelectOption < OData::AbstractQuery::Option
|
5
|
+
def self.option_name
|
6
|
+
'$select'
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :properties
|
10
|
+
|
11
|
+
def initialize(query, properties = [])
|
12
|
+
@properties = properties
|
13
|
+
|
14
|
+
super(query, self.class.option_name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.applies_to?(query)
|
18
|
+
return false if query.segments.empty?
|
19
|
+
(query.segments.last.is_a?(OData::AbstractQuery::Segments::CollectionSegment) || query.segments.last.is_a?(OData::AbstractQuery::Segments::NavigationPropertySegment))
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.parse!(query, key, value = nil)
|
23
|
+
return nil unless key == self.option_name
|
24
|
+
|
25
|
+
if query.segments.last.respond_to?(:navigation_property)
|
26
|
+
navigation_property = query.segments.last.navigation_property
|
27
|
+
|
28
|
+
raise OData::AbstractQuery::Errors::InvalidOptionValue.new(query, self.option_name) if navigation_property.to_end.polymorphic?
|
29
|
+
end
|
30
|
+
|
31
|
+
if query.segments.last.respond_to?(:entity_type)
|
32
|
+
entity_type = query.segments.last.entity_type
|
33
|
+
|
34
|
+
properties = value.to_s.strip == "*" ? entity_type.properties : value.to_s.split(/\s*,\s*/).collect { |path|
|
35
|
+
if md = path.match(/^([A-Za-z_]+)$/)
|
36
|
+
property_name = md[1]
|
37
|
+
|
38
|
+
property = entity_type.properties.find { |p| p.name == property_name }
|
39
|
+
raise OData::AbstractQuery::Errors::PropertyNotFound.new(query, property_name) if property.blank?
|
40
|
+
|
41
|
+
property
|
42
|
+
else
|
43
|
+
raise OData::AbstractQuery::Errors::PropertyNotFound.new(query, path)
|
44
|
+
end
|
45
|
+
}
|
46
|
+
|
47
|
+
query.Option(self, properties)
|
48
|
+
else
|
49
|
+
raise OData::AbstractQuery::Errors::InvalidOptionContext.new(query, self.option_name) unless value.blank?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def entity_type
|
54
|
+
return nil if self.query.segments.empty?
|
55
|
+
return nil unless self.query.segments.last.respond_to?(:entity_type)
|
56
|
+
@entity_type ||= self.query.segments.last.entity_type
|
57
|
+
end
|
58
|
+
|
59
|
+
def valid?
|
60
|
+
entity_type = self.entity_type
|
61
|
+
return false if entity_type.blank?
|
62
|
+
|
63
|
+
@properties.is_a?(Array) && @properties.all? { |property|
|
64
|
+
property.is_a?(OData::AbstractSchema::Property) && !!entity_type.properties.find { |p| p == property }
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def value
|
69
|
+
"'" + @properties.collect(&:name).join(',') + "'"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module OData
|
2
|
+
module AbstractQuery
|
3
|
+
module Options
|
4
|
+
class SkipOption < OData::AbstractQuery::Option
|
5
|
+
def self.option_name
|
6
|
+
'$skip'
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(query, key, value = nil)
|
10
|
+
super(query, key, value)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.applies_to?(query)
|
14
|
+
return false if query.segments.empty?
|
15
|
+
return false unless query.segments.last.respond_to?(:countable?)
|
16
|
+
query.segments.last.countable?
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.parse!(query, key, value = nil)
|
20
|
+
return nil unless key == self.option_name
|
21
|
+
|
22
|
+
query.Option(self, key, value.to_i)
|
23
|
+
end
|
24
|
+
|
25
|
+
def valid?
|
26
|
+
return false if self.value.blank?
|
27
|
+
self.value.to_i >= 0
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module OData
|
2
|
+
module AbstractQuery
|
3
|
+
module Options
|
4
|
+
class TopOption < OData::AbstractQuery::Option
|
5
|
+
def self.option_name
|
6
|
+
'$top'
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(query, key, value = nil)
|
10
|
+
super(query, key, value)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.applies_to?(query)
|
14
|
+
return false if query.segments.empty?
|
15
|
+
return false unless query.segments.last.respond_to?(:countable?)
|
16
|
+
query.segments.last.countable?
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.parse!(query, key, value = nil)
|
20
|
+
return nil unless key == self.option_name
|
21
|
+
|
22
|
+
query.Option(self, key, value.to_i)
|
23
|
+
end
|
24
|
+
|
25
|
+
def valid?
|
26
|
+
return false if self.value.blank?
|
27
|
+
self.value.to_i >= 1
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module OData
|
2
|
+
module AbstractQuery
|
3
|
+
class Parser
|
4
|
+
cattr_reader :reserved_option_names
|
5
|
+
@@reserved_option_names = %w{orderby expand select top skip filter format inlinecount}.freeze
|
6
|
+
|
7
|
+
attr_reader :schema
|
8
|
+
|
9
|
+
def initialize(schema)
|
10
|
+
@schema = schema
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse!(uri)
|
14
|
+
return nil if uri.blank?
|
15
|
+
|
16
|
+
query = @schema.Query
|
17
|
+
|
18
|
+
resource_path, query_string = uri.split('?', 2)
|
19
|
+
|
20
|
+
unless resource_path.blank?
|
21
|
+
resource_path_components = resource_path.split('/')
|
22
|
+
resource_path_components.each_index do |i|
|
23
|
+
resource_path_component = resource_path_components[i]
|
24
|
+
segment = _parse_segment!(query, resource_path_component)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
unless query_string.blank?
|
29
|
+
query_string_components = query_string.split('&')
|
30
|
+
query_string_components.each_index do |i|
|
31
|
+
query_string_component = query_string_components[i]
|
32
|
+
option = _parse_option!(query, query_string_component)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
query
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
def _parse_segment!(query, resource_path_component)
|
42
|
+
Object.subclasses_of(Segment).each do |segment_class|
|
43
|
+
if segment_class.can_follow?(query.segments.last)
|
44
|
+
if segment = segment_class.parse!(query, resource_path_component)
|
45
|
+
return segment
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
raise Errors::ParseQuerySegmentException.new(query, resource_path_component)
|
51
|
+
end
|
52
|
+
|
53
|
+
def _parse_option!(query, query_string_component)
|
54
|
+
key, value = query_string_component.split('=', 2)
|
55
|
+
|
56
|
+
if md = key.match(/^\$(.*?)$/)
|
57
|
+
raise Errors::InvalidReservedOptionName.new(query, key, value) unless @@reserved_option_names.include?(md[1])
|
58
|
+
end
|
59
|
+
|
60
|
+
if md = value.match(/^'\s*([^']+)\s*'$/)
|
61
|
+
value = md[1]
|
62
|
+
end
|
63
|
+
|
64
|
+
Object.subclasses_of(Option).each do |option_class|
|
65
|
+
if option_class.applies_to?(query)
|
66
|
+
if option = option_class.parse!(query, key, value)
|
67
|
+
return option
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# basic (or "custom") option
|
73
|
+
query.Option(BasicOption, key, value)
|
74
|
+
end
|
75
|
+
end # Parser
|
76
|
+
end # AbstractQuery
|
77
|
+
end # OData
|