Browse Source

Implement basic json serialisation for events with lat-lon pairs

develop
Bèr Kessels 4 months ago
parent
commit
37eafadc76

+ 3
- 3
Gemfile View File

@@ -6,11 +6,14 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6 6
 
7 7
 gem 'event_sourcery'
8 8
 gem 'event_sourcery-postgres'
9
+gem 'json_expressions'
10
+gem 'jsonapi-rb'
9 11
 gem 'nominatim', github: 'lukaszsliwa/nominatim'
10 12
 gem 'opening_hours_converter'
11 13
 gem 'rake'
12 14
 gem 'ruby_kml'
13 15
 gem 'semver'
16
+gem 'sequel-postgis-georuby'
14 17
 gem 'sinatra'
15 18
 
16 19
 group :development, :test do
@@ -29,6 +32,3 @@ group :development, :test do
29 32
 
30 33
   gem 'foreman'
31 34
 end
32
-
33
-# Added at 2019-06-05 18:00:08 +0200 by ber:
34
-gem "jsonapi-rb", "~> 0.5.0"

+ 8
- 1
Gemfile.lock View File

@@ -30,8 +30,10 @@ GEM
30 30
       multipart-post (>= 1.2, < 3)
31 31
     foreman (0.85.0)
32 32
       thor (~> 0.19.1)
33
+    georuby (2.5.2)
33 34
     jaro_winkler (1.5.2)
34 35
     json (2.1.0)
36
+    json_expressions (0.9.0)
35 37
     jsonapi-deserializable (0.2.0)
36 38
     jsonapi-rb (0.5.0)
37 39
       jsonapi-deserializable (~> 0.2.0)
@@ -74,6 +76,9 @@ GEM
74 76
       nokogiri
75 77
     semver (1.0.1)
76 78
     sequel (5.21.0)
79
+    sequel-postgis-georuby (0.1.2)
80
+      georuby (~> 2.5.2)
81
+      sequel (~> 5.0)
77 82
     sinatra (2.0.5)
78 83
       mustermann (~> 1.0)
79 84
       rack (~> 2.0)
@@ -95,7 +100,8 @@ DEPENDENCIES
95 100
   event_sourcery
96 101
   event_sourcery-postgres
97 102
   foreman
98
-  jsonapi-rb (~> 0.5.0)
103
+  json_expressions
104
+  jsonapi-rb
99 105
   minitest
100 106
   nokogiri
101 107
   nominatim!
@@ -105,6 +111,7 @@ DEPENDENCIES
105 111
   rubocop
106 112
   ruby_kml
107 113
   semver
114
+  sequel-postgis-georuby
108 115
   sinatra
109 116
 
110 117
 BUNDLED WITH

+ 1
- 1
Rakefile View File

@@ -1,6 +1,6 @@
1 1
 $LOAD_PATH.unshift '.'
2 2
 
3
-require "dotenv/load"
3
+require 'dotenv/load'
4 4
 
5 5
 task :unset_db_name do
6 6
   @db_name = ENV['DB_NAME']

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

@@ -0,0 +1,18 @@
1
+module Hours
2
+  module Models
3
+    ##
4
+    # A  Node is a readonly model for querying the projection of  "places".
5
+    # * Readonly: it is not enforced on model or ORM level, but should  be
6
+    #   enforced in the database-server and user setup. RO only means that
7
+    #   this model is only tested and optimized against reading.
8
+    class Node < Sequel::Model(::Hours.projections_database[:query_nodes])
9
+      def lat
10
+        location.lat
11
+      end
12
+
13
+      def lon
14
+        location.lon
15
+      end
16
+    end
17
+  end
18
+end

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

@@ -12,13 +12,8 @@ module Hours
12 12
 
13 13
         table :query_nodes do
14 14
           column :node_id, 'UUID NOT NULL'
15
-          column :lat, BigDecimal, size: [10, 7] # TODO: move to postgis?
16
-          column :lon, BigDecimal, size: [10, 7]
17 15
           column :name, :text
18
-          column :author_email, :text
19
-          column :contact_details, :text
20
-          column :amount, Integer
21
-          column :proposed_at, DateTime
16
+          column :location, 'geography(POINT)'
22 17
         end
23 18
 
24 19
         # Event handlers that update the projection in response to different
@@ -27,13 +22,11 @@ module Hours
27 22
         project NodeAdded do |event|
