Skip to content

Commit

Permalink
Create new serialization logic and use to generate JSON for forwarding.
Browse files Browse the repository at this point in the history
Future work:
- improve test coverage (on presenters themselves)
- Look into API versioning, use this representation in HTTP API too in a
  new version
- Delete old formatting code once released.
- Sort out the mess of different reading representations we use in
  different places and radically simplify that part of everything
  • Loading branch information
timcowlishaw committed Jul 10, 2024
1 parent 9849cc2 commit d0245d6
Show file tree
Hide file tree
Showing 11 changed files with 298 additions and 1 deletion.
3 changes: 3 additions & 0 deletions app/controllers/v0/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class ApplicationController < ActionController::API
helper ::UserHelper
include ::UserHelper

helper ::PresentationHelper
include ::PresentationHelper

respond_to :json

before_action :prepend_view_paths
Expand Down
5 changes: 5 additions & 0 deletions app/helpers/presentation_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module PresentationHelper
def present(model, options={})
Presenters.present(model, current_user, self, options)
end
end
26 changes: 26 additions & 0 deletions app/lib/presenters.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module Presenters
# This is work in progress we're releasing early so
# that it can be used in forwarding to send the current
# values as they're received.
# TODO: add presenter tests, finish refactor following
# spec in your spreadsheet, remove unneeded options,
# use in appropriate views, add unauthorized_fields logic
# delete unneeded code in models and views.
PRESENTERS = {
Device => Presenters::DevicePresenter,
User => Presenters::UserPresenter,
Component => Presenters::ComponentPresenter,
Sensor => Presenters::SensorPresenter,
Measurement => Presenters::MeasurementPresenter,
}

def self.present(model_or_collection, user, render_context, options={})
if model_or_collection.is_a?(Enumerable)
model_or_collection.map { |model| present(model, user, render_context, options) }
else
PRESENTERS[model_or_collection.class]&.new(
model_or_collection, user, render_context, options
).as_json
end
end
end
42 changes: 42 additions & 0 deletions app/lib/presenters/base_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module Presenters
class BasePresenter

def default_options
{}
end

def exposed_fields
[]
end

def initialize(model, current_user=nil, render_context=nil, options={})
@model = model
@current_user = current_user
@render_context = render_context
@options = self.default_options.merge(options)
end

def as_json(_opts=nil)
self.exposed_fields.inject({}) { |hash, field|
value = self.send(field)
value.nil? ? hash : hash.merge(field => value)
}
end

def method_missing(method, *args, &block)
if self.exposed_fields.include?(method)
model.public_send(method, *args, &block)
else
super
end
end

def present(other_model, options={})
Presenters.present(other_model, current_user, render_context, options)
end

private

attr_reader :model, :current_user, :options, :render_context
end
end
50 changes: 50 additions & 0 deletions app/lib/presenters/component_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
module Presenters
class ComponentPresenter < BasePresenter

alias_method :component, :model

def default_options
{ readings: nil }
end

def exposed_fields
%i{key sensor last_reading_at latest_value previous_value readings}
end

def sensor
present(component.sensor)
end

def latest_value
data = component.device.data
data[component.sensor_id.to_s] if data
end

def previous_value
old_data = component.device.old_data
old_data[component.sensor_id.to_s] if old_data
end

def readings
readings = options[:readings]
if readings
readings.flat_map { |reading| format_reading(reading) }.compact
end
end

private

def format_reading(reading)
# TODO sort out the mess of multiple reading formats used ini
# DataParser, RawStorer, etc, etc.
reading.data.map { |entry|
timestamp = entry.timestamp
value = entry.sensors&.find { |sensor|
sensor["id"] == component.sensor_id
}.dig("value")
{ timestamp: timestamp, value: value } if value
}.compact

end
end
end
94 changes: 94 additions & 0 deletions app/lib/presenters/device_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
module Presenters
class DevicePresenter < BasePresenter
alias_method :device, :model

def default_options
{
with_owner: true,
with_data: true,
with_postprocessing: true,
with_location: true,
slim_owner: false,
never_authorized: false,
readings: nil
}
end

def exposed_fields
%i{id uuid name description state system_tags user_tags last_reading_at created_at updated_at notify device_token mac_address postprocessing location data_policy hardware owner components}
end

def notify
{
stopped_publishing: device.notify_stopped_publishing,
low_battery: device.notify_low_battery
}
end

def location
if options[:with_location]
{
exposure: device.exposure,
elevation: device.elevation.try(:to_i) ,
latitude: device.latitude,
longitude: device.longitude,
geohash: device.geohash,
city: device.city,
country_code: device.country_code,
country: device.country_name
}
end
end

def data_policy
{
is_private: authorized? ? device.is_private : "[FILTERED]",
enable_forwarding: authorized? ? device.enable_forwarding : "[FILTERED]",
precise_location: authorized? ? device.precise_location : "[FILTERED]"
}
end

def hardware
{
name: device.hardware_name,
type: device.hardware_type,
version: device.hardware_version,
slug: device.hardware_slug,
last_status_message: authorized? ? device.hardware_info : "[FILTERED]",
}
end

def owner
if options[:with_owner] && device.owner
present(device.owner, with_devices: false)
end
end

def postprocessing
device.postprocessing if options[:with_postprocessing]
end

def device_token
authorized? ? device.device_token : "[FILTERED]"
end

def mac_address
authorized? ? device.mac_address : "[FILTERED]"
end

def components
present(device.components)
end

private

def authorized?
!options[:never_authorized] && policy.show_private_info?
end

def policy
@policy ||= DevicePolicy.new(current_user, device)
end

end
end
10 changes: 10 additions & 0 deletions app/lib/presenters/measurement_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Presenters
class MeasurementPresenter < BasePresenter

alias_method :measurement, :model

def exposed_fields
%i{id name description unit uuid definition}
end
end
end
14 changes: 14 additions & 0 deletions app/lib/presenters/sensor_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module Presenters
class SensorPresenter < BasePresenter

alias_method :sensor, :model

def exposed_fields
%i{id parent_id name description unit created_at updated_at uuid default_key datasheet unit_definition measurement tags}
end

def measurement
present(sensor.measurement)
end
end
end
42 changes: 42 additions & 0 deletions app/lib/presenters/user_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module Presenters
class UserPresenter < BasePresenter

alias_method :user, :model

def default_options
{
with_devices: true
}
end

def exposed_fields
%i{id uuid role username profile_picture url location email legacy_api_key devices created_at updated_at}
end

def profile_picture
render_context&.profile_picture_url(user)
end

def email
user.email if authorized?
end

def legacy_api_key
user.legacy_api_key if authorized?
end

def devices
present(user.devices) if options[:with_devices]
end

private

def authorized?
policy.show_private_info?
end

def policy
@policy ||= UserPolicy.new(current_user, user)
end
end
end
10 changes: 9 additions & 1 deletion app/policies/device_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,23 @@ def resolve
end
end

def show_private_info?
admin_or_owner?
end

def show?
if record.is_private?
update?
admin_or_owner?
else
true
end
end

def update?
admin_or_owner?
end

def admin_or_owner?
user.try(:is_admin?) || user == record.owner
end

Expand Down
3 changes: 3 additions & 0 deletions app/policies/user_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@ def update_password?
update?
end

def show_private_info?
update?
end
end

0 comments on commit d0245d6

Please sign in to comment.