Browse Source

Merge branch 'feature/parse-hours' into develop

* feature/parse-hours:
  Remove week_stable as we don't need that ATM.
  Use parser to parse the Opening Hours String.
  Introduce and use test assertion helper to check for http status
  Refactor iterator object creation into a constant
  Replace hardcoded address with hstore object
  Add a console for manual investigation and debugging
  Remove nodejs server runner
  Autocorrect with rubocop after upgrading.
  Update Ruby and Rubygems.
  Remove kml builder
  Add parser for opening_hours to determine the state
tags/0.3.0^2
Bèr Kessels 9 months ago
parent
commit
740a0bd44a

+ 1
- 0
.ruby-version View File

@@ -0,0 +1 @@
2.5.0

+ 2
- 1
Gemfile View File

@@ -11,7 +11,6 @@ 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'
@@ -23,12 +22,14 @@ group :development, :test do
gem 'minitest'
gem 'nokogiri'
gem 'rack-test'
gem 'timecop'

gem 'rubocop'

gem 'awesome_print'
gem 'better_errors'
gem 'byebug'
gem 'pry'

gem 'foreman'
end

+ 9
- 7
Gemfile.lock View File

@@ -15,7 +15,6 @@ GEM
coderay (>= 1.0.0)
erubi (>= 1.0.0)
rack (>= 0.9.0)
builder (3.2.3)
byebug (10.0.2)
coderay (1.1.2)
database_cleaner (1.7.0)
@@ -41,6 +40,7 @@ GEM
jsonapi-renderer (0.2.0)
jsonapi-serializable (0.3.1)
jsonapi-renderer (~> 0.2.0)
method_source (0.9.2)
mini_portile2 (2.3.0)
minitest (5.11.3)
multi_json (1.13.1)
@@ -51,10 +51,13 @@ GEM
opening_hours_converter (1.7.20)
json
parallel (1.12.1)
parser (2.5.0.4)
parser (2.5.0.5)
ast (~> 2.4.0)
pg (1.1.4)
powerpack (0.1.2)
pry (0.12.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
rack (2.0.6)
rack-protection (2.0.5)
rack
@@ -71,9 +74,6 @@ GEM
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.4.0)
ruby-progressbar (1.10.0)
ruby_kml (0.1.7)
builder
nokogiri
semver (1.0.1)
sequel (5.21.0)
sequel-postgis-georuby (0.1.2)
@@ -86,6 +86,7 @@ GEM
tilt (~> 2.0)
thor (0.19.4)
tilt (2.0.9)
timecop (0.9.1)
unicode-display_width (1.4.1)

PLATFORMS
@@ -106,13 +107,14 @@ DEPENDENCIES
nokogiri
nominatim!
opening_hours_converter
pry
rack-test
rake
rubocop
ruby_kml
semver
sequel-postgis-georuby
sinatra
timecop

BUNDLED WITH
1.16.1
1.17.3

+ 4
- 4
Rakefile View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true

$LOAD_PATH.unshift '.'

require 'dotenv/load'
@@ -22,10 +24,7 @@ task run_processors: :environment do
Hours.projections_database.disconnect

esps = [
Hours::Projections::Nodes::Projector.new,
Hours::Projections::ProposedNodesKml::Projector.new(
tracker: EventSourcery::Memory::Tracker.new
)
Hours::Projections::Nodes::Projector.new
]

