parse-stack-next 4.5.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 (178) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +2 -0
  3. data/.env.sample +112 -0
  4. data/.env.test +10 -0
  5. data/.github/workflows/ruby.yml +36 -0
  6. data/.gitignore +49 -0
  7. data/.ruby-version +1 -0
  8. data/.solargraph.yml +22 -0
  9. data/CHANGELOG.md +5816 -0
  10. data/Gemfile +30 -0
  11. data/Gemfile.lock +175 -0
  12. data/LICENSE.txt +23 -0
  13. data/Makefile +63 -0
  14. data/README.md +5655 -0
  15. data/Rakefile +573 -0
  16. data/bin/console +38 -0
  17. data/bin/parse-console +136 -0
  18. data/bin/server +17 -0
  19. data/bin/setup +7 -0
  20. data/config/parse-config.json +12 -0
  21. data/docs/TEST_SERVER.md +271 -0
  22. data/docs/_config.yml +1 -0
  23. data/docs/mcp_guide.md +3484 -0
  24. data/docs/mongodb_direct_guide.md +1348 -0
  25. data/docs/mongodb_index_optimization_guide.md +631 -0
  26. data/examples/transaction_example.rb +219 -0
  27. data/lib/parse/acl_scope.rb +728 -0
  28. data/lib/parse/agent/cancellation_token.rb +80 -0
  29. data/lib/parse/agent/constraint_translator.rb +480 -0
  30. data/lib/parse/agent/describe.rb +420 -0
  31. data/lib/parse/agent/errors.rb +133 -0
  32. data/lib/parse/agent/mcp_client.rb +557 -0
  33. data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
  34. data/lib/parse/agent/mcp_rack_app.rb +1143 -0
  35. data/lib/parse/agent/mcp_server.rb +376 -0
  36. data/lib/parse/agent/metadata_audit.rb +259 -0
  37. data/lib/parse/agent/metadata_dsl.rb +733 -0
  38. data/lib/parse/agent/metadata_registry.rb +794 -0
  39. data/lib/parse/agent/pipeline_validator.rb +82 -0
  40. data/lib/parse/agent/prompts.rb +351 -0
  41. data/lib/parse/agent/rate_limiter.rb +158 -0
  42. data/lib/parse/agent/relation_graph.rb +162 -0
  43. data/lib/parse/agent/result_formatter.rb +453 -0
  44. data/lib/parse/agent/tools.rb +5489 -0
  45. data/lib/parse/agent.rb +3249 -0
  46. data/lib/parse/api/aggregate.rb +79 -0
  47. data/lib/parse/api/all.rb +26 -0
  48. data/lib/parse/api/analytics.rb +18 -0
  49. data/lib/parse/api/batch.rb +33 -0
  50. data/lib/parse/api/cloud_functions.rb +58 -0
  51. data/lib/parse/api/config.rb +125 -0
  52. data/lib/parse/api/files.rb +29 -0
  53. data/lib/parse/api/hooks.rb +117 -0
  54. data/lib/parse/api/objects.rb +146 -0
  55. data/lib/parse/api/path_segment.rb +75 -0
  56. data/lib/parse/api/push.rb +20 -0
  57. data/lib/parse/api/schema.rb +49 -0
  58. data/lib/parse/api/server.rb +50 -0
  59. data/lib/parse/api/sessions.rb +24 -0
  60. data/lib/parse/api/users.rb +250 -0
  61. data/lib/parse/atlas_search/index_manager.rb +353 -0
  62. data/lib/parse/atlas_search/result.rb +204 -0
  63. data/lib/parse/atlas_search/search_builder.rb +604 -0
  64. data/lib/parse/atlas_search/session.rb +253 -0
  65. data/lib/parse/atlas_search.rb +995 -0
  66. data/lib/parse/client/authentication.rb +97 -0
  67. data/lib/parse/client/batch.rb +234 -0
  68. data/lib/parse/client/body_builder.rb +240 -0
  69. data/lib/parse/client/caching.rb +203 -0
  70. data/lib/parse/client/logging.rb +293 -0
  71. data/lib/parse/client/profiling.rb +181 -0
  72. data/lib/parse/client/protocol.rb +91 -0
  73. data/lib/parse/client/request.rb +233 -0
  74. data/lib/parse/client/response.rb +208 -0
  75. data/lib/parse/client.rb +1104 -0
  76. data/lib/parse/clp_scope.rb +361 -0
  77. data/lib/parse/live_query/circuit_breaker.rb +256 -0
  78. data/lib/parse/live_query/client.rb +1001 -0
  79. data/lib/parse/live_query/configuration.rb +224 -0
  80. data/lib/parse/live_query/event.rb +115 -0
  81. data/lib/parse/live_query/event_queue.rb +272 -0
  82. data/lib/parse/live_query/health_monitor.rb +214 -0
  83. data/lib/parse/live_query/logging.rb +149 -0
  84. data/lib/parse/live_query/subscription.rb +294 -0
  85. data/lib/parse/live_query.rb +163 -0
  86. data/lib/parse/lookup_rewriter.rb +445 -0
  87. data/lib/parse/model/acl.rb +968 -0
  88. data/lib/parse/model/associations/belongs_to.rb +275 -0
  89. data/lib/parse/model/associations/collection_proxy.rb +435 -0
  90. data/lib/parse/model/associations/has_many.rb +597 -0
  91. data/lib/parse/model/associations/has_one.rb +158 -0
  92. data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
  93. data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
  94. data/lib/parse/model/bytes.rb +62 -0
  95. data/lib/parse/model/classes/audience.rb +262 -0
  96. data/lib/parse/model/classes/installation.rb +363 -0
  97. data/lib/parse/model/classes/job_schedule.rb +153 -0
  98. data/lib/parse/model/classes/job_status.rb +264 -0
  99. data/lib/parse/model/classes/product.rb +75 -0
  100. data/lib/parse/model/classes/push_status.rb +263 -0
  101. data/lib/parse/model/classes/role.rb +751 -0
  102. data/lib/parse/model/classes/session.rb +201 -0
  103. data/lib/parse/model/classes/user.rb +943 -0
  104. data/lib/parse/model/clp.rb +544 -0
  105. data/lib/parse/model/core/actions.rb +1268 -0
  106. data/lib/parse/model/core/builder.rb +139 -0
  107. data/lib/parse/model/core/create_lock.rb +386 -0
  108. data/lib/parse/model/core/describe.rb +382 -0
  109. data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
  110. data/lib/parse/model/core/errors.rb +38 -0
  111. data/lib/parse/model/core/fetching.rb +566 -0
  112. data/lib/parse/model/core/field_guards.rb +220 -0
  113. data/lib/parse/model/core/indexing.rb +382 -0
  114. data/lib/parse/model/core/parse_reference.rb +407 -0
  115. data/lib/parse/model/core/properties.rb +809 -0
  116. data/lib/parse/model/core/querying.rb +491 -0
  117. data/lib/parse/model/core/schema.rb +202 -0
  118. data/lib/parse/model/core/search_indexing.rb +174 -0
  119. data/lib/parse/model/date.rb +88 -0
  120. data/lib/parse/model/email.rb +213 -0
  121. data/lib/parse/model/file.rb +527 -0
  122. data/lib/parse/model/geojson.rb +271 -0
  123. data/lib/parse/model/geopoint.rb +261 -0
  124. data/lib/parse/model/model.rb +260 -0
  125. data/lib/parse/model/object.rb +2068 -0
  126. data/lib/parse/model/phone.rb +520 -0
  127. data/lib/parse/model/pointer.rb +443 -0
  128. data/lib/parse/model/polygon.rb +406 -0
  129. data/lib/parse/model/push.rb +975 -0
  130. data/lib/parse/model/shortnames.rb +8 -0
  131. data/lib/parse/model/time_zone.rb +141 -0
  132. data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
  133. data/lib/parse/model/validations.rb +96 -0
  134. data/lib/parse/mongodb.rb +2300 -0
  135. data/lib/parse/pipeline_security.rb +554 -0
  136. data/lib/parse/query/constraint.rb +198 -0
  137. data/lib/parse/query/constraints.rb +3279 -0
  138. data/lib/parse/query/cursor.rb +434 -0
  139. data/lib/parse/query/n_plus_one_detector.rb +445 -0
  140. data/lib/parse/query/operation.rb +104 -0
  141. data/lib/parse/query/ordering.rb +66 -0
  142. data/lib/parse/query.rb +7028 -0
  143. data/lib/parse/schema/index_migrator.rb +291 -0
  144. data/lib/parse/schema/search_index_migrator.rb +289 -0
  145. data/lib/parse/schema.rb +494 -0
  146. data/lib/parse/stack/generators/rails.rb +40 -0
  147. data/lib/parse/stack/generators/templates/model.erb +51 -0
  148. data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
  149. data/lib/parse/stack/generators/templates/model_role.rb +4 -0
  150. data/lib/parse/stack/generators/templates/model_session.rb +4 -0
  151. data/lib/parse/stack/generators/templates/model_user.rb +11 -0
  152. data/lib/parse/stack/generators/templates/parse.rb +12 -0
  153. data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
  154. data/lib/parse/stack/railtie.rb +18 -0
  155. data/lib/parse/stack/tasks.rb +563 -0
  156. data/lib/parse/stack/version.rb +11 -0
  157. data/lib/parse/stack.rb +455 -0
  158. data/lib/parse/two_factor_auth/user_extension.rb +449 -0
  159. data/lib/parse/two_factor_auth.rb +310 -0
  160. data/lib/parse/webhooks/payload.rb +360 -0
  161. data/lib/parse/webhooks/registration.rb +199 -0
  162. data/lib/parse/webhooks/replay_protection.rb +189 -0
  163. data/lib/parse/webhooks.rb +510 -0
  164. data/lib/parse-stack-next.rb +5 -0
  165. data/lib/parse-stack.rb +5 -0
  166. data/parse-stack-next.gemspec +82 -0
  167. data/parse-stack.png +0 -0
  168. data/scripts/debug-ips.js +35 -0
  169. data/scripts/docker/Dockerfile.parse +13 -0
  170. data/scripts/docker/atlas-init.js +284 -0
  171. data/scripts/docker/docker-compose.atlas.yml +76 -0
  172. data/scripts/docker/docker-compose.test.yml +106 -0
  173. data/scripts/docker/mongo-init.js +21 -0
  174. data/scripts/eval_mcp_with_lm_studio.rb +274 -0
  175. data/scripts/start-parse.sh +90 -0
  176. data/scripts/start_mcp_server.rb +78 -0
  177. data/scripts/test_server_connection.rb +82 -0
  178. metadata +377 -0
