Browse Source

Add Address model to fill an address when it cannot be found in the geojson

feature/region-slug-gone
Bèr Kessels 5 months ago
parent
commit
3ad53663ce

+ 8
- 4
app/aggregates/place.rb View File

@@ -1,6 +1,7 @@
# frozen_string_literal: true

require_relative '../events/place_added.rb'
require_relative './place/address.rb'
require_relative './place/place_id.rb'
require_relative './place/region.rb'

@@ -22,7 +23,7 @@ module Hours
PlaceId.new(payload).id)
end

attr_writer :place_id_builder, :region_builder
attr_writer :address_builder, :place_id_builder, :region_builder
attr_reader :place_id, :region_slug

# These apply methods are the hook that this aggregate uses to update
@@ -53,7 +54,10 @@ module Hours
properties: properties.merge(
place_id: place_id,
region_slug: region_slug,
'addr:city': city
'addr:city': address_builder.city,
'addr:postcode': address_builder.postcode,
'addr:housenumber': address_builder.number,
'addr:street': address_builder.street
)
)
end
@@ -70,8 +74,8 @@ module Hours
@region_builder || Region.new(@payload)
end

def city
properties[:'addr:city'] || region_builder.city
def address_builder
@address_builder || Address.new(@payload)
end
end
end

+ 74
- 0
app/aggregates/place/address.rb View File

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

require 'georuby'

module Hours
module Aggregates
# Generates (finds) an address for a lat:lon
class Address
ADDRESS_TABLE = :query_addresses
DISTANCE_MARGIN = 20

def initialize(geojson)
@geojson = Hashie.symbolize_keys(geojson)
end

def number
properties[:'addr:housenumber'] || address[:number]
end
alias housenumber number

def street
properties[:'addr:street'] || address[:street]
end

def city
properties[:'addr:city'] || address[:city]
end

def postcode
properties[:'addr:postcode'] || address[:postcode]
end

private

def properties
@geojson.fetch(:properties, {})
end

def dataset
Hours.address_database[ADDRESS_TABLE]
end

def address
return @address if @address

# Fallback to setting the memoized @address to empty hash to avoid
# re-looking up a nonexisting address
@address = dataset.select_append(sql_distance)
.order(:distance)
.first(sql_within) || {}
end

def point
@point ||= GeoRuby::SimpleFeatures::Point.from_xy(
*@geojson.fetch(:geometry, {}).fetch(:coordinates, [0, 0])
)
end

def sql_distance
Sequel.lit(
'ST_Distance(location::geography, ?::geography) as distance',
point
)
end

def sql_within
Sequel.lit(
'ST_DWithin(location::geography, ?::geography, ?)',
point, DISTANCE_MARGIN
)
end
end
end
end

+ 58
- 0
test/aggregates/place/address_test.rb View File

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

require 'test_helper'
require_relative Hours.base_path.join 'app/aggregates/place/address.rb'

##
# Tests the Address Builder/Finder.
#
# A unit-test that does go via the Database. This is by design, since
# this unit is self-contained and using its own database/table.
# When that database/table is part of the production database, that is
# "accidental", it might live somewhere on its own entirely. This address
# is a fully self-contained unit.
describe Hours::Aggregates::Address do
subject { Hours::Aggregates::Address.new(geojson) }

describe 'with empty address' do
let(:geojson) { json_fixtures('input/addressless.json') }

it '#street is set from database' do
assert_equal 'Stationsplein', subject.street
end

it '#number aliased housenumber is set from database' do
assert_equal '6P', subject.number
assert_equal '6P', subject.housenumber
end

it '#postcode is set from database' do
assert_equal '6512AB', subject.postcode
end

it '#city is set from database' do
assert_equal 'Nijmegen', subject.city
end
end

describe 'with preset address' do
let(:geojson) { json_fixtures('input/hm_burchtstraat.json') }

it '#street is taken from geojson' do
assert_equal 'Burchtstraat', subject.street
end

it '#number aliased housenumber is taken from geojson' do
assert_equal '1', subject.number
assert_equal '1', subject.housenumber
end

it '#postcode is taken from geojson' do
assert_equal '6511RA', subject.postcode
end

it '#city is taken from geojson' do
assert_equal 'Nijmegen', subject.city
end
end
end

+ 17
- 15
test/aggregates/place_test.rb View File

