Commit 8e1682b6 by source_reader

added annotation support for nilmdb backends

parent 1d1f99a6
...@@ -84,9 +84,9 @@ module Joule ...@@ -84,9 +84,9 @@ module Joule
annotations annotations
end end
def delete_annotation(annotation_id) def delete_annotation(annotation)
# returns nil # returns nil
@backend.delete_annotation(annotation_id) @backend.delete_annotation(annotation.id)
end end
# === END ANNOTATIONS === # === END ANNOTATIONS ===
......
module Nilmdb module Nilmdb
class Adapter class Adapter
attr_accessor :backend
def initialize(url) def initialize(url)
@backend = Backend.new(url) @backend = Backend.new(url)
...@@ -49,6 +50,50 @@ module Nilmdb ...@@ -49,6 +50,50 @@ module Nilmdb
'nilmdb' 'nilmdb'
end end
# === ANNOTATIONS ===
def create_annotation(annotation)
path = annotation.db_stream.path
# returns an annotation object
annotations_json = @backend.read_annotations(path)
# find max id
if annotations_json.length > 0
new_id = annotations_json.map{|a| a["id"]}.max + 1
else
new_id = 1
end
annotation.id = new_id
annotations_json.push({
"id": new_id,
"title": annotation.title,
"content": annotation.content,
"start": annotation.start_time,
"end": annotation.end_time })
@backend.write_annotations(path, annotations_json)
end
def get_annotations(db_stream)
annotations = []
@backend.read_annotations(db_stream.path).
map do |json|
annotation = Annotation.new
annotation.id = json["id"]
annotation.title = json["title"]
annotation.content = json["content"]
annotation.start_time = json["start"]
annotation.end_time = json["end"]
annotation.db_stream = db_stream
annotations.push(annotation)
end
annotations
end
def delete_annotation(annotation)
path = annotation.db_stream.path
updated_annotations =
@backend.read_annotations(path).select do |json|
json["id"] != annotation.id
end
@backend.write_annotations(path, updated_annotations)
end
end end
end end
...@@ -145,6 +145,33 @@ module Nilmdb ...@@ -145,6 +145,33 @@ module Nilmdb
return nil return nil
end end
def write_annotations(path, annotations_json)
data = {__annotations: annotations_json.to_json}.to_json
params = { path: path,
data: data }.to_json
begin
resp = self.class.post("#{@url}/stream/update_metadata",
body: params,
headers: { 'Content-Type' => 'application/json' })
raise "error writing annotations #{resp.body}" unless resp.success?
rescue
raise "connection error"
end
end
def read_annotations(path)
begin
resp = self.class.get("#{@url}/stream/get_metadata?path=#{path}&key=__annotations")
raise "error reading annotations #{resp.body}" unless resp.success?
json = resp.parsed_response["__annotations"]
return [] if json.nil?
annotations_json = JSON.parse(json) # annotations are JSON encoded
rescue
raise "connection error"
end
annotations_json
end
def _set_path_metadata(path, data) def _set_path_metadata(path, data)
params = { path: path, params = { path: path,
data: data }.to_json data: data }.to_json
......
...@@ -40,9 +40,12 @@ class AnnotationsController < ApplicationController ...@@ -40,9 +40,12 @@ class AnnotationsController < ApplicationController
# DELETE /annotations/1.json # DELETE /annotations/1.json
def destroy def destroy
annotation = Annotation.new
annotation.db_stream = @db_stream
annotation.id = params[:id].to_i
@service = StubService.new @service = StubService.new
begin begin
@node_adapter.delete_annotation(params[:id].to_i) @node_adapter.delete_annotation(annotation)
rescue RuntimeError => e rescue RuntimeError => e
@service.add_error("Cannot delete annotation [#{e}]") @service.add_error("Cannot delete annotation [#{e}]")
render 'helpers/empty_response', status: :unprocessable_entity and return render 'helpers/empty_response', status: :unprocessable_entity and return
......
...@@ -10,4 +10,5 @@ class Annotation ...@@ -10,4 +10,5 @@ class Annotation
def self.json_keys def self.json_keys
[:id, :title, :content] [:id, :title, :content]
end end
end end
\ No newline at end of file
...@@ -31,13 +31,15 @@ describe Joule::Adapter do ...@@ -31,13 +31,15 @@ describe Joule::Adapter do
expect(annotation.db_stream).to be stream expect(annotation.db_stream).to be stream
end end
end end
it 'deletes annotations' do it 'deletes annotations' do
adapter = Joule::Adapter.new("url", "key") adapter = Joule::Adapter.new("url", "key")
mock_backend = instance_double(Joule::Backend) mock_backend = instance_double(Joule::Backend)
adapter.backend = mock_backend adapter.backend = mock_backend
annotation = FactoryBot.build(:annotation)
annotation.id = 3
expect(mock_backend).to receive(:delete_annotation) expect(mock_backend).to receive(:delete_annotation)
resp = adapter.delete_annotation(3) adapter.delete_annotation(annotation)
end end
end end
\ No newline at end of file
# frozen_string_literal: true
require 'rails_helper'
describe Nilmdb::Adapter do
it 'creates annotations' do
stream = FactoryBot.create(:db_stream, name: 'test_stream')
annotation = FactoryBot.build(:annotation, title: 'test', db_stream: stream)
adapter = Nilmdb::Adapter.new("url")
mock_backend = instance_double(Nilmdb::Backend)
adapter.backend = mock_backend
expect(mock_backend).to receive(:read_annotations) do
[{"id" => 100, "title"=> "test"},
{"id" => 250, "title" => "test"},
{"id" => 5, "title"=> "test"}]
end
expect(mock_backend).to receive(:write_annotations)
adapter.create_annotation(annotation)
expect(annotation.id).to eq 251
end
it 'creates first annotation' do
stream = FactoryBot.create(:db_stream, name: 'test_stream')
annotation = FactoryBot.build(:annotation, title: 'test', db_stream: stream)
adapter = Nilmdb::Adapter.new("url")
mock_backend = instance_double(Nilmdb::Backend)
adapter.backend = mock_backend
expect(mock_backend).to receive(:read_annotations) {[]}
expect(mock_backend).to receive(:write_annotations)
adapter.create_annotation(annotation)
expect(annotation.id).to eq 1
end
it 'gets annotations' do
adapter = Nilmdb::Adapter.new("url")
mock_backend = instance_double(Nilmdb::Backend)
adapter.backend = mock_backend
raw = File.read(File.dirname(__FILE__)+"/annotations.json")
json = JSON.parse(raw)
expect(mock_backend).to receive(:read_annotations) { json }
stream = FactoryBot.create(:db_stream, name: 'test_stream')
annotations = adapter.get_annotations(stream)
expect(annotations.length).to eq 6
annotations.each do | annotation |
expect(annotation.db_stream).to be stream
end
end
it 'deletes annotations' do
stream = FactoryBot.create(:db_stream, name: 'test_stream')
annotation = FactoryBot.build(:annotation, title: 'test', db_stream: stream)
adapter = Nilmdb::Adapter.new("url")
mock_backend = instance_double(Nilmdb::Backend)
adapter.backend = mock_backend
expect(mock_backend).to receive(:read_annotations) do
[{id: 100, title: "test"},
{id: 250, title: "test"},
{id: 5, title: "test"}]
end
expect(mock_backend).to receive(:write_annotations)
adapter.delete_annotation(annotation)
end
end
\ No newline at end of file
[
{
"id":1,
"title":"sdf",
"content":null,
"start":1561865146556000,
"end":1561888887008000
},
{
"id":2,
"title":"adsfxcv",
"content":"c",
"start":1562049135062000,
"end":1562061853161000
},
{
"id":3,
"title":"big chunk",
"content":"chunk!",
"start":1562191577775986,
"end":null
},
{
"id":4,
"title":"Point",
"content":"point",
"start":1562343347096614,
"end":null
},
{
"id":6,
"title":"just a point",
"content":"point",
"start":1561751531533494,
"end":null
},
{
"id":7,
"title":"more of it",
"content":"adf",
"start":1561857515696000,
"end":1562278060853000
}
]
\ No newline at end of file
...@@ -110,6 +110,7 @@ describe Nilmdb::Backend do ...@@ -110,6 +110,7 @@ describe Nilmdb::Backend do
end end
end end
describe 'get_intervals' do describe 'get_intervals' do
it 'returns array of interval line segments', :vcr do it 'returns array of interval line segments', :vcr do
backend = Nilmdb::Backend.new(url) backend = Nilmdb::Backend.new(url)
...@@ -121,4 +122,21 @@ describe Nilmdb::Backend do ...@@ -121,4 +122,21 @@ describe Nilmdb::Backend do
end end
end end
describe 'annotations' do
let(:url) {"http://127.0.0.1:8008"}
it 'reads and writes annotation metadata', :vcr do
backend = Nilmdb::Backend.new(url)
test_data = {"data": "data"}
backend.write_annotations("/pressure/internal", {data: "data"})
resp = backend.read_annotations("/pressure/internal")
expect(resp.symbolize_keys).to eq (test_data)
end
it 'handles errors', :vcr do
backend = Nilmdb::Backend.new(url)
expect{backend.read_annotations('/bad/path')}.to raise_error(RuntimeError)
expect{backend.write_annotations('/bad/path', {"data": "data"})}.to raise_error(RuntimeError)
end
end
end end
---
http_interactions:
- request:
method: get
uri: http://127.0.0.1:8008/stream/get_metadata?key=__annotations&path=/bad/path
body:
encoding: US-ASCII
string: ''
headers:
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
User-Agent:
- Ruby
response:
status:
code: 404
message: Not Found
headers:
Content-Length:
- '81'
Server:
- CherryPy/3.5.0
X-Jim-Is-Awesome:
- yeah
Allow:
- GET, HEAD
Date:
- Mon, 08 Jul 2019 17:14:29 GMT
Content-Type:
- application/json;charset=utf-8
body:
encoding: UTF-8
string: '{"status":"404 Not Found","message":"No stream at path /bad/path","traceback":""}'
http_version:
recorded_at: Mon, 08 Jul 2019 17:22:26 GMT
recorded_with: VCR 4.0.0
---
http_interactions:
- request:
method: post
uri: http://127.0.0.1:8008/stream/update_metadata
body:
encoding: UTF-8
string: '{"path":"/pressure/internal","data":"{\"__annotations\":\"{\\\"data\\\":\\\"data\\\"}\"}"}'
headers:
Content-Type:
- application/json
response:
status:
code: 200
message: OK
headers:
Content-Length:
- '4'
Server:
- CherryPy/3.5.0
X-Jim-Is-Awesome:
- yeah
Allow:
- POST
Date:
- Mon, 08 Jul 2019 17:15:55 GMT
Content-Type:
- application/json
body:
encoding: UTF-8
string: 'null'
http_version:
recorded_at: Mon, 08 Jul 2019 17:23:52 GMT
- request:
method: get
uri: http://127.0.0.1:8008/stream/get_metadata?key=__annotations&path=/pressure/internal
body:
encoding: US-ASCII
string: ''
headers:
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
User-Agent:
- Ruby
response:
status:
code: 200
message: OK
headers:
Content-Length:
- '40'
Server:
- CherryPy/3.5.0
X-Jim-Is-Awesome:
- yeah
Allow:
- GET, HEAD
Date:
- Mon, 08 Jul 2019 17:15:55 GMT
Content-Type:
- application/json
body:
encoding: UTF-8
string: '{"__annotations": "{\"data\":\"data\"}"}'
http_version:
recorded_at: Mon, 08 Jul 2019 17:23:52 GMT
recorded_with: VCR 4.0.0
...@@ -38,14 +38,14 @@ RSpec.describe AnnotationsController, type: :request do ...@@ -38,14 +38,14 @@ RSpec.describe AnnotationsController, type: :request do
get "/db_streams/#{stream.id}/annotations.json", headers: user.create_new_auth_token get "/db_streams/#{stream.id}/annotations.json", headers: user.create_new_auth_token
expect(response.header['Content-Type']).to include('application/json') expect(response.header['Content-Type']).to include('application/json')
body = JSON.parse(response.body) body = JSON.parse(response.body)
expect(body['messages']['errors']).to include('test') expect(body['messages']['errors'].join(' ')).to include('test')
end end
it 'returns error message if backend is not available' do it 'returns error message if backend is not available' do
allow(NodeAdapterFactory).to receive(:from_nilm).and_return(nil) allow(NodeAdapterFactory).to receive(:from_nilm).and_return(nil)
get "/db_streams/#{stream.id}/annotations.json", headers: user.create_new_auth_token get "/db_streams/#{stream.id}/annotations.json", headers: user.create_new_auth_token
expect(response.header['Content-Type']).to include('application/json') expect(response.header['Content-Type']).to include('application/json')
body = JSON.parse(response.body) body = JSON.parse(response.body)
expect(body['messages']['errors']).to include('Cannot contact installation') expect(body['messages']['errors'].join(' ')).to include('Cannot contact installation')
end end
end end
context 'without sign-in' do context 'without sign-in' do
...@@ -87,7 +87,7 @@ RSpec.describe AnnotationsController, type: :request do ...@@ -87,7 +87,7 @@ RSpec.describe AnnotationsController, type: :request do
post "/db_streams/#{stream.id}/annotations.json", headers: owner.create_new_auth_token post "/db_streams/#{stream.id}/annotations.json", headers: owner.create_new_auth_token
expect(response.header['Content-Type']).to include('application/json') expect(response.header['Content-Type']).to include('application/json')
body = JSON.parse(response.body) body = JSON.parse(response.body)
expect(body['messages']['errors']).to include('test') expect(body['messages']['errors'].join(' ')).to include('test')
end end
end end
...@@ -118,8 +118,8 @@ RSpec.describe AnnotationsController, type: :request do ...@@ -118,8 +118,8 @@ RSpec.describe AnnotationsController, type: :request do
context 'with owner' do context 'with owner' do
it 'deletes the annotation' do it 'deletes the annotation' do
mock_adapter = instance_double(Joule::Adapter) mock_adapter = instance_double(Joule::Adapter)
expect(mock_adapter).to receive(:delete_annotation) do |id| expect(mock_adapter).to receive(:delete_annotation) do |annotation|
expect(id).to eq 10 expect(annotation.id).to eq 10
end end
allow(NodeAdapterFactory).to receive(:from_nilm).and_return(mock_adapter) allow(NodeAdapterFactory).to receive(:from_nilm).and_return(mock_adapter)
delete "/db_streams/#{stream.id}/annotations/10.json", delete "/db_streams/#{stream.id}/annotations/10.json",
...@@ -136,7 +136,7 @@ RSpec.describe AnnotationsController, type: :request do ...@@ -136,7 +136,7 @@ RSpec.describe AnnotationsController, type: :request do
delete "/db_streams/#{stream.id}/annotations/10.json", headers: owner.create_new_auth_token delete "/db_streams/#{stream.id}/annotations/10.json", headers: owner.create_new_auth_token
expect(response.header['Content-Type']).to include('application/json') expect(response.header['Content-Type']).to include('application/json')
body = JSON.parse(response.body) body = JSON.parse(response.body)
expect(body['messages']['errors']).to include('test') expect(body['messages']['errors'].join(' ')).to include('test')
end end
end end
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment