Browse Source

Move queries into separate units

tags/0.3.6^2
Bèr Kessels 4 months ago
parent
commit
61dd817d0d

+ 33
- 0
app/projections/base_query.rb View File

@@ -0,0 +1,33 @@
# frozen_string_literal: true

require 'pagy'

module Hours
module Projections
##
# Common behaviour for all query objects
class BaseQuery
DEFAULT_DATASET = Hours.projections_database[:query_places]
BUFFERX = 0.005302691970002
BUFFERY = 0.01297116279602
attr_reader :dataset, :page

include Pagy::Backend

def initialize(dataset)
@dataset = dataset
@page = nil
end

def self.build
new(DEFAULT_DATASET)
end

def pagy_get_vars(collection, vars)
vars[:count] = collection.count
vars[:page] = page
vars
end
end
end
end

+ 30
- 0
app/projections/place_query.rb View File

@@ -0,0 +1,30 @@
# frozen_string_literal: true

module Hours
module Projections

# Query handler that queries the projection table for a single node
class PlaceQuery
def self.build
faraday = Faraday.new(url: ENV['BRAGI_URL']) do |conn|
conn.response :raise_error
conn.adapter Faraday.default_adapter
end
new(faraday)
end

def initialize(http_client)
@http_client = http_client
end

def handle(id)
response = @http_client.get("features/#{id}")
@place = Hours::Models::Place.from_geojson_feature(
RGeo::GeoJSON.decode(response.body).first
)
rescue Faraday::ServerError, JSON::ParserError => e
Hours::Models::NullPlace.new(error: e.message)
end
end
end
end

+ 0
- 96
app/projections/query.rb View File

@@ -1,96 +0,0 @@
# frozen_string_literal: true

require 'pagy'
require 'offline_geocoder'

module Hours
module Projections
##
# Common behaviour for all query objects
class BaseQuery
DEFAULT_DATASET = Hours.projections_database[:query_places]
BUFFERX = 0.005302691970002
BUFFERY = 0.01297116279602
attr_reader :dataset, :page

include Pagy::Backend

def initialize(dataset)
@dataset = dataset
@page = nil
end

def self.build
new(DEFAULT_DATASET)
end

def pagy_get_vars(collection, vars)
vars[:count] = collection.count
vars[:page] = page
vars
end
end

# Query handler that queries the projection table for a single node
class PlaceQuery
def self.build
faraday = Faraday.new(url: ENV['BRAGI_URL']) do |conn|
conn.response :raise_error
conn.adapter Faraday.default_adapter
end
new(faraday)
end

def initialize(http_client)
@http_client = http_client
end

def handle(id)
response = @http_client.get("features/#{id}")
@place = Hours::Models::Place.from_geojson_feature(
RGeo::GeoJSON.decode(response.body).first
)
rescue Faraday::ServerError, JSON::ParserError => e
Hours::Models::NullPlace.new(error: e.message)
end
end

# Query handler that builds a list of places in a Region
class RegionQuery < BaseQuery
def handle(slug, page = 1)
@page = page
@region_slug = slug
return Hours::Models::Region.new([], {}, nil, nil) unless region

pagy, records = pagy(dataset.order(distance).where(in_envelope))
Hours::Models::Region.new(records,
pagy,
region[:centerpoint],
region[:name])
end

private

def region
raise ArgumentError if @region_slug.nil? || @region_slug.empty?

@region ||= Hours.projections_database[:query_regions]
.first(slug: @region_slug)
end

def distance
Sequel.lit('location <-> ?', region[:centerpoint])
end

def in_envelope
Sequel.lit('location && ST_MakeEnvelope(?, ?, ?, ?, 4326)',
*envelope_corners)
end

def envelope_corners
cp = region[:centerpoint]
[cp.x + BUFFERX, cp.y + BUFFERY, cp.x - BUFFERX, cp.y - BUFFERY]
end
end
end
end

+ 45
- 0
app/projections/region_query.rb View File

@@ -0,0 +1,45 @@
# frozen_string_literal: true

require 'offline_geocoder'

module Hours
module Projections
# Query handler that builds a list of places in a Region
class RegionQuery < BaseQuery
def handle(slug, page = 1)
@page = page
@region_slug = slug
return Hours::Models::Region.new([], {}, nil, nil) unless region

pagy, records = pagy(dataset.order(distance).where(in_envelope))
Hours::Models::Region.new(records,
pagy,
region[:centerpoint],
region[:name])
end