# The ESPRunner will fork child processes for each of the ESPs passed to it.
@@ -48,6 +47,7 @@ namespace :db do
pg_db.disconnect
app_db = Sequel.connect(Hours.config.database_url)
app_db.run('CREATE EXTENSION postgis')
app_db.run('CREATE EXTENSION hstore')
rescue Sequel::DatabaseError => e
raise unless e.message.include?(
"database \"#{ENV['DB_NAME']}\" already exists"

+ 2
- 0
app/aggregates/node.rb View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true

module Hours
module Aggregates
##

+ 2
- 0
app/commands/node/add.rb View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true

module Hours
module Commands
module Node

+ 2
- 0
app/events/node_added.rb View File

@@ -1 +1,3 @@
# frozen_string_literal: true

NodeAdded = Class.new(EventSourcery::Event)

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

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

require 'opening_hours_converter'

module Hours
module Models
##
@@ -6,6 +10,9 @@ module Hours
# 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])
ITERATOR = OpeningHoursConverter::Iterator.new
PARSER = OpeningHoursConverter::OpeningHoursParser.new

def lat
location.lat
end
@@ -13,6 +20,44 @@ module Hours
def lon
location.lon
end

def status
ITERATOR.is_opened?(opening_hours)
end

def open_this_week
date_ranges = PARSER.parse(opening_hours)
get_intervals_as_week(date_ranges, Date.today)
end

private

def get_intervals_as_week(date_ranges, date_in_week)
from = beginning_of_week(date_in_week)
to = end_of_week(date_in_week)
# Ensure we always get an array to append to.
as_week = Hash.new { |hash, key| hash[key] = [] }

this_week = ITERATOR.get_time_iterator(date_ranges).select do |interval|
interval[:start] >= from && interval[:end] <= to
end

this_week.each do |interval|
as_week[interval[:start].wday] << interval
end

as_week
end

def beginning_of_week(date)
days_to_monday = date.wday != 0 ? date.wday - 1 : 6
(date - days_to_monday).to_time
end

def end_of_week(date)
days_to_sunday = date.wday != 0 ? 7 - date.wday : 0
(date + days_to_sunday).to_time
end
end
end
end

+ 11
- 2
app/projections/nodes.rb View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true

module Hours
module Projections
module Nodes
@@ -14,11 +16,11 @@ module Hours
column :name, :text
column :opening_hours, :text
column :location, 'geography(POINT)'
column :address, :hstore
end

# Event handlers that update the projection in response to different
# events from the store.

project NodeAdded do |event|
table.insert(
node_id: event.aggregate_id,
@@ -27,7 +29,14 @@ module Hours
location: GeoRuby::SimpleFeatures::Point.from_x_y(
event.body['lon'],
event.body['lat']
)
),
address: Sequel.hstore(
street: event.body['addr_street'],
housenumber: event.body['addr_housenumber'],
postcode: event.body['addr_postcode'],
city: event.body['addr_city'],
addr_country_code: event.body['addr_country_code']
) # TODO: implement as Address Model PORO instead.
)
end
end

+ 0
- 44
app/projections/proposed_nodes_kml.rb View File

@@ -1,44 +0,0 @@
require 'ruby_kml'

module Hours
module Projections
module ProposedNodesKml
##
# Handles the ProposedNodes Projection
class Projector
include EventSourcery::EventProcessing::EventStreamProcessor
attr_accessor :filename
attr_writer :kml_file

def initialize(tracker:)
@tracker = tracker
@filename = File.join('public', 'proposed_nodes.kml')
end

def setup
KMLFile.new.save(filename)
end

process NodeAdded do |event|
kml_file.objects << KML::Placemark.new(
id: event.aggregate_id,
name: event.body[:author_email],
geometry: KML::Point.new(
coordinates: {
lat: event.body['lat'],
lng: event.body['lon']
}
)
)
kml_file.save(filename)
end

private

def kml_file
@kml_file ||= KMLFile.parse(File.open(filename, 'r'))
end
end
end
end
end

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true

module Hours
module Projections
module Nodes

+ 6
- 58
app/serializers/node.rb View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true

module Hours
module Serializers
##
@@ -5,70 +7,16 @@ module Hours
class Node < JSONAPI::Serializable::Resource
type 'node'

attributes :name, :lat, :lon
attributes :name, :lat, :lon, :status, :open_this_week
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
@object.opening_hours
end

attribute :address do
{
postcode: '6511RA',
city: 'Nijmegen',
housenumber: '1',
street: 'Burchtstraat'
}
as_hash = Sequel::Postgres::HStore.parse(@object.address).to_hash
as_hash.slice('postcode', 'city', 'housenumber', 'street')
end

link :self do

+ 11
- 0
bin/console View File

@@ -0,0 +1,11 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require 'bundler/setup'
require_relative '../lib/hours.rb'

require 'opening_hours_converter'

require 'pry'

Pry.start

+ 0
- 8
bin/server View File

@@ -1,8 +0,0 @@
#!/usr/bin/env nodejs

const { app } = require("../app")
const { config } = require("../config/environment")

app.listen(config.port, () => {
console.log(`Server loaded on port: ${config.port}`)
})

+ 2
- 0
config.ru View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true

$LOAD_PATH << '.'

require 'lib/app.rb'

+ 2
- 0
config/environment.rb View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true

##
# Setup of database.
Hours.configure do |config|

+ 5
- 0
config/event_sourcery.rb View File

@@ -1,9 +1,14 @@
# frozen_string_literal: true

##
# Configure the event sourcery databases.
# This assumes they already exist!
EventSourcery::Postgres.configure do |config|
database = Sequel.connect(Hours.config.database_url)

Sequel.extension :pg_hstore
Sequel.extension :pg_hstore_ops

