frodo 0.10.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 (153) hide show
  1. checksums.yaml +7 -0
  2. data/.autotest +2 -0
  3. data/.circleci/config.yml +54 -0
  4. data/.gitignore +24 -0
  5. data/.gitlab-ci.yml +9 -0
  6. data/.rspec +2 -0
  7. data/.ruby-gemset +1 -0
  8. data/.ruby-version +1 -0
  9. data/.travis.yml +75 -0
  10. data/CHANGELOG.md +163 -0
  11. data/Gemfile +4 -0
  12. data/LICENSE.txt +23 -0
  13. data/README.md +479 -0
  14. data/Rakefile +7 -0
  15. data/TODO.md +55 -0
  16. data/frodo.gemspec +39 -0
  17. data/images/frodo.jpg +0 -0
  18. data/lib/frodo/abstract_client.rb +11 -0
  19. data/lib/frodo/client.rb +6 -0
  20. data/lib/frodo/concerns/api.rb +292 -0
  21. data/lib/frodo/concerns/authentication.rb +32 -0
  22. data/lib/frodo/concerns/base.rb +84 -0
  23. data/lib/frodo/concerns/caching.rb +26 -0
  24. data/lib/frodo/concerns/connection.rb +79 -0
  25. data/lib/frodo/concerns/verbs.rb +68 -0
  26. data/lib/frodo/config.rb +143 -0
  27. data/lib/frodo/entity.rb +335 -0
  28. data/lib/frodo/entity_container.rb +75 -0
  29. data/lib/frodo/entity_set.rb +131 -0
  30. data/lib/frodo/errors.rb +70 -0
  31. data/lib/frodo/middleware/authentication/token.rb +13 -0
  32. data/lib/frodo/middleware/authentication.rb +87 -0
  33. data/lib/frodo/middleware/authorization.rb +18 -0
  34. data/lib/frodo/middleware/caching.rb +30 -0
  35. data/lib/frodo/middleware/custom_headers.rb +14 -0
  36. data/lib/frodo/middleware/gzip.rb +33 -0
  37. data/lib/frodo/middleware/instance_url.rb +20 -0
  38. data/lib/frodo/middleware/logger.rb +42 -0
  39. data/lib/frodo/middleware/multipart.rb +64 -0
  40. data/lib/frodo/middleware/odata_headers.rb +13 -0
  41. data/lib/frodo/middleware/raise_error.rb +47 -0
  42. data/lib/frodo/middleware.rb +33 -0
  43. data/lib/frodo/navigation_property/proxy.rb +80 -0
  44. data/lib/frodo/navigation_property.rb +29 -0
  45. data/lib/frodo/properties/binary.rb +50 -0
  46. data/lib/frodo/properties/boolean.rb +37 -0
  47. data/lib/frodo/properties/collection.rb +50 -0
  48. data/lib/frodo/properties/complex.rb +114 -0
  49. data/lib/frodo/properties/date.rb +27 -0
  50. data/lib/frodo/properties/date_time.rb +83 -0
  51. data/lib/frodo/properties/date_time_offset.rb +17 -0
  52. data/lib/frodo/properties/decimal.rb +54 -0
  53. data/lib/frodo/properties/enum.rb +62 -0
  54. data/lib/frodo/properties/float.rb +67 -0
  55. data/lib/frodo/properties/geography/base.rb +162 -0
  56. data/lib/frodo/properties/geography/line_string.rb +33 -0
  57. data/lib/frodo/properties/geography/point.rb +31 -0
  58. data/lib/frodo/properties/geography/polygon.rb +38 -0
  59. data/lib/frodo/properties/geography.rb +13 -0
  60. data/lib/frodo/properties/guid.rb +17 -0
  61. data/lib/frodo/properties/integer.rb +107 -0
  62. data/lib/frodo/properties/number.rb +14 -0
  63. data/lib/frodo/properties/string.rb +72 -0
  64. data/lib/frodo/properties/time.rb +40 -0
  65. data/lib/frodo/properties/time_of_day.rb +27 -0
  66. data/lib/frodo/properties.rb +32 -0
  67. data/lib/frodo/property.rb +139 -0
  68. data/lib/frodo/property_registry.rb +41 -0
  69. data/lib/frodo/query/criteria/comparison_operators.rb +49 -0
  70. data/lib/frodo/query/criteria/date_functions.rb +61 -0
  71. data/lib/frodo/query/criteria/geography_functions.rb +21 -0
  72. data/lib/frodo/query/criteria/lambda_operators.rb +27 -0
  73. data/lib/frodo/query/criteria/string_functions.rb +40 -0
  74. data/lib/frodo/query/criteria.rb +92 -0
  75. data/lib/frodo/query/in_batches.rb +58 -0
  76. data/lib/frodo/query.rb +221 -0
  77. data/lib/frodo/railtie.rb +19 -0
  78. data/lib/frodo/schema/complex_type.rb +79 -0
  79. data/lib/frodo/schema/enum_type.rb +95 -0
  80. data/lib/frodo/schema.rb +164 -0
  81. data/lib/frodo/service.rb +199 -0
  82. data/lib/frodo/service_registry.rb +52 -0
  83. data/lib/frodo/version.rb +3 -0
  84. data/lib/frodo.rb +67 -0
  85. data/spec/fixtures/auth_success_response.json +11 -0
  86. data/spec/fixtures/error.json +11 -0
  87. data/spec/fixtures/files/entity_to_xml.xml +18 -0
  88. data/spec/fixtures/files/error.xml +5 -0
  89. data/spec/fixtures/files/metadata.xml +150 -0
  90. data/spec/fixtures/files/metadata_with_error.xml +157 -0
  91. data/spec/fixtures/files/product_0.json +10 -0
  92. data/spec/fixtures/files/product_0.xml +28 -0
  93. data/spec/fixtures/files/products.json +106 -0
  94. data/spec/fixtures/files/products.xml +308 -0
  95. data/spec/fixtures/files/supplier_0.json +26 -0
  96. data/spec/fixtures/files/supplier_0.xml +32 -0
  97. data/spec/fixtures/leads.json +923 -0
  98. data/spec/fixtures/refresh_error_response.json +8 -0
  99. data/spec/frodo/abstract_client_spec.rb +13 -0
  100. data/spec/frodo/client_spec.rb +57 -0
  101. data/spec/frodo/concerns/authentication_spec.rb +79 -0
  102. data/spec/frodo/concerns/base_spec.rb +68 -0
  103. data/spec/frodo/concerns/caching_spec.rb +40 -0
  104. data/spec/frodo/concerns/connection_spec.rb +65 -0
  105. data/spec/frodo/config_spec.rb +127 -0
  106. data/spec/frodo/entity/shared_examples.rb +83 -0
  107. data/spec/frodo/entity_container_spec.rb +38 -0
  108. data/spec/frodo/entity_set_spec.rb +169 -0
  109. data/spec/frodo/entity_spec.rb +153 -0
  110. data/spec/frodo/errors_spec.rb +48 -0
  111. data/spec/frodo/middleware/authentication/token_spec.rb +87 -0
  112. data/spec/frodo/middleware/authentication_spec.rb +83 -0
  113. data/spec/frodo/middleware/authorization_spec.rb +17 -0
  114. data/spec/frodo/middleware/custom_headers_spec.rb +21 -0
  115. data/spec/frodo/middleware/gzip_spec.rb +68 -0
  116. data/spec/frodo/middleware/instance_url_spec.rb +27 -0
  117. data/spec/frodo/middleware/logger_spec.rb +21 -0
  118. data/spec/frodo/middleware/odata_headers_spec.rb +15 -0
  119. data/spec/frodo/middleware/raise_error_spec.rb +66 -0
  120. data/spec/frodo/navigation_property/proxy_spec.rb +46 -0
  121. data/spec/frodo/navigation_property_spec.rb +55 -0
  122. data/spec/frodo/properties/binary_spec.rb +50 -0
  123. data/spec/frodo/properties/boolean_spec.rb +72 -0
  124. data/spec/frodo/properties/collection_spec.rb +44 -0
  125. data/spec/frodo/properties/date_spec.rb +23 -0
  126. data/spec/frodo/properties/date_time_offset_spec.rb +30 -0
  127. data/spec/frodo/properties/date_time_spec.rb +23 -0
  128. data/spec/frodo/properties/decimal_spec.rb +50 -0
  129. data/spec/frodo/properties/float_spec.rb +45 -0
  130. data/spec/frodo/properties/geography/line_string_spec.rb +33 -0
  131. data/spec/frodo/properties/geography/point_spec.rb +29 -0
  132. data/spec/frodo/properties/geography/polygon_spec.rb +55 -0
  133. data/spec/frodo/properties/geography/shared_examples.rb +72 -0
  134. data/spec/frodo/properties/guid_spec.rb +17 -0
  135. data/spec/frodo/properties/integer_spec.rb +58 -0
  136. data/spec/frodo/properties/string_spec.rb +46 -0
  137. data/spec/frodo/properties/time_of_day_spec.rb +23 -0
  138. data/spec/frodo/properties/time_spec.rb +15 -0
  139. data/spec/frodo/property_registry_spec.rb +16 -0
  140. data/spec/frodo/property_spec.rb +71 -0
  141. data/spec/frodo/query/criteria_spec.rb +229 -0
  142. data/spec/frodo/query_spec.rb +156 -0
  143. data/spec/frodo/schema/complex_type_spec.rb +97 -0
  144. data/spec/frodo/schema/enum_type_spec.rb +112 -0
  145. data/spec/frodo/schema_spec.rb +113 -0
  146. data/spec/frodo/service_registry_spec.rb +19 -0
  147. data/spec/frodo/service_spec.rb +153 -0
  148. data/spec/frodo/usage_example_spec.rb +161 -0
  149. data/spec/spec_helper.rb +35 -0
  150. data/spec/support/coverage.rb +2 -0
  151. data/spec/support/fixture_helpers.rb +14 -0
  152. data/spec/support/middleware.rb +19 -0
  153. metadata +479 -0
