Browse Source

Implement basic json serialisation for events with lat-lon pairs

feature/geojson
Bèr Kessels 5 months ago
parent
commit
37eafadc76

+ 3
- 3
Gemfile View File

@@ -6,11 +6,14 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }

gem 'event_sourcery'
gem 'event_sourcery-postgres'
gem 'json_expressions'
gem 'jsonapi-rb'
gem 'nominatim', github: 'lukaszsliwa/nominatim'
gem 'opening_hours_converter'
gem 'rake'
gem 'ruby_kml'
gem 'semver'
gem 'sequel-postgis-georuby'
gem 'sinatra'

group :development, :test do
@@ -29,6 +32,3 @@ group :development, :test do

gem 'foreman'
end

# Added at 2019-06-05 18:00:08 +0200 by ber:
gem "jsonapi-rb", "~> 0.5.0"

+ 8
- 1
Gemfile.lock View File

@@ -30,8 +30,10 @@ GEM
multipart-post (>= 1.2, < 3)
foreman (0.85.0)
thor (~> 0.19.1)
georuby (2.5.2)
jaro_winkler (1.5.2)
json (2.1.0)
json_expressions (0.9.0)
jsonapi-deserializable (0.2.0)
jsonapi-rb (0.5.0)
jsonapi-deserializable (~> 0.2.0)
@@ -74,6 +76,9 @@ GEM
nokogiri
semver (1.0.1)
sequel (5.21.0)
sequel-postgis-georuby (0.1.2)
georuby (~> 2.5.2)
sequel (~> 5.0)
sinatra (2.0.5)
mustermann (~> 1.0)
rack (~> 2.0)
@@ -95,7 +100,8 @@ DEPENDENCIES
event_sourcery
event_sourcery-postgres
foreman
jsonapi-rb (~> 0.5.0)
json_expressions
jsonapi-rb
minitest
nokogiri
nominatim!
@@ -105,6 +111,7 @@ DEPENDENCIES
rubocop
ruby_kml
semver
sequel-postgis-georuby
sinatra

BUNDLED WITH

+ 1
- 1
Rakefile View File

@@ -1,6 +1,6 @@
$LOAD_PATH.unshift '.'

require "dotenv/load"
require 'dotenv/load'

task :unset_db_name do
@db_name = ENV['DB_NAME']

+ 18
- 0
app/models/node.rb View File

@@ -0,0 +1,18 @@
module Hours
module Models
##
# A Node is a readonly model for querying the projection of "places".
# * Readonly: it is not enforced on model or ORM level, but should be
# enforced in the database-server and user setup. RO only means that
# this model is only tested and optimized against reading.
class Node < Sequel::Model(::Hours.projections_database[:query_nodes])
def lat
location.lat
end

def lon
location.lon
end
end
end
end

+ 5
- 12
app/projections/nodes.rb View File

@@ -12,13 +12,8 @@ module Hours

table :query_nodes do
column :node_id, 'UUID NOT NULL'
column :lat, BigDecimal, size: [10, 7] # TODO: move to postgis?
column :lon, BigDecimal, size: [10, 7]
column :name, :text
column :author_email, :text
column :contact_details, :text
column :amount, Integer
column :proposed_at, DateTime
column :location, 'geography(POINT)'
end

# Event handlers that update the projection in response to different
@@ -27,13 +22,11 @@ module Hours
project NodeAdded do |event|
table.insert(
node_id: event.aggregate_id,
lat: event.body['lat'],
lon: event.body['lon'],
name: event.body['name'],
author_email: event.body['author_email'],
contact_details: event.body['contact_details'],
amount: event.body['amount'].to_i,
proposed_at: Time.now
location: GeoRuby::SimpleFeatures::Point.from_x_y(
event.body['lon'],
event.body['lat']
)
)
end
end

+ 1
- 4
app/projections/query.rb View File

@@ -4,10 +4,7 @@ module Hours
# Query handler that queries the projection table.
class Query
def self.handle
Hours.projections_database[:query_nodes]
.select(:node_id, :lat, :lon, :author_email, :contact_details)
.order(Sequel.desc(:name))
.all
Hours::Models::Node.all
end
end
end

+ 84
- 0
app/serializers/node.rb View File

@@ -0,0 +1,84 @@
module Hours
module Serializers
##
# Represent a Node as JSON
class Node < JSONAPI::Serializable::Resource
type 'node'

attributes :name, :lat, :lon
id { @object.node_id }

attribute :raw_opening_hours do
'Mo-We 10:00-18:00; Th 10:00-21:00; Fr 10:00-18:00; Sa 09:30-17:30; '\
'PH closed; Su 12:00-17:00 open "Koopzondag"'
end

attribute :week_stable do
false
end