# NOTE: Often we choose to split our events and projections into separate
# databases. For the purposes of this example we'll use one.
config.event_store_database = database

+ 2
- 0
lib/app.rb View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true

# This contains the Sinatra App
require_relative './hours.rb'
require_relative '../config/event_sourcery.rb'

+ 2
- 1
lib/hours.rb View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true

require 'dotenv/load'

require 'event_sourcery'
@@ -9,7 +11,6 @@ require_relative '../app/events/node_added.rb'
require_relative '../app/commands/node/add.rb'
require_relative '../app/aggregates/node.rb'
require_relative '../app/projections/nodes.rb'
require_relative '../app/projections/proposed_nodes_kml.rb'
require_relative '../app/projections/query.rb'

# Monkey patch

+ 2
- 0
test/commands/node/add_test.rb View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true

require 'test_helper'

describe Hours::Commands::Node::Add::Command do

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

@@ -6,7 +6,7 @@
"addr_street": "Burchtstraat",
"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\"",
"opening_hours": "Mo-We 10:00-18:00; Th 10:00-21:00; Fr 10:00-18:00; Sa 09:30-17:30",
"lat": 51.8473397,
"lon": 5.8653234
}

+ 27
- 40
test/fixtures/json/nodes.json View File

@@ -9,46 +9,33 @@
},
"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"
}
],
"raw_opening_hours" : "Mo-We 10:00-18:00; Th 10:00-21:00; Fr 10:00-18:00; Sa 09:30-17:30",
"open_this_week" : {
"1": [{
"start" : "1989-11-06 10:00:00 +0100",
"end" : "1989-11-06 18:00:00 +0100"
}],
"2": [{
"start" : "1989-11-07 10:00:00 +0100",
"end" : "1989-11-07 18:00:00 +0100"
}],
"3": [{
"start" : "1989-11-08 10:00:00 +0100",
"end" : "1989-11-08 18:00:00 +0100"
}],
"4": [{
"start" : "1989-11-09 10:00:00 +0100",
"end" : "1989-11-09 21:00:00 +0100"
}],
"5": [{
"start" : "1989-11-10 10:00:00 +0100",
"end" : "1989-11-10 18:00:00 +0100"
}],
"6": [{
"start" : "1989-11-11 09:30:00 +0100",
"end" : "1989-11-11 17:30:00 +0100"
}]
},
"name" : "H&M",
"status" : true,
"lat": 51.8473397,

+ 2
- 17
test/integration/add_node_test.rb View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true

require 'test_helper'

describe 'add node' do
@@ -41,23 +43,6 @@ describe 'add node' do
)
end

# TODO: we probably don't want a KML file, do we?
it 'adds a node to proposed_nodes KML projection' do
setup_projectors
post_json "/nodes/#{node_id}",
lat: lat,
lon: lon,
author_email: 'harry@example.com',
contact_details: 'h.potter@example.com or visit me at home'

projector_process_event(node_id)

kml = File.read(File.join('public', 'proposed_nodes.kml'))
placemarks = Nokogiri::XML(kml).xpath('//xmlns:Placemark')

assert_includes("#{lon},#{lat}", placemarks.inner_text.strip!)
end

describe 'when the node id already exists' do
before do
post_json "/nodes/#{node_id}", lat: lat, lon: lon

+ 43
- 7
test/integration/view_proposed_nodes_test.rb View File

@@ -1,5 +1,8 @@
# frozen_string_literal: true

require 'test_helper'
require 'json_expressions/minitest'
require 'timecop'

describe 'pending nodes' do
describe 'GET /nodes' do
@@ -24,16 +27,49 @@ describe 'pending nodes' do
projector.process(event)
end

get '/nodes'
assert_equal(200, last_response.status)
Timecop.travel(die_wende) do
assert Time.now.hour < 21 # Before 21:00
assert Time.now.thursday?

get '/nodes'
assert_status(200)

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_json_match(expected, parsed)
end
end

it 'on a thursday, after 21:00 it is closed' do
projector.setup

events.each do |event|
projector.process(event)
end

after_hours = die_wende + (60 * 60 * 4)
Timecop.travel(after_hours) do
assert Time.now.hour > 21 # After 21:00

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

parsed = JSON.parse(last_response.body, symbolize_names: true)
assert_status(200)
parsed = JSON.parse(last_response.body, symbolize_names: true)

assert_json_match(expected, parsed)
expected = {
"data": [
{
attributes: { status: false }.ignore_extra_keys!
}.ignore_extra_keys!
]
}.ignore_extra_keys!

assert_json_match(expected, parsed)
end
end
end
end

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true