28 23
           table.insert(
29 24
             node_id: event.aggregate_id,
30
-            lat: event.body['lat'],
31
-            lon: event.body['lon'],
32 25
             name: event.body['name'],
33
-            author_email: event.body['author_email'],
34
-            contact_details: event.body['contact_details'],
35
-            amount: event.body['amount'].to_i,
36
-            proposed_at: Time.now
26
+            location: GeoRuby::SimpleFeatures::Point.from_x_y(
27
+              event.body['lon'],
28
+              event.body['lat']
29
+            )
37 30
           )
38 31
         end
39 32
       end

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

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

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

@@ -0,0 +1,84 @@
1
+module Hours
2
+  module Serializers
3
+    ##
4
+    # Represent a Node as JSON
5
+    class Node < JSONAPI::Serializable::Resource
6
+      type 'node'
7
+
8
+      attributes :name, :lat, :lon
9
+      id { @object.node_id }
10
+
11
+      attribute :raw_opening_hours do
12
+        'Mo-We 10:00-18:00; Th 10:00-21:00; Fr 10:00-18:00; Sa 09:30-17:30; '\
13
+         'PH closed; Su 12:00-17:00 open "Koopzondag"'
14
+      end
15
+
16
+      attribute :week_stable do
17
+        false
18
+      end
19
+
20
+      attribute :open_this_week do
21
+        [
22
+          {
23
+            from: '1989-11-06T09:00:00.000Z',
24
+            to: '1989-11-06T17:00:00.000Z',
25
+            unknown: false
26
+          },
27
+          {
28
+            to: '1989-11-07T17:00:00.000Z',
29
+            unknown: false,
30
+            from: '1989-11-07T09:00:00.000Z'
31
+          },
32
+          {
33
+            unknown: false,
34
+            to: '1989-11-08T17:00:00.000Z',
35
+            from: '1989-11-08T09:00:00.000Z'
36
+          },
37
+          {
38
+            from: '1989-11-09T09:00:00.000Z',
39
+            to: '1989-11-09T20:00:00.000Z',
40
+            unknown: false
41
+          },
42
+          {
43
+            to: '1989-11-10T17:00:00.000Z',
44
+            unknown: false,
45
+            from: '1989-11-10T09:00:00.000Z'
46
+          },
47
+          {
48
+            to: '1989-11-11T16:30:00.000Z',
49
+            unknown: false,
50
+            from: '1989-11-11T08:30:00.000Z'
51
+          },
52
+          {
53
+            comment: 'Koopzondag',
54
+            to: '1989-11-12T16:00:00.000Z',
55
+            unknown: false,
56
+            from: '1989-11-12T11:00:00.000Z'
57
+          }
58
+        ]
59
+      end
60
+
61
+      attribute :status do
62
+        true
63
+      end
64
+
65
+      attribute :address do
66
+        {
67
+          postcode: '6511RA',
68
+          city: 'Nijmegen',
69
+          housenumber: '1',
70
+          street: 'Burchtstraat'
71
+        }
72
+      end
73
+
74
+      link :self do
75
+        # TODO: move  into an URL-helper
76
+        "/nodes/#{@object.node_id}"
77
+      end
78
+
79
+      meta do
80
+        { copyright: 'OpenStreetMap-contributors' }
81
+      end
82
+    end
83
+  end
84
+end

+ 1
- 1
config.ru View File

@@ -1,5 +1,5 @@
1 1
 $LOAD_PATH << '.'
2 2
 
3
-require 'lib/hours.rb'
3
+require 'lib/app.rb'
4 4
 
5 5
 run Sinatra::Application

+ 2
- 0
config/environment.rb View File

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

BIN
images/open_openingstijden.png View File


+ 103
- 0
images/open_openingstijden.svg View File

