From 24378b150d61d9b431ad7c986b9b914c50cb2bca Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sun, 13 Jul 2025 12:50:24 +0200 Subject: [PATCH] Add user serializer and update CHANGELOG.md --- CHANGELOG.md | 33 ++++++++ app/controllers/api/v1/users_controller.rb | 2 +- app/serializers/api/user_serializer.rb | 44 ++++++++++ app/services/users/safe_settings.rb | 12 ++- spec/requests/api/v1/users_spec.rb | 22 ++++- spec/serializers/api/user_serializer_spec.rb | 85 ++++++++++++++++++++ 6 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 app/serializers/api/user_serializer.rb create mode 100644 spec/serializers/api/user_serializer_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index a495db16..93771c6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,39 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Notification about Photon API load is now disabled. - All distance values are now stored in the database in meters. Conversion to user's preferred unit is done on the fly. - Every night, Dawarich will try to fetch names for places and visits that don't have them. #1281 #902 #583 #212 +- User settings are now being serialized in a more consistent way. `GET /api/v1/users/me` now returns the following data structure: +```json +{ + "user": { + "email": "test@example.com", + "theme": "light", + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z", + "settings": { + "maps": { + "url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", + "name": "Custom OpenStreetMap", + "distance_unit": "km" + }, + "fog_of_war_meters": 51, + "meters_between_routes": 500, + "preferred_map_layer": "Light", + "speed_colored_routes": false, + "points_rendering_mode": "raw", + "minutes_between_routes": 30, + "time_threshold_minutes": 30, + "merge_threshold_minutes": 15, + "live_map_enabled": false, + "route_opacity": 0.3, + "immich_url": "https://persistence-test-1752264458724.com", + "photoprism_url": "", + "visits_suggestions_enabled": true, + "speed_color_scale": "0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300", + "fog_of_war_threshold": 5 + } + } +} +``` ## Fixed diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb index 4fbb3f60..810eb55a 100644 --- a/app/controllers/api/v1/users_controller.rb +++ b/app/controllers/api/v1/users_controller.rb @@ -2,6 +2,6 @@ class Api::V1::UsersController < ApiController def me - render json: { user: current_api_user } + render json: Api::UserSerializer.new(current_api_user).call end end diff --git a/app/serializers/api/user_serializer.rb b/app/serializers/api/user_serializer.rb new file mode 100644 index 00000000..d3e89dfe --- /dev/null +++ b/app/serializers/api/user_serializer.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +class Api::UserSerializer + def initialize(user) + @user = user + end + + def call + { + user: { + email: user.email, + theme: user.theme, + created_at: user.created_at, + updated_at: user.updated_at, + settings: settings, + } + } + end + + private + + attr_reader :user + + def settings + { + maps: user.safe_settings.maps, + fog_of_war_meters: user.safe_settings.fog_of_war_meters.to_i, + meters_between_routes: user.safe_settings.meters_between_routes.to_i, + preferred_map_layer: user.safe_settings.preferred_map_layer, + speed_colored_routes: user.safe_settings.speed_colored_routes, + points_rendering_mode: user.safe_settings.points_rendering_mode, + minutes_between_routes: user.safe_settings.minutes_between_routes.to_i, + time_threshold_minutes: user.safe_settings.time_threshold_minutes.to_i, + merge_threshold_minutes: user.safe_settings.merge_threshold_minutes.to_i, + live_map_enabled: user.safe_settings.live_map_enabled, + route_opacity: user.safe_settings.route_opacity.to_f, + immich_url: user.safe_settings.immich_url, + photoprism_url: user.safe_settings.photoprism_url, + visits_suggestions_enabled: user.safe_settings.visits_suggestions_enabled?, + speed_color_scale: user.safe_settings.speed_color_scale, + fog_of_war_threshold: user.safe_settings.fog_of_war_threshold + } + end +end diff --git a/app/services/users/safe_settings.rb b/app/services/users/safe_settings.rb index 47548983..308121e5 100644 --- a/app/services/users/safe_settings.rb +++ b/app/services/users/safe_settings.rb @@ -45,7 +45,9 @@ class Users::SafeSettings photoprism_api_key: photoprism_api_key, maps: maps, distance_unit: distance_unit, - visits_suggestions_enabled: visits_suggestions_enabled? + visits_suggestions_enabled: visits_suggestions_enabled?, + speed_color_scale: speed_color_scale, + fog_of_war_threshold: fog_of_war_threshold } end # rubocop:enable Metrics/MethodLength @@ -118,4 +120,12 @@ class Users::SafeSettings def visits_suggestions_enabled? settings['visits_suggestions_enabled'] == 'true' end + + def speed_color_scale + settings['speed_color_scale'] + end + + def fog_of_war_threshold + settings['fog_of_war_threshold'] + end end diff --git a/spec/requests/api/v1/users_spec.rb b/spec/requests/api/v1/users_spec.rb index 3075a94f..b1669b39 100644 --- a/spec/requests/api/v1/users_spec.rb +++ b/spec/requests/api/v1/users_spec.rb @@ -7,12 +7,28 @@ RSpec.describe 'Api::V1::Users', type: :request do let(:user) { create(:user) } let(:headers) { { 'Authorization' => "Bearer #{user.api_key}" } } - it 'returns http success' do + it 'returns success response' do get '/api/v1/users/me', headers: headers expect(response).to have_http_status(:success) - expect(response.body).to include(user.email) - expect(response.body).to include(user.id.to_s) + end + + it 'returns only the keys and values stated in the serializer' do + get '/api/v1/users/me', headers: headers + + json = JSON.parse(response.body, symbolize_names: true) + + expect(json.keys).to eq([:user]) + expect(json[:user].keys).to match_array( + %i[email theme created_at updated_at settings] + ) + expect(json[:user][:settings].keys).to match_array(%i[ + maps fog_of_war_meters meters_between_routes preferred_map_layer + speed_colored_routes points_rendering_mode minutes_between_routes + time_threshold_minutes merge_threshold_minutes live_map_enabled + route_opacity immich_url photoprism_url visits_suggestions_enabled + speed_color_scale fog_of_war_threshold + ]) end end end diff --git a/spec/serializers/api/user_serializer_spec.rb b/spec/serializers/api/user_serializer_spec.rb new file mode 100644 index 00000000..178c64e0 --- /dev/null +++ b/spec/serializers/api/user_serializer_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Api::UserSerializer do + describe '#call' do + subject(:serializer) { described_class.new(user).call } + + let(:user) { create(:user, email: 'test@example.com', theme: 'dark') } + + it 'returns JSON with correct user attributes' do + expect(serializer[:user][:email]).to eq(user.email) + expect(serializer[:user][:theme]).to eq(user.theme) + expect(serializer[:user][:created_at]).to eq(user.created_at) + expect(serializer[:user][:updated_at]).to eq(user.updated_at) + end + + it 'returns settings with expected keys and types' do + settings = serializer[:user][:settings] + expect(settings).to include( + :maps, + :fog_of_war_meters, + :meters_between_routes, + :preferred_map_layer, + :speed_colored_routes, + :points_rendering_mode, + :minutes_between_routes, + :time_threshold_minutes, + :merge_threshold_minutes, + :live_map_enabled, + :route_opacity, + :immich_url, + :photoprism_url, + :visits_suggestions_enabled, + :speed_color_scale, + :fog_of_war_threshold + ) + end + + context 'with custom settings' do + let(:custom_settings) do + { + 'fog_of_war_meters' => 123, + 'meters_between_routes' => 456, + 'preferred_map_layer' => 'Satellite', + 'speed_colored_routes' => true, + 'points_rendering_mode' => 'cluster', + 'minutes_between_routes' => 42, + 'time_threshold_minutes' => 99, + 'merge_threshold_minutes' => 77, + 'live_map_enabled' => false, + 'route_opacity' => 0.75, + 'immich_url' => 'https://immich.example.com', + 'photoprism_url' => 'https://photoprism.example.com', + 'visits_suggestions_enabled' => 'false', + 'speed_color_scale' => 'rainbow', + 'fog_of_war_threshold' => 5, + 'maps' => { 'distance_unit' => 'mi' } + } + end + + let(:user) { create(:user, settings: custom_settings) } + + it 'serializes custom settings correctly' do + settings = serializer[:user][:settings] + expect(settings[:fog_of_war_meters]).to eq(123) + expect(settings[:meters_between_routes]).to eq(456) + expect(settings[:preferred_map_layer]).to eq('Satellite') + expect(settings[:speed_colored_routes]).to eq(true) + expect(settings[:points_rendering_mode]).to eq('cluster') + expect(settings[:minutes_between_routes]).to eq(42) + expect(settings[:time_threshold_minutes]).to eq(99) + expect(settings[:merge_threshold_minutes]).to eq(77) + expect(settings[:live_map_enabled]).to eq(false) + expect(settings[:route_opacity]).to eq(0.75) + expect(settings[:immich_url]).to eq('https://immich.example.com') + expect(settings[:photoprism_url]).to eq('https://photoprism.example.com') + expect(settings[:visits_suggestions_enabled]).to eq(false) + expect(settings[:speed_color_scale]).to eq('rainbow') + expect(settings[:fog_of_war_threshold]).to eq(5) + expect(settings[:maps]).to eq({ 'distance_unit' => 'mi' }) + end + end + end +end