require 'test_helper'

describe Hours::Models::Node do
@@ -33,4 +35,30 @@ describe Hours::Models::Node do
assert_equal lon, Hours::Models::Node.new(location: point).lon
end
end

describe 'opening_hours' do
let(:opening_hours) { '1989 Mo-Fr 10:00-19:00' }

before do
subject.opening_hours = opening_hours
end

describe 'status' do
it 'is true between 10:00 and 19:00' do
at_time do
assert_time_after(Time.parse('10:00'))
assert_time_before(Time.parse('19:00'))

assert_equal true, subject.status
end
end

it 'is false after 19:00' do
at_time(die_wende + 3600) do
assert_time_after(Time.parse('19:00'))
assert_equal false, subject.status
end
end
end
end
end

+ 0
- 70
test/projections/proposed_nodes_kml_test.rb View File

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

describe Hours::Projections::ProposedNodesKml::Projector do
let(:file) { File.join('public', 'proposed_nodes.kml') }
let(:tracker) { Minitest::Mock.new }
subject do
Hours::Projections::ProposedNodesKml::Projector.new(tracker: tracker)
end

before do
File.delete(file) if File.exist?(file)
end

describe 'setup' do
it 'creates a KML file' do
subject.setup

assert(File.exist?(file), "File #{file} not found")
end
end

describe 'process on NodeAdded' do
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

let(:kml_file_mock) do
Minitest::Mock.new.expect(:objects, [])
end

before do
subject.setup
end

it 'inserts a PlaceMark' do
subject.process(event)
assert_file_contains(file, '<coordinates>20.02,20.01</coordinates>')
end

it 'saves a file' do
kml_file_mock.expect(:save, true, [file])
subject.kml_file = kml_file_mock

subject.process(event)
assert_mock kml_file_mock
end

it 'appends a file' do
kml_builder = KMLFile.new
kml_builder.objects << KML::Placemark.new(
geometry: KML::Point.new(coordinates: { lat: 30.01, lng: 30.02 })
)
kml_builder.save(file)

subject.process(event)

# the order does not really matter
assert_file_contains(file, '<coordinates>20.02,20.01</coordinates>')
assert_file_contains(file, '<coordinates>30.02,30.01</coordinates>')
end
end
end

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

@@ -1,3 +1,5 @@
# frozen_string_literal: true

require 'test_helper'

describe Hours::Projections::Nodes::Query do

+ 3
- 4
test/support/event_helpers.rb View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true

##
# Helpers for testing events.
module EventHelpers
@@ -19,10 +21,7 @@ module EventHelpers

def projectors
@projectors = [
Hours::Projections::Nodes::Projector.new,
Hours::Projections::ProposedNodesKml::Projector.new(
tracker: EventSourcery::Memory::Tracker.new
)
Hours::Projections::Nodes::Projector.new
]
end
end

+ 2
- 0
test/support/file_helpers.rb View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true

##
# Helpers for testing against files
module FileHelpers

+ 8
- 0
test/support/request_helpers.rb View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true

require 'ostruct'
require 'json'
require 'rack/test'
@@ -13,4 +15,10 @@ module RequestHelpers
defaults = { 'Content-Type' => 'application/json' }
post url, body.to_json, headers.merge(defaults)
end

def assert_status(status, message = nil)
message ||= "Expected #{status}, got #{last_response.status}.\n"\
"#{last_response.body}"
assert_equal(status, last_response.status, message)
end
end

+ 17
- 3
test/support/time_helpers.rb View File

@@ -1,9 +1,11 @@
# frozen_string_literal: true

module TimeHelpers
##
# Time Helper to freeze the time at die_wende for the duration of a block
# stub goes away once the block is done
def at_die_wende(&block)
Time.stub :now, die_wende do
def at_time(time = die_wende, &block)
Time.stub :now, time do
yield block
end
end
@@ -11,6 +13,18 @@ module TimeHelpers
##
# Time Helper, returns a Time
def die_wende
Time.new(1989, 11, 9, 18, 57, 0, 0)
Time.local(1989, 11, 9, 18, 57, 0, 0)
end

##
# Assert after a certain time
def assert_time_after(expected, actual = Time.now.getlocal)
assert expected < actual, "#{actual} is not after #{expected}"
end

##
# Assert before a certain time
def assert_time_before(expected, actual = Time.now.getlocal)
assert expected > actual, "#{actual} is not before #{expected}"
end
end

+ 2
- 0
test/test_helper.rb View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true

require 'minitest/autorun'
require 'database_cleaner'
require 'byebug'

Loading…
Cancel
Save