attribute :open_this_week do
[
{
from: '1989-11-06T09:00:00.000Z',
to: '1989-11-06T17:00:00.000Z',
unknown: false
},
{
to: '1989-11-07T17:00:00.000Z',
unknown: false,
from: '1989-11-07T09:00:00.000Z'
},
{
unknown: false,
to: '1989-11-08T17:00:00.000Z',
from: '1989-11-08T09:00:00.000Z'
},
{
from: '1989-11-09T09:00:00.000Z',
to: '1989-11-09T20:00:00.000Z',
unknown: false
},
{
to: '1989-11-10T17:00:00.000Z',
unknown: false,
from: '1989-11-10T09:00:00.000Z'
},
{
to: '1989-11-11T16:30:00.000Z',
unknown: false,
from: '1989-11-11T08:30:00.000Z'
},
{
comment: 'Koopzondag',
to: '1989-11-12T16:00:00.000Z',
unknown: false,
from: '1989-11-12T11:00:00.000Z'
}
]
end

attribute :status do
true
end

attribute :address do
{
postcode: '6511RA',
city: 'Nijmegen',
housenumber: '1',
street: 'Burchtstraat'
}
end

link :self do
# TODO: move into an URL-helper
"/nodes/#{@object.node_id}"
end

meta do
{ copyright: 'OpenStreetMap-contributors' }
end
end
end
end

+ 1
- 1
config.ru View File

@@ -1,5 +1,5 @@
$LOAD_PATH << '.'

require 'lib/hours.rb'
require 'lib/app.rb'

run Sinatra::Application

+ 2
- 0
config/environment.rb View File

@@ -21,4 +21,6 @@ EventSourcery::Postgres.configure do |config|
# databases. For the purposes of this example we'll use one.
config.event_store_database = database
config.projections_database = database

config.projections_database.extension :postgis_georuby
end

BIN
images/open_openingstijden.png View File


+ 103
- 0
images/open_openingstijden.svg View File

@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->

<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
width="494.239px"
height="494.238px"
viewBox="0 0 494.239 494.238"
style="enable-background:new 0 0 494.239 494.238;"
xml:space="preserve"
sodipodi:docname="open_openingstijden.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
inkscape:export-filename="/home/ber/Documenten/PBX_placebazaar/hours/images/open_openingstijden.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90"><metadata
id="metadata8713"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs8711" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1850"
inkscape:window-height="1016"
id="namedview8709"
showgrid="false"
inkscape:zoom="1.6874461"
inkscape:cx="257.78551"
inkscape:cy="247.11901"
inkscape:window-x="70"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="g8676" />
<g
id="g8676">
<path
d="M 258.27253,28.491798 V 60.363265 H 103.87927 V 433.40762 l 154.39326,0.0832 v 32.25539 l 131.89611,-33.22506 0.1911,-370.57338 z m 30.68065,203.989772 c 6.21239,0 11.24901,6.55389 11.24901,14.63743 0,8.08532 -5.03573,14.63743 -11.24901,14.63743 -6.21328,0 -11.24901,-6.55211 -11.24901,-14.63743 -8.9e-4,-8.08354 5.03662,-14.63743 11.24901,-14.63743 z M 133.95921,403.35245 V 90.443208 H 258.27253 V 403.40996 Z"
id="path8674"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccssscscccsc"
style="fill:#2c3e50;fill-opacity:1;stroke-width:0.88470417" />
</g>
<g
id="g8678">
</g>
<g
id="g8680">
</g>
<g
id="g8682">
</g>
<g
id="g8684">
</g>
<g
id="g8686">
</g>
<g
id="g8688">
</g>
<g
id="g8690">
</g>
<g
id="g8692">
</g>
<g
id="g8694">
</g>
<g
id="g8696">
</g>
<g
id="g8698">
</g>
<g
id="g8700">
</g>
<g
id="g8702">
</g>
<g
id="g8704">
</g>
<g
id="g8706">
</g>
</svg>

+ 1
- 1
lib/app.rb View File

@@ -43,7 +43,7 @@ get '/nodes' do
render = JSONAPI::Serializable::Renderer.new
body render.render(
Hours::Projections::Nodes::Query.handle,
class: { "Hours::Models::Node": Hours::Serializers::Node }
class: { "Hours::Models::Node": Hours::Serializers::Node }
).to_json
status 200
end

+ 1
- 0
lib/hours.rb View File

@@ -2,6 +2,7 @@ require 'dotenv/load'

require 'event_sourcery'
require 'event_sourcery/postgres'
require 'sequel-postgis-georuby'
require 'securerandom'

require_relative '../app/events/node_added.rb'

+ 2
- 1
test/fixtures/db/full.json View File

@@ -7,5 +7,6 @@
"addr_city": "Nijmegen",
"addr_country_code": "nl",
"opening_hours": "Mo-We 10:00-18:00; Th 10:00-21:00; Fr 10:00-18:00; Sa 09:30-17:30; PH closed; Su 12:00-17:00 open \"Koopzondag\"",
"geometry": { "type": "Point", "coordinates": [51.8473397, 5.8653234] }
"lat": 51.8473397,
"lon": 5.8653234
}