private

def region
raise ArgumentError if @region_slug.nil? || @region_slug.empty?

@region ||= Hours.projections_database[:query_regions]
.first(slug: @region_slug)
end

def distance
Sequel.lit('location <-> ?', region[:centerpoint])
end

def in_envelope
Sequel.lit('location && ST_MakeEnvelope(?, ?, ?, ?, 4326)',
*envelope_corners)
end

def envelope_corners
cp = region[:centerpoint]
[cp.x + BUFFERX, cp.y + BUFFERY, cp.x - BUFFERX, cp.y - BUFFERY]
end
end
end
end

+ 136
- 0
test/projections/place_query_test.rb View File

@@ -0,0 +1,136 @@
# frozen_string_literal: true

# TODO: split into separate unit files

require 'test_helper'
require 'geo_ruby/geojson'

describe Hours::Projections::PlaceQuery do
let(:body) do
{
type: 'FeatureCollection',
features: [
{ type: 'Feature',
geometry: { type: 'Point', coordinates: [102.0, 0.5] },
properties: {} }
]
}
end

let(:bragi_response) { OpenStruct.new(body: body.to_json, code: 200) }
let(:emtpy_response) { OpenStruct.new(body: '{}', code: 200) }
let(:error_response) { OpenStruct.new(body: '', code: 500) }

let(:place_id) { 'poi:osm:node:989225414' }

let(:http_client) do
Minitest::Mock.new
end

subject do
Hours::Projections::PlaceQuery.new(http_client)
end

it '#handle fetches a single Place' do
http_client.expect(:get, bragi_response, ["features/#{place_id}"])
assert_kind_of Hours::Models::Place, subject.handle(place_id)
assert_mock http_client
end

it 'handles broken body from HTTP service' do
http_client.expect(:get, error_response, [String])
assert_kind_of Hours::Models::NullPlace, subject.handle(place_id)
assert_mock http_client
end

it 'handles HTTP errors from HTTP service' do
error = ->(_args) { raise Faraday::ServerError, '503' }
http_client = OpenStruct.new(get: '')

http_client.stub(:get, error) do
result = Hours::Projections::PlaceQuery.new(http_client).handle('')
assert_equal '503', result.error
end
end
end

describe Hours::Projections::RegionQuery do
before do
skip 'Implement region query from bragi'
end
let(:regions) do
[
json_fixtures('input/nijmegen_city.json'),
json_fixtures('input/arnhem_city.json')
]
end

let(:nijmegen) { geojson_fixtures('input/nijmegen_city.json') }
let(:arnhem) { geojson_fixtures('input/arnhem_city.json') }

subject do
Hours::Projections::RegionQuery.build
end

it '#handle fetches a Region' do
assert_kind_of Hours::Models::Region, subject.handle('nijmegen')
end

it 'pages from :query_places in pages of 20' do
20.times.each do
assert subject.dataset.insert(
id: SecureRandom.uuid,
place_id: SecureRandom.hex,
region_slug: 'TODO-REMOVE',
location: nijmegen.geometry
)
end
assert subject.dataset.insert(
id: SecureRandom.uuid,
place_id: SecureRandom.hex,
region_slug: 'TODO-REMOVE',
location: arnhem.geometry
)

assert_equal 22, subject.dataset.count

result = subject.handle('nijmegen', 1)
assert_equal 20, result.count
assert_kind_of Hours::Models::Place, result.first
end

it 'sorts the results by distance from region center' do
# Counting down to avoid false positive because of insert order
location = nijmegen.geometry.dup
Array.new(3) do
location.x += 0.00001
assert subject.dataset.insert(
id: SecureRandom.uuid,
place_id: SecureRandom.hex,
region_slug: 'TODO-REMOVE',
location: location
)
end
result = subject.handle('nijmegen')
assert_equal nijmegen.geometry, result.centerpoint

places = result.to_a
distance0 = nijmegen.geometry.euclidian_distance(places[0].location)
distance1 = nijmegen.geometry.euclidian_distance(places[1].location)
assert distance1 > distance0
end

describe 'avoid looking up places without regions' do
it '#handle does not allow nil for slug' do
assert_raises ArgumentError do
subject.handle(nil)
end
end

it '#handle does not allow empty string for slug' do
assert_raises ArgumentError do
subject.handle('')
end
end
end
end

+ 0
- 81
test/projections/query_test.rb View File

@@ -53,84 +53,3 @@ describe Hours::Projections::PlaceQuery do
end
end
end