@@ -0,0 +1,103 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
3
+
4
+<svg
5
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
6
+   xmlns:cc="http://creativecommons.org/ns#"
7
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8
+   xmlns:svg="http://www.w3.org/2000/svg"
9
+   xmlns="http://www.w3.org/2000/svg"
10
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12
+   version="1.1"
13
+   id="Capa_1"
14
+   x="0px"
15
+   y="0px"
16
+   width="494.239px"
17
+   height="494.238px"
18
+   viewBox="0 0 494.239 494.238"
19
+   style="enable-background:new 0 0 494.239 494.238;"
20
+   xml:space="preserve"
21
+   sodipodi:docname="open_openingstijden.svg"
22
+   inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
23
+   inkscape:export-filename="/home/ber/Documenten/PBX_placebazaar/hours/images/open_openingstijden.png"
24
+   inkscape:export-xdpi="90"
25
+   inkscape:export-ydpi="90"><metadata
26
+   id="metadata8713"><rdf:RDF><cc:Work
27
+       rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
28
+         rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
29
+   id="defs8711" /><sodipodi:namedview
30
+   pagecolor="#ffffff"
31
+   bordercolor="#666666"
32
+   borderopacity="1"
33
+   objecttolerance="10"
34
+   gridtolerance="10"
35
+   guidetolerance="10"
36
+   inkscape:pageopacity="0"
37
+   inkscape:pageshadow="2"
38
+   inkscape:window-width="1850"
39
+   inkscape:window-height="1016"
40
+   id="namedview8709"
41
+   showgrid="false"
42
+   inkscape:zoom="1.6874461"
43
+   inkscape:cx="257.78551"
44
+   inkscape:cy="247.11901"
45
+   inkscape:window-x="70"
46
+   inkscape:window-y="27"
47
+   inkscape:window-maximized="1"
48
+   inkscape:current-layer="g8676" />
49
+<g
50
+   id="g8676">
51
+	<path
52
+   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"
53
+   id="path8674"
54
+   inkscape:connector-curvature="0"
55
+   sodipodi:nodetypes="cccccccccssscscccsc"
56
+   style="fill:#2c3e50;fill-opacity:1;stroke-width:0.88470417" />
57
+</g>
58
+<g
59
+   id="g8678">
60
+</g>
61
+<g
62
+   id="g8680">
63
+</g>
64
+<g
65
+   id="g8682">
66
+</g>
67
+<g
68
+   id="g8684">
69
+</g>
70
+<g
71
+   id="g8686">
72
+</g>
73
+<g
74
+   id="g8688">
75
+</g>
76
+<g
77
+   id="g8690">
78
+</g>
79
+<g
80
+   id="g8692">
81
+</g>
82
+<g
83
+   id="g8694">
84
+</g>
85
+<g
86
+   id="g8696">
87
+</g>
88
+<g
89
+   id="g8698">
90
+</g>
91
+<g
92
+   id="g8700">
93
+</g>
94
+<g
95
+   id="g8702">
96
+</g>
97
+<g
98
+   id="g8704">
99
+</g>
100
+<g
101
+   id="g8706">
102
+</g>
103
+</svg>

+ 1
- 1
lib/app.rb View File

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

+ 1
- 0
lib/hours.rb View File

@@ -2,6 +2,7 @@ require 'dotenv/load'
2 2
 
3 3
 require 'event_sourcery'
4 4
 require 'event_sourcery/postgres'
5
+require 'sequel-postgis-georuby'
5 6
 require 'securerandom'
6 7
 
7 8
 require_relative '../app/events/node_added.rb'

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

@@ -7,5 +7,6 @@
7 7
   "addr_city": "Nijmegen",
8 8
   "addr_country_code": "nl",
9 9
   "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\"",
10
-  "geometry": { "type": "Point", "coordinates": [51.8473397, 5.8653234] }
10
+  "lat": 51.8473397,
11
+  "lon": 5.8653234
11 12
 }

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

@@ -0,0 +1,67 @@
1
+{
2
+  "data": [
3
+    {
4
+       "links" : {
5
+          "self" : "/places/"
6
+       },
7
+       "meta" : {
8
+          "copyright" : "OpenStreetMap-contributors"
9
+       },
10
+       "type" : "node",
11
+       "attributes" : {
12
+         "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\"",
13
+         "week_stable" : false,
14
+         "open_this_week" : [
15
+           {
16
+             "from" : "1989-11-06T09:00:00.000Z",
17
+             "to" : "1989-11-06T17:00:00.000Z",
18
+             "unknown" : false
19
+           },
20
+           {
21
+             "to" : "1989-11-07T17:00:00.000Z",
22
+             "unknown" : false,
23
+             "from" : "1989-11-07T09:00:00.000Z"
24
+           },
25
+           {
26
+             "unknown" : false,
27
+             "to" : "1989-11-08T17:00:00.000Z",
28
+             "from" : "1989-11-08T09:00:00.000Z"
29
+           },
30
+           {
31
+             "from" : "1989-11-09T09:00:00.000Z",
32
+             "to" : "1989-11-09T20:00:00.000Z",
33
+             "unknown" : false
34
+           },
35
+           {
36
+             "to" : "1989-11-10T17:00:00.000Z",
37
+             "unknown" : false,
38
+             "from" : "1989-11-10T09:00:00.000Z"
39
+           },
40
+           {
41
+             "to" : "1989-11-11T16:30:00.000Z",
42
+             "unknown" : false,
43
+             "from" : "1989-11-11T08:30:00.000Z"
44
+           },
45
+           {
46
+             "comment" : "Koopzondag",
47
+             "to" : "1989-11-12T16:00:00.000Z",
48
+             "unknown" : false,
49
+             "from" : "1989-11-12T11:00:00.000Z"
50
+           }
51
+         ],
52
+         "name" : "H&M",
53
+         "status" : true,
54
+         "lat": 51.8473397,
55
+         "lon": 5.8653234,
56
+         "address" : {
57
+           "postcode" : "6511RA",
58
+           "city" : "Nijmegen",
59
+           "housenumber" : "1",
60
+           "street" : "Burchtstraat"
61
+         }
62
+       },
63
+       "id" : "1337"
64
+    }
65
+  ]
66
+}
67
+

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

