dse-driver 1.0.1

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.
@@ -0,0 +1,181 @@
1
+ # encoding: utf-8
2
+
3
+ #--
4
+ # Copyright (C) 2016 DataStax Inc.
5
+ #
6
+ # This software can be used solely with DataStax Enterprise. Please consult the license at
7
+ # http://www.datastax.com/terms/datastax-dse-driver-license-terms
8
+ #++
9
+
10
+ module Dse
11
+ module Geometry
12
+ # Encapsulates a set of lines, characterized by a sequence of {Point}s in the xy-plane. It corresponds to the
13
+ # `org.apache.cassandra.db.marshal.LineStringType` column type in DSE.
14
+ #
15
+ # @see https://en.wikipedia.org/wiki/Well-known_text Wikipedia article on Well Known Text
16
+ class LineString
17
+ include Cassandra::CustomData
18
+
19
+ # @return [Array<Point>] collection of points that make up this line-string.
20
+ attr_reader :points
21
+
22
+ # @private
23
+ WKT_RE = /^LINESTRING\s*\(\s*([^)]+)\s*\)$/
24
+ # @private
25
+ POINT_SEPARATOR_RE = /\s*,\s*/
26
+
27
+ # @param args [Array<Point>,Array<String>] varargs-style arguments in two forms:
28
+ # <ul><li>ordered collection of points that make up this line-string.
29
+ # Must be empty or have at least two points.</li>
30
+ # <li>one-element string array with the wkt representation.</li></ul>
31
+ #
32
+ # @example Construct an empty LineString
33
+ # line = LineString.new
34
+ # @example Construct a LineString with Point objects.
35
+ # line = LineString.new(Point.new(1.0, 2.0), Point.new(3.0, 4.0))
36
+ # @example Construct a LineString with a wkt string.
37
+ # line = LineString.new('LINESTRING (1.0 2.0, 3.0 4.0)')
38
+ def initialize(*args)
39
+ # The constructor has two forms:
40
+ # 1. 0, 2, or more Point objects in the order in which one connects them to make a line-string.
41
+ # 2. one String arg as the wkt representation.
42
+
43
+ if args.size == 1
44
+ wkt = args.first
45
+ Cassandra::Util.assert_instance_of(String, wkt)
46
+
47
+ if wkt == 'LINESTRING EMPTY'
48
+ @points = [].freeze
49
+ else
50
+ match = wkt.match(WKT_RE)
51
+ raise ArgumentError, "#{wkt.inspect} is not a valid WKT representation of a line-string" unless match
52
+ @points = self.class.parse_wkt_internal(match[1])
53
+ end
54
+ else
55
+ @points = args.freeze
56
+ @points.each do |p|
57
+ Cassandra::Util.assert_instance_of(Point, p, "#{p.inspect} is not a Point")
58
+ end
59
+ end
60
+ end
61
+
62
+ # @private
63
+ def self.parse_wkt_internal(line_str)
64
+ point_strings = line_str.split(POINT_SEPARATOR_RE)
65
+ points = []
66
+ point_strings.each do |ps|
67
+ next if ps.empty?
68
+ points << Point.new(*Point.parse_wkt_internal(ps))
69
+ end
70
+ points.freeze
71
+ end
72
+
73
+ # @return [String] well-known-text representation of this line-string.
74
+ def wkt
75
+ @points.empty? ? 'LINESTRING EMPTY' : "LINESTRING (#{wkt_internal})"
76
+ end
77
+
78
+ # @return [String] a human-readable English string describing this {LineString}.
79
+ def to_s
80
+ @points.join(' to ')
81
+ end
82
+
83
+ # @private
84
+ def wkt_internal
85
+ # This is a helper used to embed point coords into some container (e.g. polygon)
86
+ @points.map(&:wkt_internal).join(', ')
87
+ end
88
+
89
+ # @private
90
+ def eql?(other)
91
+ other.is_a?(LineString) && \
92
+ @points == other.points
93
+ end
94
+ alias == eql?
95
+
96
+ # @private
97
+ def hash
98
+ @hash ||= 31 * 17 + @points.hash
99
+ end
100
+
101
+ # @private
102
+ def inspect
103
+ "#<LineString:0x#{object_id.to_s(16)} " \
104
+ "@points=#{@points.inspect}>"
105
+ end
106
+
107
+ # methods related to serializing/deserializing.
108
+
109
+ # @private
110
+ TYPE = Cassandra::Types::Custom.new('org.apache.cassandra.db.marshal.LineStringType')
111
+
112
+ # @return [Cassandra::Types::Custom] type of column that is processed by this domain object class.
113
+ def self.type
114
+ TYPE
115
+ end
116
+
117
+ # Deserialize the given data into an instance of this domain object class.
118
+ # @param data [String] byte-array representation of a column value of this custom type.
119
+ # @return [LineString]
120
+ # @raise [Cassandra::Errors::DecodingError] upon failure.
121
+ def self.deserialize(data)
122
+ buffer = Cassandra::Protocol::CqlByteBuffer.new(data)
123
+ little_endian = buffer.read(1) != "\x00"
124
+
125
+ # Depending on the endian-ness of the data, we want to read it differently. Wrap the buffer
126
+ # with an "endian-aware" reader that reads the desired way.
127
+ buffer = Dse::Util::EndianBuffer.new(buffer, little_endian)
128
+
129
+ type = buffer.read_unsigned
130
+ raise Cassandra::Errors::DecodingError, "LineString data-type value should be 2, but was #{type}" if type != 2
131
+
132
+ deserialize_raw(buffer)
133
+ end
134
+
135
+ # This is a helper function to deserialize the meat of the data (after we've accounted for endianness
136
+ # and other metadata)
137
+ # @private
138
+ def self.deserialize_raw(buffer)
139
+ # Now comes the number of points in the line-string.
140
+ num_points = buffer.read_unsigned
141
+
142
+ # Read that many x,y coords from the buffer.
143
+ points = []
144
+ num_points.times do
145
+ points << Point.deserialize_raw(buffer)
146
+ end
147
+ LineString.new(*points)
148
+ end
149
+
150
+ # Serialize this domain object into a byte array to send to DSE.
151
+ # @return [String] byte-array representation of this domain object.
152
+ def serialize
153
+ buffer = Cassandra::Protocol::CqlByteBuffer.new
154
+
155
+ # We serialize in little-endian form.
156
+
157
+ buffer << "\x01"
158
+
159
+ # This is a line-string.
160
+ buffer.append([2].pack(Cassandra::Protocol::Formats::INT_FORMAT_LE))
161
+
162
+ # Write out the count of how many points we have.
163
+ serialize_raw(buffer)
164
+
165
+ buffer
166
+ end
167
+
168
+ # This is a helper function to serialize the meat of the data (after we've accounted for endianness
169
+ # and other metadata)
170
+ # @private
171
+ def serialize_raw(buffer)
172
+ buffer.append([@points.size].pack(Cassandra::Protocol::Formats::INT_FORMAT_LE))
173
+
174
+ # Now write out x and y for each point.
175
+ @points.each do |point|
176
+ point.serialize_raw(buffer)
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,179 @@
1
+ # encoding: utf-8
2
+
3
+ #--
4
+ # Copyright (C) 2016 DataStax Inc.
5
+ #
6
+ # This software can be used solely with DataStax Enterprise. Please consult the license at
7
+ # http://www.datastax.com/terms/datastax-dse-driver-license-terms
8
+ #++
9
+
10
+ module Dse
11
+ module Geometry
12
+ # Encapsulates a 2D point with x,y coordinates. It corresponds to the `org.apache.cassandra.db.marshal.PointType`
13
+ # column type in DSE.
14
+ #
15
+ # @see https://en.wikipedia.org/wiki/Well-known_text Wikipedia article on Well Known Text
16
+ class Point
17
+ include Cassandra::CustomData
18
+
19
+ # @return [Float] the x coordinate of the point.
20
+ attr_reader :x
21
+ # @return [Float] the y coordinate of the point.
22
+ attr_reader :y
23
+
24
+ # @private
25
+ WKT_RE = /^POINT\s*\(\s*([^)]+?)\s*\)$/
26
+ # @private
27
+ POINT_SPEC_RE = /^([0-9\-\.]+)\s+([0-9\-\.]+)$/
28
+ # @private
29
+ EOL_RE = /[\r\n]/
30
+
31
+ # @param args [Array<Numeric>,Array<String>] varargs-style arguments in two forms:
32
+ # <ul><li>two-element numeric array representing x,y coordinates.</li>
33
+ # <li>one-element string array with the wkt representation.</li></ul>
34
+ #
35
+ # @example Construct a Point with numeric arguments.
36
+ # point = Point.new(3, 4)
37
+ # @example Construct a Point with a wkt string.
38
+ # point = Point.new('POINT (3.0 4.0)')
39
+ def initialize(*args)
40
+ # The constructor has two forms:
41
+ # 1. two numeric args (x,y)
42
+ # 2. one String arg as the wkt representation.
43
+
44
+ case args.size
45
+ when 2
46
+ x, y = args
47
+ Cassandra::Util.assert_instance_of(::Numeric, x)
48
+ Cassandra::Util.assert_instance_of(::Numeric, y)
49
+ Cassandra::Util.assert(!x.nan?, 'x cannot be Float::NAN') if x.is_a?(Float)
50
+ Cassandra::Util.assert(!y.nan?, 'y cannot be Float::NAN') if y.is_a?(Float)
51
+ @x = x.to_f
52
+ @y = y.to_f
53
+ when 1
54
+ wkt = args.first
55
+ Cassandra::Util.assert_instance_of(String, wkt)
56
+ # subsitute eol chars in the string with a space.
57
+ wkt.gsub!(EOL_RE, ' ')
58
+ match = wkt.match(WKT_RE)
59
+ raise ArgumentError, "#{wkt.inspect} is not a valid WKT representation of a point" unless match
60
+ @x, @y = self.class.parse_wkt_internal(match[1])
61
+ else
62
+ raise ArgumentError,
63
+ 'wrong number of arguments: use one string argument (wkt) or two numeric arguments (x,y)'
64
+ end
65
+ end
66
+
67
+ # @return [String] well-known-text representation of this point.
68
+ def wkt
69
+ "POINT (#{wkt_internal})"
70
+ end
71
+
72
+ # @return [String] a human-readable English string describing this {Point}.
73
+ def to_s
74
+ "#{@x},#{@y}"
75
+ end
76
+
77
+ # @private
78
+ def wkt_internal
79
+ # This is a helper used to embed point coords into some container (e.g. line-string, polygon)
80
+ "#{@x} #{@y}"
81
+ end
82
+
83
+ # @private
84
+ def self.parse_wkt_internal(point_str)
85
+ point_str.rstrip!
86
+ match = point_str.match(POINT_SPEC_RE)
87
+ raise ArgumentError, "#{point_str.inspect} is not a valid WKT representation of a point" unless match
88
+ [match[1].to_f, match[2].to_f]
89
+ end
90
+
91
+ # @private
92
+ def eql?(other)
93
+ other.is_a?(Point) && \
94
+ @x == other.x && \
95
+ @y == other.y
96
+ end
97
+ alias == eql?
98
+
99
+ # @private
100
+ def hash
101
+ @hash ||= begin
102
+ h = 17
103
+ h = 31 * h + @x.hash
104
+ h = 31 * h + @y.hash
105
+ h
106
+ end
107
+ end
108
+
109
+ # @private
110
+ def inspect
111
+ "#<Point:0x#{object_id.to_s(16)} " \
112
+ "@x=#{@x.inspect}, " \
113
+ "@y=#{@y.inspect}>"
114
+ end
115
+
116
+ # methods related to serializing/deserializing.
117
+
118
+ # @private
119
+ TYPE = Cassandra::Types::Custom.new('org.apache.cassandra.db.marshal.PointType')
120
+
121
+ # @return [Cassandra::Types::Custom] type of column that is processed by this domain object class.
122
+ def self.type
123
+ TYPE
124
+ end
125
+
126
+ # Deserialize the given data into an instance of this domain object class.
127
+ # @param data [String] byte-array representation of a column value of this custom type.
128
+ # @return [Point]
129
+ # @raise [Cassandra::Errors::DecodingError] upon failure.
130
+ def self.deserialize(data)
131
+ buffer = Cassandra::Protocol::CqlByteBuffer.new(data)
132
+ little_endian = buffer.read(1) != "\x00"
133
+
134
+ # Depending on the endian-ness of the data, we want to read it differently. Wrap the buffer
135
+ # with an "endian-aware" reader that reads the desired way.
136
+ buffer = Dse::Util::EndianBuffer.new(buffer, little_endian)
137
+
138
+ type = buffer.read_unsigned
139
+ raise Cassandra::Errors::DecodingError, "Point data-type value should be 1, but was #{type}" if type != 1
140
+ deserialize_raw(buffer)
141
+ end
142
+
143
+ # This is a helper function to deserialize the meat of the data (after we've accounted for endianness
144
+ # and other metadata)
145
+ # @private
146
+ def self.deserialize_raw(buffer)
147
+ x = buffer.read_double
148
+ y = buffer.read_double
149
+ Point.new(x, y)
150
+ end
151
+
152
+ # Serialize this domain object into a byte array to send to DSE.
153
+ # @return [String] byte-array representation of this domain object.
154
+ def serialize
155
+ buffer = Cassandra::Protocol::CqlByteBuffer.new
156
+
157
+ # Serialize little-endian.
158
+
159
+ buffer << "\x01"
160
+
161
+ # This is a point.
162
+ buffer.append([1].pack(Cassandra::Protocol::Formats::INT_FORMAT_LE))
163
+
164
+ # Write out x and y.
165
+ serialize_raw(buffer)
166
+
167
+ buffer
168
+ end
169
+
170
+ # This is a helper function to serialize the meat of the data (after we've accounted for endianness
171
+ # and other metadata)
172
+ # @private
173
+ def serialize_raw(buffer)
174
+ buffer.append([@x].pack(Cassandra::Protocol::Formats::DOUBLE_FORMAT_LE))
175
+ buffer.append([@y].pack(Cassandra::Protocol::Formats::DOUBLE_FORMAT_LE))
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,196 @@
1
+ # encoding: utf-8
2
+
3
+ #--
4
+ # Copyright (C) 2016 DataStax Inc.
5
+ #
6
+ # This software can be used solely with DataStax Enterprise. Please consult the license at
7
+ # http://www.datastax.com/terms/datastax-dse-driver-license-terms
8
+ #++
9
+
10
+ module Dse
11
+ module Geometry
12
+ # Encapsulates a polygon consisting of a set of linear-rings in the xy-plane. It corresponds to the
13
+ # `org.apache.cassandra.db.marshal.PolygonType` column type in DSE.
14
+ #
15
+ # A linear-ring is a {LineString} whose last point is the same as its first point. The first ring specified
16
+ # in a polygon defines the outer edges of the polygon and is called the _exterior ring_. A polygon may also have
17
+ # _holes_ within it, specified by other linear-rings, and those holes may contain linear-rings indicating
18
+ # _islands_. All such rings are called _interior rings_.
19
+ #
20
+ # @see https://en.wikipedia.org/wiki/Well-known_text Wikipedia article on Well Known Text
21
+ class Polygon
22
+ include Cassandra::CustomData
23
+
24
+ # @private
25
+ WKT_RE = /^POLYGON\s*\(\s*(.+?)\s*\)$/
26
+ # @private
27
+ LINESTRING_SEPARATOR_RE = /\),?/
28
+ # @private
29
+ EOL_RE = /[\r\n]/
30
+
31
+ # @param args [Array<LineString>,Array<String>] varargs-style arguments in two forms:
32
+ # <ul><li>ordered collection of linear-rings that make up this polygon. Can be empty.</li>
33
+ # <li>one-element string array with the wkt representation.</li></ul>
34
+ #
35
+ # @example Construct an empty Polygon
36
+ # polygon = Polygon.new
37
+ # @example Construct a Polygon with LineString objects.
38
+ # exterior_ring = LineString.new(Point.new(0, 0), Point.new(10, 0), Point.new(10, 10), Point.new(0, 0))
39
+ # interior_ring = LineString.new(Point.new(1, 1), Point.new(1, 5), Point.new(5, 1), Point.new(1, 1))
40
+ # polygon = Polygon.new(exterior_ring, interior_ring)
41
+ # @example Construct a line-string with a wkt string.
42
+ # polygon = Polygon.new('POLYGON ((0.0 0.0, 10.0 0.0, 10.0 10.0, 0.0 0.0), ' \
43
+ # '(1.0 1.0, 1.0 5.0, 5.0 1.0, 1.0 1.0))')
44
+ def initialize(*args)
45
+ # The constructor has two forms:
46
+ # 1. 0 or more LineString objects where the first is the exterior ring and the rest are interior rings.
47
+ # 2. one String arg as the wkt representation.
48
+
49
+ if args.size == 1 && args.first.is_a?(String)
50
+ # subsitute eol chars in the string with a space.
51
+ wkt = args.first.gsub(EOL_RE, ' ')
52
+ # Consolidate whitespace before/after commas and parens.
53
+ wkt.gsub!(/\s*([,\(\)])\s*/, '\1')
54
+ if wkt == 'POLYGON EMPTY'
55
+ @rings = [].freeze
56
+ else
57
+ match = wkt.match(WKT_RE)
58
+ raise ArgumentError, "#{wkt.inspect} is not a valid WKT representation of a polygon" unless match
59
+ @rings = parse_wkt_internal(match[1])
60
+ end
61
+ else
62
+ @rings = args.freeze
63
+ @rings.each do |ring|
64
+ Cassandra::Util.assert_instance_of(LineString, ring, "#{ring.inspect} is not a LineString")
65
+ end
66
+ end
67
+ end
68
+
69
+ # @private
70
+ def parse_wkt_internal(poly_str)
71
+ # poly_str is everything inside the outer parens. Example:
72
+ # original: POLYGON ( (1 2, 3 4, 5 6, 1 2), (9 8, 7 6, 5 4, 9 8) )
73
+ # poly_str: (1 2, 3 4, 5 6, 1 2), (9 8, 7 6, 5 4, 9 8)
74
+ line_string_strings = poly_str.split(LINESTRING_SEPARATOR_RE)
75
+ line_strings = []
76
+ line_string_strings.each do |ls|
77
+ # ls still has a leading open paren and possibly spaces.
78
+ ls.sub!(/\s*\(\s*/, '')
79
+ line_strings << LineString.new(*LineString.parse_wkt_internal(ls))
80
+ end
81
+ line_strings.freeze
82
+ end
83
+
84
+ # @return [LineString] linear-ring characterizing the exterior of the polygon. `nil` for empty polygon.
85
+ def exterior_ring
86
+ @rings.first
87
+ end
88
+
89
+ # @return [Array<LineString>] ordered collection of linear-rings that make up the interior of this polygon.
90
+ # Empty if there are no interior rings.
91
+ def interior_rings
92
+ @interior_rings ||= (@rings[1..-1] || []).freeze
93
+ end
94
+
95
+ # @return [String] well-known-text representation of this polygon.
96
+ def wkt
97
+ return 'POLYGON EMPTY' if @rings.empty?
98
+
99
+ result = 'POLYGON ('
100
+ first = true
101
+ @rings.each do |ring|
102
+ result += ', ' unless first
103
+ first = false
104
+ result += "(#{ring.wkt_internal})"
105
+ end
106
+ result += ')'
107
+ result
108
+ end
109
+
110
+ # @return [String] a human-readable English string describing this {Polygon}.
111
+ def to_s
112
+ "Exterior ring: #{@rings.first}\n" \
113
+ "Interior rings:\n " +
114
+ interior_rings.join("\n ")
115
+ end
116
+
117
+ # @private
118
+ def eql?(other)
119
+ other.is_a?(Polygon) && \
120
+ @rings == other.instance_variable_get(:@rings)
121
+ end
122
+ alias == eql?
123
+
124
+ # @private
125
+ def hash
126
+ @hash ||= 31 * 17 + @rings.hash
127
+ end
128
+
129
+ # @private
130
+ def inspect
131
+ "#<Polygon:0x#{object_id.to_s(16)} " \
132
+ "@exterior_ring=#{@rings.first.inspect}, " \
133
+ "@interior_rings=#{interior_rings.inspect}>"
134
+ end
135
+
136
+ # methods related to serializing/deserializing.
137
+
138
+ # @private
139
+ TYPE = Cassandra::Types::Custom.new('org.apache.cassandra.db.marshal.PolygonType')
140
+
141
+ # @return [Cassandra::Types::Custom] type of column that is processed by this domain object class.
142
+ def self.type
143
+ TYPE
144
+ end
145
+
146
+ # Deserialize the given data into an instance of this domain object class.
147
+ # @param data [String] byte-array representation of a column value of this custom type.
148
+ # @return [Polygon]
149
+ # @raise [Cassandra::Errors::DecodingError] upon failure.
150
+ def self.deserialize(data)
151
+ buffer = Cassandra::Protocol::CqlByteBuffer.new(data)
152
+ little_endian = buffer.read(1) != "\x00"
153
+
154
+ # Depending on the endian-ness of the data, we want to read it differently. Wrap the buffer
155
+ # with an "endian-aware" reader that reads the desired way.
156
+ buffer = Dse::Util::EndianBuffer.new(buffer, little_endian)
157
+
158
+ type = buffer.read_unsigned
159
+ raise Cassandra::Errors::DecodingError, "LineString data-type value should be 3, but was #{type}" if type != 3
160
+
161
+ # Now comes the number of rings.
162
+ num_rings = buffer.read_unsigned
163
+
164
+ # Read that many line-string's (rings) from the buffer.
165
+ rings = []
166
+ num_rings.times do
167
+ rings << LineString.deserialize_raw(buffer)
168
+ end
169
+ Polygon.new(*rings)
170
+ end
171
+
172
+ # Serialize this domain object into a byte array to send to DSE.
173
+ # @return [String] byte-array representation of this domain object.
174
+ def serialize
175
+ buffer = Cassandra::Protocol::CqlByteBuffer.new
176
+
177
+ # We serialize in little-endian form.
178
+
179
+ buffer << "\x01"
180
+
181
+ # This is a polygon.
182
+ buffer.append([3].pack(Cassandra::Protocol::Formats::INT_FORMAT_LE))
183
+
184
+ # Write out the count of how many rings we have.
185
+ buffer.append([@rings.size].pack(Cassandra::Protocol::Formats::INT_FORMAT_LE))
186
+
187
+ # Now write out the raw serialization of each ring (e.g. linestring).
188
+ @rings.each do |ring|
189
+ ring.serialize_raw(buffer)
190
+ end
191
+
192
+ buffer
193
+ end
194
+ end
195
+ end
196
+ end