@@ -0,0 +1,50 @@
1
+ module Frodo
2
+ module Properties
3
+ # Defines the Collection Frodo type.
4
+ class Collection < Frodo::Property
5
+ # Overriding default constructor to avoid converting
6
+ # value to string.
7
+ # TODO: Make this the default for all property types?
8
+ def initialize(name, value, options = {})
9
+ super(name, value, options)
10
+ self.value = value
11
+ end
12
+
13
+ def value
14
+ if @value.nil?
15
+ nil
16
+ else
17
+ @value.map(&:value)
18
+ end
19
+ end
20
+
21
+ def value=(value)
22
+ if value.nil? && allows_nil?
23
+ @value = nil
24
+ elsif value.respond_to?(:map)
25
+ @value = value.map.with_index do |element, index|
26
+ type_class.new("#{name}[#{index}]", element)
27
+ end
28
+ else
29
+ validation_error 'Value must be an array'
30
+ end
31
+ end
32
+
33
+ def url_value
34
+ '[' + @value.map(&:url_value).join(',') + ']'
35
+ end
36
+
37
+ def type
38
+ "Collection(#{value_type})"
39
+ end
40
+
41
+ def value_type
42
+ options[:value_type] || 'Edm.String'
43
+ end
44
+
45
+ def type_class
46
+ Frodo::PropertyRegistry[value_type]
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,114 @@
1
+ module Frodo
2
+ module Properties
3
+ # Abstract base class for Frodo ComplexTypes
4
+ # @see [Frodo::Schema::ComplexType]
5
+ class Complex < Frodo::Property
6
+ def initialize(name, value, options = {})
7
+ super(name, value, options)
8
+ init_properties
9
+ end
10
+
11
+ # Returns the property value, properly typecast
12
+ # @return [Hash, nil]
13
+ def value
14
+ if allows_nil? && properties.values.all?(&:nil?)
15
+ nil
16
+ else
17
+ Hash[properties.map { |key, value| [key, value.value] }]
18
+ end
19
+ end
20
+
21
+ # Sets the property value
22
+ # @params new_value [Hash]
23
+ def value=(new_value)
24
+ validate(new_value)
25
+ if new_value.nil?
26
+ property_names.each { |name| self[name] = nil }
27
+ else
28
+ property_names.each { |name| self[name] = new_value[name] }
29
+ end
30
+ end
31
+
32
+ # Returns a list of this ComplexType's property names.
33
+ # @return [Array<String>]
34
+ def property_names
35
+ @property_names ||= properties.keys
36
+ end
37
+
38
+ # Returns the value of the requested property.
39
+ # @param property_name [to_s]
40
+ # @return [*]
41
+ def [](property_name)
42
+ properties[property_name.to_s].value
43
+ end
44
+
45
+ # Sets the value of the named property.
46
+ # @param property_name [to_s]
47
+ # @param value [*]
48
+ # @return [*]
49
+ def []=(property_name, value)
50
+ properties[property_name.to_s].value = value
51
+ end
52
+
53
+ # Returns the XML representation of the property to the supplied XML
54
+ # builder.
55
+ # @param xml_builder [Nokogiri::XML::Builder]
56
+ def to_xml(xml_builder)
57
+ attributes = {
58
+ 'metadata:type' => type,
59
+ }
60
+
61
+ xml_builder['data'].send(name.to_sym, attributes) do
62
+ properties.each do |name, property|
63
+ property.to_xml(xml_builder)
64
+ end
65
+ end
66
+ end
67
+
68
+ # Creates a new property instance from an XML element
69
+ # @param property_xml [Nokogiri::XML::Element]
70
+ # @param options [Hash]
71
+ # @return [Frodo::Property]
72
+ def self.from_xml(property_xml, options = {})
73
+ nodes = property_xml.element_children
74
+ props = Hash[nodes.map { |el| [el.name, el.content] }]
75
+ new(property_xml.name, props.to_json, options)
76
+ end
77
+
78
+ private
79
+
80
+ def complex_type
81
+ raise NotImplementedError, 'Subclass must override'
82
+ end
83
+
84
+ def properties
85
+ @properties
86
+ end
87
+
88
+ def init_properties
89
+ @properties = complex_type.send(:collect_properties)
90
+ set_properties(JSON.parse(@value)) unless @value.nil?
91
+ end
92
+
93
+ def set_properties(new_properties)
94
+ property_names.each do |prop_name|
95
+ self[prop_name] = new_properties[prop_name]
96
+ end
97
+ end
98
+
99
+ def validate(value)
100
+ return if value.nil? && allows_nil?
101
+ raise ArgumentError, 'Value must be a Hash' unless value.is_a?(Hash)
102
+ value.keys.each do |name|
103
+ unless property_names.include?(name) || name =~ /@odata/
104
+ raise ArgumentError, "Invalid property #{name}"
105
+ end
106
+ end
107
+ end
108
+
109
+ def validate_options(options)
110
+ raise ArgumentError, 'Type is required' unless options[:type]
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,27 @@
1
+ require 'frodo/properties/date_time'
2
+
3
+ module Frodo
4
+ module Properties
5
+ # Defines the Date Frodo type.
6
+ class Date < Frodo::Properties::DateTime
7
+ # The Frodo type name
8
+ def type
9
+ 'Edm.Date'
10
+ end
11
+
12
+ def url_value
13
+ @value
14
+ end
15
+
16
+ protected
17
+
18
+ def date_class
19
+ ::Date
20
+ end
21
+
22
+ def strptime_format
23
+ '%Y-%m-%d'
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,83 @@
1
+ module Frodo
2
+ module Properties
3
+ # Defines the DateTime Frodo type.
4
+ class DateTime < Frodo::Property
5
+ # Returns the property value, properly typecast
6
+ # @return [DateTime, nil]
7
+ def value
8
+ if (@value.nil? || @value.empty?) && allows_nil?
9
+ nil
10
+ else
11
+ begin
12
+ date_class.strptime(@value, strptime_format)
13
+ rescue ArgumentError
14
+ date_class.parse(@value)
15
+ end
16
+ end
17
+ end
18
+
19
+ # Sets the property value
20
+ # @params new_value [DateTime]
21
+ def value=(new_value)
22
+ validate(new_value)
23
+ @value = parse_value(new_value)
24
+ end
25
+
26
+ # The Frodo type name
27
+ def type
28
+ 'Edm.DateTime'
29
+ end
30
+
31
+ # Value to be used in JSON.
32
+ # @return [String]
33
+ def xml_value
34
+ @value.andand.sub(/[\+\-]00:00$/, 'Z')
35
+ end
36
+
37
+ # Value to be used in JSON.
38
+ # @return [String]
39
+ def json_value
40
+ xml_value
41
+ end
42
+
43
+ # Value to be used in URLs.
44
+ # @return [String]
45
+ def url_value
46
+ xml_value
47
+ end
48
+
49
+ protected
50
+
51
+ # Specifies date/time implementation to use
52
+ def date_class
53
+ ::DateTime
54
+ end
55
+
56
+ # Specifies the date/time format string used for `strptime`
57
+ def strptime_format
58
+ #'%Y-%m-%dT%H:%M:%S.%L'
59
+ "%Y-%m-%dT%H:%M:%SZ"
60
+ end
61
+
62
+ def validate(value)
63
+ begin
64
+ return if value.nil? && allows_nil?
65
+ return if value.is_a?(date_class)
66
+ date_class.parse(value)
67
+ rescue
68
+ validation_error "Value '#{value}' is not a date time format that can be parsed"
69
+ end
70
+ end
71
+
72
+ def parse_value(value)
73
+ return value if value.nil? && allows_nil?
74
+ if value.is_a?(date_class)
75
+ parsed_value = value
76
+ else
77
+ parsed_value = date_class.parse(value)
78
+ end
79
+ parsed_value.strftime(strptime_format)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,17 @@
1
+ module Frodo
2
+ module Properties
3
+ # Defines the DateTimeOffset Frodo type.
4
+ class DateTimeOffset < Frodo::Properties::DateTime
5
+ # The Frodo type name
6
+ def type
7
+ 'Edm.DateTimeOffset'
8
+ end
9
+
10
+ protected
11
+
12
+ def strptime_format
13
+ '%Y-%m-%dT%H:%M:%S%:z'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,54 @@
1
+ module Frodo
2
+ module Properties
3
+ # Defines the Decimal Frodo type.
4
+ class Decimal < Frodo::Property
5
+ # Returns the property value, properly typecast
6
+ # @return [BigDecimal,nil]
7
+ def value
8
+ if (@value.nil? || @value.empty?) && (!strict? && allows_nil?)
9
+ nil
10
+ else
11
+ BigDecimal(@value)
12
+ end
13
+ end
14
+
15
+ # Sets the property value
16
+ # @params new_value something BigDecimal() can parse
17
+ def value=(new_value)
18
+ @value = if (new_value.nil? && !strict? && allows_nil?)
19
+ nil
20
+ else
21
+ validate(BigDecimal(new_value.to_s))
22
+ new_value.to_s
23
+ end
24
+ end
25
+
26
+ # The Frodo type name
27
+ def type
28
+ 'Edm.Decimal'
29
+ end
30
+
31
+ # Value to be used in URLs.
32
+ # @return [String]
33
+ def url_value
34
+ "#{value.to_f}"
35
+ end
36
+
37
+ private
38
+
39
+ def validate(value)
40
+ if value > max_value || value < min_value || value.precs.first > 29
41
+ validation_error "Value is outside accepted range: #{min_value} to #{max_value}, or has more than 29 significant digits"
42
+ end
43
+ end
44
+
45
+ def min_value
46
+ @min ||= BigDecimal(-7.9 * (10**28), 2)
47
+ end
48
+
49
+ def max_value
50
+ @max ||= BigDecimal(7.9 * (10**28), 2)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,62 @@
1
+ module Frodo
2
+ module Properties
3
+ # Abstract base class for Frodo EnumTypes
4
+ # @see [Frodo::Schema::EnumType]
5
+ class Enum < Frodo::Property
6
+ # Returns the property value, properly typecast
7
+ # @return [String, nil]
8
+ def value
9
+ if @value.nil? && allows_nil?
10
+ nil
11
+ else
12
+ @value
13
+ end
14
+ end
15
+
16
+ # Sets the property value
17
+ # @params new_value [String]
18
+ def value=(new_value)
19
+ parsed_value = validate(new_value)
20
+ @value = is_flags? ? parsed_value : parsed_value.first
21
+ end
22
+
23
+ # Value to be used in URLs.
24
+ # @return [String]
25
+ def url_value
26
+ "#{type}'#{@value}'"
27
+ end
28
+
29
+ private
30
+
31
+ def members
32
+ raise NotImplementedError, 'Subclass must override'
33
+ end
34
+
35
+ def validate(value)
36
+ return [] if value.nil? && allows_nil?
37
+ values = parse_value(value)
38
+
39
+ if values.length > 1 && !is_flags?
40
+ raise ArgumentError, 'Multiple values are not allowed for this property'
41
+ end
42
+
43
+ values.map do |value|
44
+ if members.keys.include?(value)
45
+ members[value]
46
+ elsif members.values.include?(value)
47
+ value
48
+ else
49
+ validation_error "Value must be one of #{members.to_a}, but was: '#{value}'"
50
+ end
51
+ end.compact
52
+ end
53
+
54
+ def parse_value(value)
55
+ return nil if value.nil?
56
+ value.to_s.split(',').map(&:strip).map do |val|
57
+ val =~ /^[0-9]+$/ ? val.to_i : val
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,67 @@
1
+ module Frodo
2
+ module Properties
3
+ # Defines the Float Frodo type.
4
+ class Float < Frodo::Property
5
+ include Frodo::Properties::Number
6
+
7
+ # Returns the property value, properly typecast
8
+ # @return [Float,nil]
9
+ def value
10
+ if (@value.nil? || @value.empty?) && allows_nil?
11
+ nil
12
+ else
13
+ @value.to_f
14
+ end
15
+ end
16
+
17
+ # Sets the property value
18
+ # @params new_value [to_f]
19
+ def value=(new_value)
20
+ validate(new_value.to_f)
21
+ @value = new_value.to_f.to_s
22
+ end
23
+
24
+ # The Frodo type name
25
+ def type
26
+ 'Edm.Double'
27
+ end
28
+
29
+ private
30
+
31
+ def min_value
32
+ @min ||= -(1.7 * (10**308))
33
+ end
34
+
35
+ def max_value
36
+ @max ||= (1.7 * (10**308))
37
+ end
38
+ end
39
+
40
+ # Defines the Double (Float) Frodo type.
41
+ class Double < Frodo::Properties::Float; end
42
+
43
+ # Defines the Single (Float) Frodo type.
44
+ class Single < Frodo::Properties::Float
45
+ # The Frodo type name
46
+ def type
47
+ 'Edm.Single'
48
+ end
49
+
50
+ # Value to be used in URLs.
51
+ # @return [String]
52
+ def url_value
53
+ "#{value}F"
54
+ end
55
+
56
+ private
57
+
58
+ def min_value
59
+ @min ||= -(3.4 * (10**38))
60
+ end
61
+
62
+ def max_value
63
+ @max ||= (3.4 * (10**38))
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,162 @@
1
+ module Frodo
2
+ module Properties
3
+ module Geography
4
+ class Base < Frodo::Property
5
+ # The SRID (Spatial Reference ID) of this property.
6
+ attr_accessor :srid
7
+
8
+ # The default SRID (same as used by GPS)
9
+ DEFAULT_SRID = 4326
10
+
11
+ # Initializes a geography property.
12
+ #
13
+ # Special options available for geographic types:
14
+ #
15
+ # +srid+: the SRID (spatial reference ID) of the
16
+ # coordinate system being used.
17
+ # Defaults to 4326 (same as GPS).
18
+ #
19
+ # @param name [to_s]
20
+ # @param value [Hash|Array|to_s|nil]
21
+ # @param options [Hash]
22
+ def initialize(name, value, options = {})
23
+ super(name, value, options)
24
+ self.value = value
25
+ self.srid = srid || options[:srid] || DEFAULT_SRID
26
+ end
27
+
28
+ # Sets the value of the property.
29
+ # @param value [Hash|Array|to_s|nil]
30
+ def value=(value)
31
+ if value.nil? && allows_nil?
32
+ @value = nil
33
+ elsif value.is_a?(Hash)
34
+ @value = value['coordinates']
35
+ elsif value.is_a?(Array)
36
+ @value = value
37
+ else
38
+ @value = parse_wkt(value.to_s)
39
+ end
40
+ end
41
+
42
+ # Value to be used in URLs.
43
+ # @return [String]
44
+ def url_value
45
+ "geography'SRID=#{srid};#{type_name}(#{coords_to_s})'"
46
+ end
47
+
48
+ # Value to be used in JSON.
49
+ # @return [Hash]
50
+ def json_value
51
+ {
52
+ type: type_name,
53
+ coordinates: value,
54
+ crs: crs
55
+ }
56
+ end
57
+
58
+ # The name of the SRS (Spatial Reference System) used.
59
+ # Basically, the SRID in URI/URL form.
60
+ # @return [String]
61
+ def srs_name
62
+ if srid == DEFAULT_SRID
63
+ "http://www.opengis.net/def/crs/EPSG/0/#{srid}"
64
+ else
65
+ raise NotImplementedError, "Unsupported SRID #{srid}"
66
+ end
67
+ end
68
+
69
+ # The name of the CRS (Coordinate Reference System) used.
70
+ # Used in GeoJSON representation
71
+ # @return [String]
72
+ def crs_name
73
+ if srid == DEFAULT_SRID
74
+ "EPSG:#{srid}"
75
+ else
76
+ raise NotImplementedError, "Unsupported SRID #{srid}"
77
+ end
78
+ end
79
+
80
+ # The full CRS representation as used by GeoJSON
81
+ # @return [Hash]
82
+ def crs
83
+ {
84
+ type: 'name',
85
+ properties: { name: crs_name }
86
+ }
87
+ end
88
+
89
+ # Returns the XML representation of the property to the supplied XML
90
+ # builder.
91
+ # @param xml_builder [Nokogiri::XML::Builder]
92
+ def to_xml(xml_builder)
93
+ attributes = { 'metadata:type' => type }
94
+ type_attrs = { 'gml:srsName' => srs_name }
95
+
96
+ xml_builder['data'].send(name.to_sym, attributes) do
97
+ xml_builder['gml'].send(type_name, type_attrs) do
98
+ value_to_xml(xml_value, xml_builder)
99
+ end
100
+ end
101
+ end
102
+
103
+ # Creates a new property instance from an XML element
104
+ # @param property_xml [Nokogiri::XML::Element]
105
+ # @param options [Hash]
106
+ # @return [Frodo::Properties::Geography]
107
+ def self.from_xml(property_xml, options = {})
108
+ if property_xml.attributes['null'].andand.value == 'true'
109
+ content = nil
110
+ else
111
+ content = parse_xml(property_xml)
112
+ options.merge!(srid: srid_from_xml(property_xml))
113
+ end
114
+
115
+ new(property_xml.name, content, options)
116
+ end
117
+
118
+ protected
119
+
120
+ def type_name
121
+ self.class.name.split('::').last
122
+ end
123
+
124
+ def parse_wkt(value)
125
+ if value =~ /^geography'(SRID=(\d+);)+(\w+)\((.+)\)'$/
126
+ $3 == type_name or raise ArgumentError, "Invalid geography type '#{$3}'"
127
+ self.srid = $1 ? $2.to_i : DEFAULT_SRID
128
+ coords_from_s($4)
129
+ else
130
+ raise ArgumentError, "Invalid geography value '#{value}'"
131
+ end
132
+ end
133
+
134
+ # Recursively turn a JSON-like data structure into XML
135
+ def value_to_xml(value, xml_builder)
136
+ if value.is_a?(Hash)
137
+ value.each do |key, val|
138
+ xml_builder['gml'].send(key) do
139
+ value_to_xml(val, xml_builder)
140
+ end
141
+ end
142
+ elsif value.is_a?(Array)
143
+ value.each do |val|
144
+ value_to_xml(val, xml_builder)
145
+ end
146
+ else
147
+ xml_builder.text(value)
148
+ end
149
+ end
150
+
151
+ # Extract the SRID from a GML element's `srsName` attribute
152
+ def self.srid_from_xml(property_xml)
153
+ gml_elem = property_xml.element_children.first
154
+ srs_attr = gml_elem.attributes['srsName']
155
+ if srs_attr
156
+ srs_attr.value.split(/[\/:]/).last.to_i
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,33 @@
1
+ module Frodo
2
+ module Properties
3
+ module Geography
4
+ class LineString < Base
5
+ def type
6
+ 'Edm.GeographyLineString'
7
+ end
8
+
9
+ def coords_to_s
10
+ value.map { |pos| pos.join(' ') }.join(',')
11
+ end
12
+
13
+ def coords_from_s(str)
14
+ str.split(',').map { |pos| pos.split(' ').map(&:to_f) }
15
+ end
16
+
17
+ def xml_value
18
+ value.map do |coords|
19
+ { pos: coords.join(' ') }
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def self.parse_xml(property_xml)
26
+ property_xml.xpath('//pos').map do |el|
27
+ el.content.split(' ').map(&:to_f)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,31 @@
1
+ module Frodo
2
+ module Properties
3
+ module Geography
4
+ class Point < Base
5
+ def type
6
+ 'Edm.GeographyPoint'
7
+ end
8
+
9
+ def coords_to_s
10
+ value.join(' ')
11
+ end
12
+
13
+ def coords_from_s(str)
14
+ str.split(' ').map(&:to_f)
15
+ end
16
+
17
+ def xml_value
18
+ { pos: coords_to_s }
19
+ end
20
+
21
+ private
22
+
23
+ def self.parse_xml(property_xml)
24
+ property_xml.xpath('//pos').map do |el|
25
+ el.content.split(' ').map(&:to_f)
26
+ end.flatten
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end