@@ -11,6 +11,7 @@ describe Hours::Aggregates::Place do
let(:region_slug) { 'region' }
let(:city) { 'Region' }
let(:region_builder) { Minitest::Mock.new }
let(:address_builder) { Minitest::Mock.new }
subject { Hours::Aggregates::Place.new(aggregate_id, events) }

before do
@@ -18,8 +19,13 @@ describe Hours::Aggregates::Place do
subject.place_id_builder = place_id_builder

region_builder.expect(:region_slug, region_slug)
region_builder.expect(:city, city)
subject.region_builder = region_builder

address_builder.expect(:city, 'City')
address_builder.expect(:postcode, '')
address_builder.expect(:number, '')
address_builder.expect(:street, '')
subject.address_builder = address_builder
end

# This is implemented in EventSourcery::AggregateRoot, but it allows
@@ -43,20 +49,16 @@ describe Hours::Aggregates::Place do
assert_mock region_builder
end

describe 'when city is empty' do
it 'sets the addr:city from region' do
place = subject.add(payload)
assert_equal 'Region', place.changes.first.body['properties']['addr:city']
assert_mock region_builder
end
end

describe 'when city is not empty' do
let(:payload) { { properties: { 'addr:city' => 'City' } } }

it 'leaves the add:city alone' do
place = subject.add(payload)
assert_equal 'City', place.changes.first.body['properties']['addr:city']
describe 'address' do
# Test with :city. other attributes are the same
describe 'when addr:* is empty' do
it 'sets the addr:city from address_builder' do
place = subject.add(payload)
# The capitalized versions are what is mocked
assert_equal 'City',
place.changes.first.body['properties']['addr:city']
assert_mock address_builder
end
end
end
end

+ 8
- 4
test/fixtures/input/addressless.json View File

@@ -1,11 +1,15 @@
{
"type": "Feature",
"geometry": {
"id": "2303136888",
"geometry": {
"type": "Point",
"coordinates": [5.8653234, 51.8473397]
"coordinates": [
5.8531038,
51.8429894
]
},
"properties": {
"name": "Sint-Nicolaaskapel",
"opening_hours": "24/7; PH closed"
"amenity": "fast_food",
"name": "Smullers"
}
}

+ 9
- 9
test/fixtures/input/hm_arnhem.json View File

@@ -9,19 +9,19 @@
"type" : "Feature",
"id" : "2725646258",
"properties" : {
"addr:postcode" : "6811ET",
"addr:city" : "Arnhem",
"name" : "H&M",
"opening_hours" : "Mo 11:00-18:00; Tu-We 10:00-18:00; Th 10:00-21:00; Fr 10:00-18:00; Sa 09:30-18:00",
"shop" : "clothes",
"source:date" : "2014-02-11",
"brand:wikipedia" : "en:H&M",
"shop:number" : "NL0076",
"brand" : "H&M",
"addr:postcode" : "6811ET",
"addr:city" : "Arnhem",
"addr:housenumber" : "26",
"brand:wikidata" : "Q188326",
"source" : "BAG;http://www.hm.com/nl/",
"addr:street" : "Vijzelstraat",
"addr:country" : "NL",
"name" : "H&M"
"source" : "BAG;http://www.hm.com/nl/",
"brand" : "H&M",
"source:date" : "2014-02-11",
"brand:wikipedia" : "en:H&M",
"brand:wikidata" : "Q188326",
"shop:number" : "NL0076"
}
}

+ 33
- 0
test/integration/api/add_places_test.rb View File

@@ -75,6 +75,39 @@ describe 'add place' do
end
end

describe 'without an address' do
let(:payload) { json_fixtures('input/addressless.json') }

before do
header('Accept', 'application/json')
%i[street housenumber postcode city country].each do |attr|
full_attr = :"addr:#{attr.to_s}"
assert_nil payload[:properties][full_attr]
end
end

it 'adds an address from the geometry point' do
post_json '/places', payload
assert_status(201)
projector_process_event(id_from_header)

get "/places/#{id_from_header}"
expected = {
data: {
attributes: {
address: {
postcode: '6512AB',
city: 'Nijmegen',
housenumber: '6P',
street: 'Stationsplein'
}
}.ignore_extra_keys!
}.ignore_extra_keys!
}
assert_json_match expected, last_response.body
end
end

describe 'with a missing geometry' do
it 'returns bad request for missting missing geometry' do
payload_missing_geom = payload.slice(:type, :id, :properties)

Loading…
Cancel
Save