describe Hours::Projections::RegionQuery do
before do
skip 'Implement region query from bragi'
end
let(:regions) do
[
json_fixtures('input/nijmegen_city.json'),
json_fixtures('input/arnhem_city.json')
]
end

let(:nijmegen) { geojson_fixtures('input/nijmegen_city.json') }
let(:arnhem) { geojson_fixtures('input/arnhem_city.json') }

subject do
Hours::Projections::RegionQuery.build
end

it '#handle fetches a Region' do
assert_kind_of Hours::Models::Region, subject.handle('nijmegen')
end

it 'pages from :query_places in pages of 20' do
20.times.each do
assert subject.dataset.insert(
id: SecureRandom.uuid,
place_id: SecureRandom.hex,
region_slug: 'TODO-REMOVE',
location: nijmegen.geometry
)
end
assert subject.dataset.insert(
id: SecureRandom.uuid,
place_id: SecureRandom.hex,
region_slug: 'TODO-REMOVE',
location: arnhem.geometry
)

assert_equal 22, subject.dataset.count

result = subject.handle('nijmegen', 1)
assert_equal 20, result.count
assert_kind_of Hours::Models::Place, result.first
end

it 'sorts the results by distance from region center' do
# Counting down to avoid false positive because of insert order
location = nijmegen.geometry.dup
Array.new(3) do
location.x += 0.00001
assert subject.dataset.insert(
id: SecureRandom.uuid,
place_id: SecureRandom.hex,
region_slug: 'TODO-REMOVE',
location: location
)
end
result = subject.handle('nijmegen')
assert_equal nijmegen.geometry, result.centerpoint

places = result.to_a
distance0 = nijmegen.geometry.euclidian_distance(places[0].location)
distance1 = nijmegen.geometry.euclidian_distance(places[1].location)
assert distance1 > distance0
end

describe 'avoid looking up places without regions' do
it '#handle does not allow nil for slug' do
assert_raises ArgumentError do
subject.handle(nil)
end
end

it '#handle does not allow empty string for slug' do
assert_raises ArgumentError do
subject.handle('')
end
end
end
end

+ 85
- 0
test/projections/region_query_test.rb View File

@@ -0,0 +1,85 @@
# frozen_string_literal: true

require 'test_helper'
require 'geo_ruby/geojson'

describe Hours::Projections::RegionQuery do
before do
skip 'Implement region query from bragi'
end
let(:regions) do
[
json_fixtures('input/nijmegen_city.json'),
json_fixtures('input/arnhem_city.json')
]
end

let(:nijmegen) { geojson_fixtures('input/nijmegen_city.json') }
let(:arnhem) { geojson_fixtures('input/arnhem_city.json') }

subject do
Hours::Projections::RegionQuery.build
end

it '#handle fetches a Region' do
assert_kind_of Hours::Models::Region, subject.handle('nijmegen')
end

it 'pages from :query_places in pages of 20' do
20.times.each do
assert subject.dataset.insert(
id: SecureRandom.uuid,
place_id: SecureRandom.hex,
region_slug: 'TODO-REMOVE',
location: nijmegen.geometry
)
end
assert subject.dataset.insert(
id: SecureRandom.uuid,
place_id: SecureRandom.hex,
region_slug: 'TODO-REMOVE',
location: arnhem.geometry
)

assert_equal 22, subject.dataset.count

result = subject.handle('nijmegen', 1)
assert_equal 20, result.count
assert_kind_of Hours::Models::Place, result.first
end

it 'sorts the results by distance from region center' do
# Counting down to avoid false positive because of insert order
location = nijmegen.geometry.dup
Array.new(3) do
location.x += 0.00001
assert subject.dataset.insert(
id: SecureRandom.uuid,
place_id: SecureRandom.hex,
region_slug: 'TODO-REMOVE',
location: location
)
end
result = subject.handle('nijmegen')
assert_equal nijmegen.geometry, result.centerpoint

places = result.to_a
distance0 = nijmegen.geometry.euclidian_distance(places[0].location)
distance1 = nijmegen.geometry.euclidian_distance(places[1].location)
assert distance1 > distance0
end

describe 'avoid looking up places without regions' do
it '#handle does not allow nil for slug' do
assert_raises ArgumentError do
subject.handle(nil)
end
end

it '#handle does not allow empty string for slug' do
assert_raises ArgumentError do
subject.handle('')
end
end
end
end

Loading…
Cancel
Save