+ 67
- 0
test/fixtures/json/nodes.json View File

@@ -0,0 +1,67 @@
{
"data": [
{
"links" : {
"self" : "/places/"
},
"meta" : {
"copyright" : "OpenStreetMap-contributors"
},
"type" : "node",
"attributes" : {
"raw_opening_hours" : "Mo-We 10:00-18:00; Th 10:00-21:00; Fr 10:00-18:00; Sa 09:30-17:30; PH closed; Su 12:00-17:00 open \"Koopzondag\"",
"week_stable" : false,
"open_this_week" : [
{
"from" : "1989-11-06T09:00:00.000Z",
"to" : "1989-11-06T17:00:00.000Z",
"unknown" : false
},
{
"to" : "1989-11-07T17:00:00.000Z",
"unknown" : false,
"from" : "1989-11-07T09:00:00.000Z"
},
{
"unknown" : false,
"to" : "1989-11-08T17:00:00.000Z",
"from" : "1989-11-08T09:00:00.000Z"
},
{
"from" : "1989-11-09T09:00:00.000Z",
"to" : "1989-11-09T20:00:00.000Z",
"unknown" : false
},
{
"to" : "1989-11-10T17:00:00.000Z",
"unknown" : false,
"from" : "1989-11-10T09:00:00.000Z"
},
{
"to" : "1989-11-11T16:30:00.000Z",
"unknown" : false,
"from" : "1989-11-11T08:30:00.000Z"
},
{
"comment" : "Koopzondag",
"to" : "1989-11-12T16:00:00.000Z",
"unknown" : false,
"from" : "1989-11-12T11:00:00.000Z"
}
],
"name" : "H&M",
"status" : true,
"lat": 51.8473397,
"lon": 5.8653234,
"address" : {
"postcode" : "6511RA",
"city" : "Nijmegen",
"housenumber" : "1",
"street" : "Burchtstraat"
}
},
"id" : "1337"
}
]
}


+ 10
- 5
test/integration/view_proposed_nodes_test.rb View File

@@ -1,4 +1,5 @@
require 'test_helper'
require 'json_expressions/minitest'

describe 'pending nodes' do
describe 'GET /nodes' do
@@ -10,8 +11,8 @@ describe 'pending nodes' do
[
NodeAdded.new(
aggregate_id: hm_id,
body: body
),
body: body
)
]
end
let(:projector) { Hours::Projections::Nodes::Projector.new }
@@ -24,11 +25,15 @@ describe 'pending nodes' do
end

get '/nodes'

assert_equal(200, last_response.status)
expected = [json_fixtures('json/hm.json')]

expected = json_fixtures('json/nodes.json')
expected[:data][0][:links][:self] = String
expected[:data][0][:id] = String

parsed = JSON.parse(last_response.body, symbolize_names: true)
assert_equal(expected, parsed)

assert_json_match(expected, parsed)
end
end
end

+ 36
- 0
test/models/node_test.rb View File

@@ -0,0 +1,36 @@
require 'test_helper'

describe Hours::Models::Node do
let(:pk) { SecureRandom.uuid }
subject do
Hours::Models::Node.new
end

describe 'dataset' do
it 'reads from :query_nodes' do
assert_equal :query_nodes, subject.class.table_name
end
end

describe 'location' do
let(:lat) do
20.1
end

let(:lon) do
20.2
end

let(:point) do
GeoRuby::SimpleFeatures::Point.from_x_y(lon, lat)
end

it 'has a lat' do
assert_equal lat, Hours::Models::Node.new(location: point).lat
end

it 'has a lon' do
assert_equal lon, Hours::Models::Node.new(location: point).lon
end
end
end

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

@@ -0,0 +1,35 @@
require 'test_helper'

describe Hours::Projections::Nodes::Query do
subject do
Hours::Projections::Nodes::Query.handle
end

let(:tracker) { Minitest::Mock.new }

let(:projector) do
Hours::Projections::Nodes::Projector.new(tracker: tracker)
end

let(:event) do
NodeAdded.new(
aggregate_id: SecureRandom.uuid,
body: {
lat: 20.01,
lon: 20.02,
author_email: 'ronweasly@example.com',
contact_details: 'The Nest'
}
)
end

before do
# Ensure that we have processed events, which stores nodes in the db
projector.process(event)
end

it 'fetches a list of Nodes through Sequel model' do
assert_kind_of Array, subject
assert_kind_of Hours::Models::Node, subject.first
end
end

+ 2
- 1
test/test_helper.rb View File

@@ -3,6 +3,7 @@ require 'database_cleaner'
require 'byebug'

require 'hours'
require 'app'

require 'awesome_print'
require 'ostruct'
@@ -37,7 +38,7 @@ module Minitest
end

def json_fixtures(file)
JSON.parse(File.read(fixtures(file)))
JSON.parse(File.read(fixtures(file)), symbolize_names: true)
end
end
end

Loading…
Cancel
Save