@@ -1,4 +1,5 @@
1 1
 require 'test_helper'
2
+require 'json_expressions/minitest'
2 3
 
3 4
 describe 'pending nodes' do
4 5
   describe 'GET /nodes' do
@@ -10,8 +11,8 @@ describe 'pending nodes' do
10 11
       [
11 12
         NodeAdded.new(
12 13
           aggregate_id: hm_id,
13
-          body:  body
14
-        ),
14
+          body: body
15
+        )
15 16
       ]
16 17
     end
17 18
     let(:projector) { Hours::Projections::Nodes::Projector.new }
@@ -24,11 +25,15 @@ describe 'pending nodes' do
24 25
       end
25 26
 
26 27
       get '/nodes'
27
-
28 28
       assert_equal(200, last_response.status)
29
-      expected = [json_fixtures('json/hm.json')]
29
+
30
+      expected = json_fixtures('json/nodes.json')
31
+      expected[:data][0][:links][:self] = String
32
+      expected[:data][0][:id] = String
33
+
30 34
       parsed = JSON.parse(last_response.body, symbolize_names: true)
31
-      assert_equal(expected, parsed)
35
+
36
+      assert_json_match(expected, parsed)
32 37
     end
33 38
   end
34 39
 end

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

@@ -0,0 +1,36 @@
1
+require 'test_helper'
2
+
3
+describe Hours::Models::Node do
4
+  let(:pk) { SecureRandom.uuid }
5
+  subject do
6
+    Hours::Models::Node.new
7
+  end
8
+
9
+  describe 'dataset' do
10
+    it 'reads from :query_nodes' do
11
+      assert_equal :query_nodes, subject.class.table_name
12
+    end
13
+  end
14
+
15
+  describe 'location' do
16
+    let(:lat) do
17
+      20.1
18
+    end
19
+
20
+    let(:lon) do
21
+      20.2
22
+    end
23
+
24
+    let(:point) do
25
+      GeoRuby::SimpleFeatures::Point.from_x_y(lon, lat)
26
+    end
27
+
28
+    it 'has a lat' do
29
+      assert_equal lat, Hours::Models::Node.new(location: point).lat
30
+    end
31
+
32
+    it 'has a lon' do
33
+      assert_equal lon, Hours::Models::Node.new(location: point).lon
34
+    end
35
+  end
36
+end

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

@@ -0,0 +1,35 @@
1
+require 'test_helper'
2
+
3
+describe Hours::Projections::Nodes::Query do
4
+  subject do
5
+    Hours::Projections::Nodes::Query.handle
6
+  end
7
+
8
+  let(:tracker) { Minitest::Mock.new }
9
+
10
+  let(:projector) do
11
+    Hours::Projections::Nodes::Projector.new(tracker: tracker)
12
+  end
13
+
14
+  let(:event) do
15
+    NodeAdded.new(
16
+      aggregate_id: SecureRandom.uuid,
17
+      body: {
18
+        lat: 20.01,
19
+        lon: 20.02,
20
+        author_email: 'ronweasly@example.com',
21
+        contact_details: 'The Nest'
22
+      }
23
+    )
24
+  end
25
+
26
+  before do
27
+    # Ensure that we have processed events, which stores nodes in the db
28
+    projector.process(event)
29
+  end
30
+
31
+  it 'fetches a list of Nodes through Sequel model' do
32
+    assert_kind_of Array, subject
33
+    assert_kind_of Hours::Models::Node, subject.first
34
+  end
35
+end

+ 2
- 1
test/test_helper.rb View File

@@ -3,6 +3,7 @@ require 'database_cleaner'
3 3
 require 'byebug'
4 4
 
5 5
 require 'hours'
6
+require 'app'
6 7
 
7 8
 require 'awesome_print'
8 9
 require 'ostruct'
@@ -37,7 +38,7 @@ module Minitest
37 38
     end
38 39
 
39 40
     def json_fixtures(file)
40
-      JSON.parse(File.read(fixtures(file)))
41
+      JSON.parse(File.read(fixtures(file)), symbolize_names: true)
41 42
     end
42 43
   end
43 44
 end

Loading…
Cancel
Save