@@ -0,0 +1,271 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "model"
5
+ require_relative "geopoint"
6
+
7
+ module Parse
8
+ # GeoJSON-native geometry wrappers for types that Parse Server's schema
9
+ # does NOT model directly but that MongoDB's `2dsphere` index supports
10
+ # natively. These classes are designed for callers that go through the
11
+ # mongo-direct surface (`Parse::MongoDB`) or Atlas Search, where stored
12
+ # geometry can be richer than the `GeoPoint` / `Polygon` types Parse
13
+ # Server exposes.
14
+ #
15
+ # **Axis order.** Unlike {Parse::GeoPoint} and {Parse::Polygon}, which
16
+ # store coordinates in Parse-native `[latitude, longitude]` order to
17
+ # match the REST wire format, every class under `Parse::GeoJSON` stores
18
+ # coordinates in GeoJSON-native `[longitude, latitude]` order. The
19
+ # namespace itself is the axis-order signal — pick the namespace based
20
+ # on which side of the boundary you're working on.
21
+ #
22
+ # **Storage.** These geometries live in `:object` Parse columns. Parse
23
+ # Server treats the value as an opaque hash on read and write; MongoDB
24
+ # will happily index it on a `2dsphere` index regardless of whether
25
+ # Parse Server's schema knows the type exists.
26
+ module GeoJSON
27
+ # Base class for GeoJSON geometry wrappers. Subclasses define `TYPE`
28
+ # and `#valid_coordinates?` and inherit the round-trip plumbing.
29
+ class Geometry < Parse::Model
30
+ # @return [Array] the raw coordinates array, in GeoJSON nesting and
31
+ # `[longitude, latitude]` axis order. Shape varies by subclass.
32
+ attr_reader :coordinates
33
+
34
+ # @return [String] the GeoJSON `type` discriminator (`"Point"`,
35
+ # `"LineString"`, `"Polygon"`, `"MultiPolygon"`, etc.).
36
+ def self.geojson_type
37
+ const_get(:TYPE)
38
+ end
39
+
40
+ def geojson_type
41
+ self.class.geojson_type
42
+ end
43
+
44
+ # The initializer accepts either a GeoJSON Hash (`{type:, coordinates:}`),
45
+ # a plain coordinates Array, or another instance of the same class.
46
+ def initialize(value = nil)
47
+ @coordinates = []
48
+ self.coordinates = value unless value.nil?
49
+ end
50
+
51
+ def coordinates=(value)
52
+ coords =
53
+ case value
54
+ when self.class
55
+ deep_copy_array(value.coordinates)
56
+ when Hash
57
+ hash = value.respond_to?(:symbolize_keys) ? value.symbolize_keys : value
58
+ type = hash[:type] || hash["type"]
59
+ if type && type.to_s != self.class.geojson_type
60
+ raise ArgumentError, "[#{self.class}] expected GeoJSON type " \
61
+ "#{self.class.geojson_type.inspect}, got #{type.inspect}."
62
+ end
63
+ normalize(hash[:coordinates] || hash["coordinates"] || [])
64
+ when Array
65
+ normalize(value)
66
+ else
67
+ raise ArgumentError, "[#{self.class}] cannot build from #{value.class}: " \
68
+ "expected GeoJSON Hash, coordinates Array, or another #{self.class}."
69
+ end
70
+ @coordinates = coords
71
+ validate!
72
+ @coordinates
73
+ end
74
+
75
+ # @return [Hash] the standard GeoJSON `{type:, coordinates:}` hash.
76
+ def to_geojson
77
+ { "type" => geojson_type, "coordinates" => deep_copy_array(@coordinates) }
78
+ end
79
+ alias_method :as_json, :to_geojson
80
+
81
+ # @return [String] the JSON form, suitable for direct shipment to
82
+ # any GeoJSON-aware consumer.
83
+ def to_json(*args)
84
+ to_geojson.to_json(*args)
85
+ end
86
+
87
+ def ==(other)
88
+ return false unless other.is_a?(self.class)
89
+ @coordinates == other.coordinates
90
+ end
91
+
92
+ def inspect
93
+ "#<#{self.class.name} #{@coordinates.inspect}>"
94
+ end
95
+
96
+ # @!visibility private
97
+ def initialize_copy(other)
98
+ super
99
+ @coordinates = deep_copy_array(other.coordinates)
100
+ end
101
+
102
+ # Build any GeoJSON geometry from its wire-format Hash. Dispatches to
103
+ # the matching subclass based on the `type` field.
104
+ # @example
105
+ # Parse::GeoJSON::Geometry.from_geojson(line_string_hash) # => Parse::GeoJSON::LineString
106
+ # @return [Parse::GeoJSON::Geometry]
107
+ def self.from_geojson(hash)
108
+ raise ArgumentError, "[Parse::GeoJSON::Geometry] expected a Hash." unless hash.is_a?(Hash)
109
+ h = hash.respond_to?(:symbolize_keys) ? hash.symbolize_keys : hash
110
+ type = (h[:type] || h["type"]).to_s
111
+ klass = TYPE_REGISTRY[type]
112
+ raise ArgumentError, "[Parse::GeoJSON::Geometry] unsupported GeoJSON type #{type.inspect}." if klass.nil?
113
+ klass.new(h)
114
+ end
115
+
116
+ private
117
+
118
+ def deep_copy_array(arr)
119
+ arr.map { |entry| entry.is_a?(Array) ? deep_copy_array(entry) : entry }
120
+ end
121
+
122
+ def normalize(_value)
123
+ raise NotImplementedError, "subclass must implement #normalize"
124
+ end
125
+
126
+ def validate!
127
+ # subclasses may override
128
+ end
129
+ end
130
+
131
+ # `LineString` — an ordered sequence of `[longitude, latitude]` points
132
+ # describing a path. The most common applications are GPS tracks,
133
+ # delivery routes, road segments, and trail centerlines.
134
+ #
135
+ # GeoJSON requires ≥ 2 points; this class warns when the constraint
136
+ # is violated rather than raising, matching {Parse::GeoPoint} /
137
+ # {Parse::Polygon} validation style.
138
+ #
139
+ # @example
140
+ # Parse::GeoJSON::LineString.new [[-122.4, 37.7], [-122.39, 37.78]]
141
+ class LineString < Geometry
142
+ TYPE = "LineString"
143
+ MIN_POINTS = 2
144
+
145
+ # @return [Array<Parse::GeoPoint>] the path as Parse::GeoPoint objects
146
+ # (axis-swapped back to Parse's `[lat, lng]`).
147
+ def geo_points
148
+ @coordinates.map { |(lng, lat)| Parse::GeoPoint.new(lat, lng) }
149
+ end
150
+
151
+ private
152
+
153
+ def normalize(value)
154
+ raise ArgumentError, "[Parse::GeoJSON::LineString] coordinates must be an Array." unless value.is_a?(Array)
155
+ value.map do |pair|
156
+ case pair
157
+ when Parse::GeoPoint
158
+ finite_lnglat!(pair.longitude.to_f, pair.latitude.to_f)
159
+ when Array
160
+ unless pair.length == 2 && pair[0].is_a?(Numeric) && pair[1].is_a?(Numeric)
161
+ raise ArgumentError, "[Parse::GeoJSON::LineString] each coordinate must be a [lng, lat] numeric pair."
162
+ end
163
+ finite_lnglat!(pair[0].to_f, pair[1].to_f)
164
+ else
165
+ raise ArgumentError, "[Parse::GeoJSON::LineString] unsupported coordinate entry #{pair.inspect}."
166
+ end
167
+ end
168
+ end
169
+
170
+ # Reject NaN / Infinity at the door. `Float::NAN.is_a?(Numeric)`
171
+ # is true so the earlier type check is insufficient; persisting
172
+ # a line with NaN errors mid-pipeline at `2dsphere` index rebuild
173
+ # time.
174
+ def finite_lnglat!(lng, lat)
175
+ unless lng.finite? && lat.finite?
176
+ raise ArgumentError, "[Parse::GeoJSON::LineString] coordinates must be finite numerics; got [#{lng}, #{lat}]."
177
+ end
178
+ [lng, lat]
179
+ end
180
+
181
+ def validate!
182
+ return if @coordinates.empty?
183
+ if @coordinates.length < MIN_POINTS
184
+ warn "[Parse::GeoJSON::LineString] requires at least #{MIN_POINTS} points; got #{@coordinates.length}."
185
+ end
186
+ end
187
+ end
188
+
189
+ # `MultiPolygon` — an Array of Polygons, each Polygon an Array of
190
+ # linear rings, each ring an Array of `[lng, lat]` pairs. The canonical
191
+ # use case is administrative or territorial regions made up of
192
+ # disjoint pieces (Hawaii, Indonesia, multi-island service areas,
193
+ # postal-code clusters).
194
+ #
195
+ # GeoJSON nesting depth is 4: `coordinates[polygon][ring][point][lng_or_lat]`.
196
+ # Each ring must contain ≥ 4 points and be explicitly closed.
197
+ #
198
+ # @example
199
+ # Parse::GeoJSON::MultiPolygon.new [
200
+ # [[[ 0, 0], [ 1, 0], [ 1, 1], [ 0, 1], [ 0, 0]]],
201
+ # [[[ 5, 5], [ 6, 5], [ 6, 6], [ 5, 6], [ 5, 5]]],
202
+ # ]
203
+ class MultiPolygon < Geometry
204
+ TYPE = "MultiPolygon"
205
+ MIN_RING_POINTS = 4
206
+
207
+ # @return [Array<Parse::Polygon>] each member polygon as a
208
+ # {Parse::Polygon} (with axis swap back to Parse's `[lat, lng]`).
209
+ # Inner rings (holes) are dropped because {Parse::Polygon} does
210
+ # not support them.
211
+ def polygons
212
+ @coordinates.map do |rings|
213
+ outer = rings.first
214
+ Parse::Polygon.new(outer.map { |(lng, lat)| [lat.to_f, lng.to_f] })
215
+ end
216
+ end
217
+
218
+ private
219
+
220
+ def normalize(value)
221
+ raise ArgumentError, "[Parse::GeoJSON::MultiPolygon] coordinates must be an Array." unless value.is_a?(Array)
222
+ value.map do |polygon|
223
+ unless polygon.is_a?(Array)
224
+ raise ArgumentError, "[Parse::GeoJSON::MultiPolygon] each polygon must be an Array of rings."
225
+ end
226
+ polygon.map do |ring|
227
+ unless ring.is_a?(Array)
228
+ raise ArgumentError, "[Parse::GeoJSON::MultiPolygon] each ring must be an Array of [lng, lat] pairs."
229
+ end
230
+ ring.map do |pair|
231
+ unless pair.is_a?(Array) && pair.length == 2 &&
232
+ pair[0].is_a?(Numeric) && pair[1].is_a?(Numeric)
233
+ raise ArgumentError, "[Parse::GeoJSON::MultiPolygon] each coordinate must be a [lng, lat] numeric pair."
234
+ end
235
+ # Reject NaN / Infinity; see LineString#finite_lnglat!
236
+ # for the same rationale.
237
+ lng = pair[0].to_f
238
+ lat = pair[1].to_f
239
+ unless lng.finite? && lat.finite?
240
+ raise ArgumentError, "[Parse::GeoJSON::MultiPolygon] coordinates must be finite " \
241
+ "numerics; got [#{lng}, #{lat}]."
242
+ end
243
+ [lng, lat]
244
+ end
245
+ end
246
+ end
247
+ end
248
+
249
+ def validate!
250
+ @coordinates.each_with_index do |polygon, i|
251
+ polygon.each_with_index do |ring, j|
252
+ next if ring.empty?
253
+ if ring.length < MIN_RING_POINTS
254
+ warn "[Parse::GeoJSON::MultiPolygon] polygon[#{i}].ring[#{j}] has #{ring.length} points; " \
255
+ "GeoJSON requires at least #{MIN_RING_POINTS}."
256
+ elsif ring.first != ring.last
257
+ warn "[Parse::GeoJSON::MultiPolygon] polygon[#{i}].ring[#{j}] is not closed (first != last)."
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
263
+
264
+ # Dispatch table for {Geometry.from_geojson}. Kept here at the end so
265
+ # subclasses are registered after their constants are defined.
266
+ Geometry::TYPE_REGISTRY = {
267
+ "LineString" => LineString,
268
+ "MultiPolygon" => MultiPolygon,
269
+ }.freeze
270
+ end
271
+ end
@@ -0,0 +1,261 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "model"
5
+
6
+ module Parse
7
+
8
+ # This class manages the GeoPoint data type that Parse provides to support
9
+ # geo-queries. To define a GeoPoint property, use the `:geopoint` data type.
10
+ # Please note that latitudes should not be between -90.0 and 90.0, and
11
+ # longitudes should be between -180.0 and 180.0.
12
+ # @example
13
+ # class PlaceObject < Parse::Object
14
+ # property :location, :geopoint
15
+ # end
16
+ #
17
+ # san_diego = Parse::GeoPoint.new(32.8233, -117.6542)
18
+ # los_angeles = Parse::GeoPoint.new [34.0192341, -118.970792]
19
+ # san_diego == los_angeles # false
20
+ #
21
+ # place = PlaceObject.new
22
+ # place.location = san_diego
23
+ # place.save
24
+ #
25
+ class GeoPoint < Model
26
+ # The default attributes in a Parse GeoPoint hash.
27
+ ATTRIBUTES = { __type: :string, latitude: :float, longitude: :float }.freeze
28
+
29
+ # @return [Float] latitude value between -90.0 and 90.0
30
+ attr_reader :latitude
31
+ # @return [Float] longitude value between -180.0 and 180.0
32
+ attr_reader :longitude
33
+ # The key field for latitude
34
+ FIELD_LAT = "latitude".freeze
35
+ # The key field for longitude
36
+ FIELD_LNG = "longitude".freeze
37
+
38
+ # The minimum latitude value.
39
+ LAT_MIN = -90.0
40
+ # The maximum latitude value.
41
+ LAT_MAX = 90.0
42
+ # The minimum longitude value.
43
+ LNG_MIN = -180.0
44
+ # The maximum longitude value.
45
+ LNG_MAX = 180.0
46
+
47
+ alias_method :lat, :latitude
48
+ alias_method :lng, :longitude
49
+ # @return [Model::TYPE_GEOPOINT]
50
+ def self.parse_class; TYPE_GEOPOINT; end
51
+ # @return [Model::TYPE_GEOPOINT]
52
+ def parse_class; self.class.parse_class; end
53
+
54
+ alias_method :__type, :parse_class
55
+
56
+ # The initializer can create a GeoPoint with a hash, array or values.
57
+ # @example
58
+ # san_diego = Parse::GeoPoint.new(32.8233, -117.6542)
59
+ # san_diego = Parse::GeoPoint.new [32.8233, -117.6542]
60
+ # san_diego = Parse::GeoPoint.new { latitude: 32.8233, longitude: -117.6542}
61
+ #
62
+ # @param latitude [Numeric] The latitude value between LAT_MIN and LAT_MAX.
63
+ # @param longitude [Numeric] The longitude value between LNG_MIN and LNG_MAX.
64
+ def initialize(latitude = nil, longitude = nil)
65
+ @latitude = @longitude = 0.0
66
+ if latitude.is_a?(Hash) || latitude.is_a?(Array)
67
+ self.attributes = latitude
68
+ elsif latitude.is_a?(Numeric) && longitude.is_a?(Numeric)
69
+ @latitude = latitude
70
+ @longitude = longitude
71
+ elsif latitude.is_a?(GeoPoint)
72
+ @latitude = latitude.latitude
73
+ @longitude = latitude.longitude
74
+ end
75
+
76
+ _validate_point
77
+ end
78
+
79
+ # @!visibility private
80
+ def _validate_point
81
+ unless @latitude.nil? || @latitude.between?(LAT_MIN, LAT_MAX)
82
+ warn "[Parse::GeoPoint] Latitude (#{@latitude}) is not between #{LAT_MIN}, #{LAT_MAX}!"
83
+ warn "Attempting to use GeoPoint’s with latitudes outside these ranges will raise an exception in a future release."
84
+ end
85
+
86
+ unless @longitude.nil? || @longitude.between?(LNG_MIN, LNG_MAX)
87
+ warn "[Parse::GeoPoint] Longitude (#{@longitude}) is not between #{LNG_MIN}, #{LNG_MAX}!"
88
+ warn "Attempting to use GeoPoint’s with longitude outside these ranges will raise an exception in a future release."
89
+ end
90
+ end
91
+
92
+ # @return [Hash] attributes for a Parse GeoPoint.
93
+ def attributes
94
+ ATTRIBUTES
95
+ end
96
+
97
+ # Helper method for performing geo-queries with radial miles constraints
98
+ # @return [Array] containing [lat,lng,miles]
99
+ def max_miles(m)
100
+ m = 0 if m.nil?
101
+ [@latitude, @longitude, m]
102
+ end
103
+
104
+ # Helper method for performing geo-queries with a radial kilometer
105
+ # constraint. Used with `:field.near => gp.max_kilometers(N)` to compile
106
+ # a `$nearSphere` + `$maxDistanceInKilometers` query against Parse Server.
107
+ # @return [Array] containing `[lat, lng, kilometers, :km]`
108
+ def max_kilometers(km)
109
+ km = 0 if km.nil?
110
+ [@latitude, @longitude, km, :km]
111
+ end
112
+ alias_method :max_km, :max_kilometers
113
+
114
+ # Helper method for performing geo-queries with a radial radians
115
+ # constraint. Used with `:field.near => gp.max_radians(R)` to compile
116
+ # a `$nearSphere` + `$maxDistance` query against Parse Server (raw
117
+ # `$maxDistance` is measured in radians). Convert from miles/km by
118
+ # dividing by mean-Earth-radius (~3958.8 miles or ~6371 km).
119
+ # @return [Array] containing `[lat, lng, radians, :radians]`
120
+ def max_radians(rad)
121
+ rad = 0 if rad.nil?
122
+ [@latitude, @longitude, rad, :radians]
123
+ end
124
+
125
+ def latitude=(l)
126
+ @latitude = l
127
+ _validate_point
128
+ end
129
+
130
+ def longitude=(l)
131
+ @longitude = l
132
+ _validate_point
133
+ end
134
+
135
+ # Setting lat and lng for an GeoPoint can be done using a hash with the attributes set
136
+ # or with an array of two items where the first is the lat and the second is the lng (ex. [32.22,-118.81])
137
+ def attributes=(h)
138
+ if h.is_a?(Hash)
139
+ h = h.symbolize_keys
140
+ @latitude = h[:latitude].to_f || h[:lat].to_f || @latitude
141
+ @longitude = h[:longitude].to_f || h[:lng].to_f || @longitude
142
+ elsif h.is_a?(Array) && h.count == 2
143
+ @latitude = h.first.to_f
144
+ @longitude = h.last.to_f
145
+ end
146
+ _validate_point
147
+ end
148
+
149
+ # @return [Boolean] true if two geopoints are equal based on lat and lng.
150
+ def ==(g)
151
+ return false unless g.is_a?(GeoPoint)
152
+ @latitude == g.latitude && @longitude == g.longitude
153
+ end
154
+
155
+ # Helper method for reducing the precision of a geopoint.
156
+ # @param precision [Integer] The number of floating digits to keep.
157
+ # @return [GeoPoint] Reduces the precision of a geopoint.
158
+ def estimated(precision = 2)
159
+ Parse::GeoPoint.new(@latitude.to_f.round(precision), @longitude.round(precision))
160
+ end
161
+
162
+ # Returns a tuple containing latitude and longitude
163
+ # @return [Array]
164
+ def to_a
165
+ [@latitude, @longitude]
166
+ end
167
+
168
+ # GeoJSON (RFC 7946) representation of this point. GeoJSON requires
169
+ # `[longitude, latitude]` axis order — the inverse of Parse's wire
170
+ # format — so this method performs the swap. Useful when handing the
171
+ # value to Leaflet/Mapbox/PostGIS, or when constructing literals for
172
+ # MongoDB-direct geo queries (which use GeoJSON internally).
173
+ # @example
174
+ # geopoint.to_geojson
175
+ # # => {"type" => "Point", "coordinates" => [-117.6542, 32.8233]}
176
+ # @return [Hash] a GeoJSON `Point` geometry object.
177
+ def to_geojson
178
+ { "type" => "Point", "coordinates" => [@longitude, @latitude] }
179
+ end
180
+
181
+ # Build a {Parse::GeoPoint} from a GeoJSON `Point` geometry object.
182
+ # Accepts either symbol or string keys and the standard
183
+ # `[longitude, latitude]` axis order; performs the swap to Parse's
184
+ # `[latitude, longitude]` internal storage.
185
+ # @example
186
+ # Parse::GeoPoint.from_geojson("type" => "Point", "coordinates" => [-117.6542, 32.8233])
187
+ # @param geojson [Hash] a GeoJSON Point geometry object.
188
+ # @return [Parse::GeoPoint]
189
+ # @raise [ArgumentError] if the input is not a valid GeoJSON Point.
190
+ def self.from_geojson(geojson)
191
+ raise ArgumentError, "[Parse::GeoPoint] from_geojson expects a Hash." unless geojson.is_a?(Hash)
192
+ hash = geojson.respond_to?(:symbolize_keys) ? geojson.symbolize_keys : geojson
193
+ type = hash[:type] || hash["type"]
194
+ coords = hash[:coordinates] || hash["coordinates"]
195
+ # Mirror Parse::Polygon.from_geojson: require both coordinates to
196
+ # be finite Numerics. Without this check, non-numeric entries
197
+ # (`"evil"`, `{"$where": "..."}`, `nil`) silently coerce to 0.0
198
+ # via `.to_f`, producing a null-island point that matches
199
+ # ACL-relevant proximity queries unintentionally. NaN / Infinity
200
+ # similarly produce silent geo bugs and 2dsphere index errors.
201
+ unless type.to_s == "Point" && coords.is_a?(Array) && coords.length == 2 &&
202
+ coords[0].is_a?(Numeric) && coords[1].is_a?(Numeric) &&
203
+ coords[0].finite? && coords[1].finite?
204
+ raise ArgumentError, "[Parse::GeoPoint] from_geojson expects a GeoJSON Point with " \
205
+ "two finite numeric coordinates."
206
+ end
207
+ Parse::GeoPoint.new(coords[1].to_f, coords[0].to_f)
208
+ end
209
+
210
+ # @!visibility private
211
+ def inspect
212
+ "#<GeoPoint [#{@latitude},#{@longitude}]>"
213
+ end
214
+
215
+ # Calculate the distance in miles to another GeoPoint using Haversine.
216
+ # You may also call this method with a latitude and longitude.
217
+ # @example
218
+ # point.distance_in_miles(geotpoint)
219
+ # point.distance_in_miles(lat, lng)
220
+ #
221
+ # @param geopoint [GeoPoint]
222
+ # @param lng [Float] Longitude assuming that the first parameter
223
+ # is longitude instead of a GeoPoint.
224
+ # @return [Float] number of miles between geopoints.
225
+ # @see #distance_in_km
226
+ def distance_in_miles(geopoint, lng = nil)
227
+ distance_in_km(geopoint, lng) * 0.621371
228
+ end
229
+
230
+ # Calculate the distance in kilometers to another GeoPoint using Haversine
231
+ # method. You may also call this method with a latitude and longitude.
232
+ # @example
233
+ # point.distance_in_km(geotpoint)
234
+ # point.distance_in_km(lat, lng)
235
+ #
236
+ # @param geopoint [GeoPoint]
237
+ # @param lng [Float] Longitude assuming that the first parameter is a latitude instead of a GeoPoint.
238
+ # @return [Float] number of miles between geopoints.
239
+ # @see #distance_in_miles
240
+ def distance_in_km(geopoint, lng = nil)
241
+ unless geopoint.is_a?(Parse::GeoPoint)
242
+ geopoint = Parse::GeoPoint.new(geopoint, lng)
243
+ end
244
+
245
+ dtor = Math::PI / 180
246
+ r = 6378.14
247
+ r_lat1 = self.latitude * dtor
248
+ r_lng1 = self.longitude * dtor
249
+ r_lat2 = geopoint.latitude * dtor
250
+ r_lng2 = geopoint.longitude * dtor
251
+
252
+ delta_lat = r_lat1 - r_lat2
253
+ delta_lng = r_lng1 - r_lng2
254
+
255
+ a = (Math::sin(delta_lat / 2.0) ** 2).to_f + (Math::cos(r_lat1) * Math::cos(r_lat2) * (Math::sin(delta_lng / 2.0) ** 2))
256
+ c = 2.0 * Math::atan2(Math::sqrt(a), Math::sqrt(1.0 - a))
257
+ d = r * c
258
+ d
259
+ end
260
+ end
261
+ end