From 8b4fea702cb44431383982124ce73d60af189c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Serge=20H=C3=A4nni?= Date: Tue, 31 Jan 2023 17:37:09 +0100 Subject: [PATCH] Add support for bodies other than objects --- lib/ioki/apis/endpoints/show.rb | 6 +- lib/ioki/apis/passenger_api.rb | 6 +- lib/ioki/model/base.rb | 45 +++++++++- .../model/passenger/notification_settings.rb | 8 +- spec/ioki/endpoints/show_spec.rb | 80 ++++++++++++++++- spec/ioki/model/base_spec.rb | 85 +++++++++++++++++++ spec/ioki/passenger_api_spec.rb | 4 +- 7 files changed, 214 insertions(+), 20 deletions(-) diff --git a/lib/ioki/apis/endpoints/show.rb b/lib/ioki/apis/endpoints/show.rb index 514f0800..e087ee45 100644 --- a/lib/ioki/apis/endpoints/show.rb +++ b/lib/ioki/apis/endpoints/show.rb @@ -29,11 +29,7 @@ def call(client, args = [], options = {}) model = options[:model] if options[:model].is_a?(model_class) attributes, etag = model_params(client, args, options, model) - if model_class == Array - attributes - else - model_class.new(attributes, etag) - end + model_class.new(attributes, etag) end private diff --git a/lib/ioki/apis/passenger_api.rb b/lib/ioki/apis/passenger_api.rb index 6c9bea93..006c5e5c 100644 --- a/lib/ioki/apis/passenger_api.rb +++ b/lib/ioki/apis/passenger_api.rb @@ -93,19 +93,19 @@ class PassengerApi :notification_settings, path: 'notification_settings', base_path: [API_BASE_PATH], - model_class: Array + model_class: Ioki::Model::Passenger::NotificationSettings ), Endpoints::ShowSingular.new( :default_notification_settings, path: %w[passenger notification_settings defaults], base_path: [API_BASE_PATH], - model_class: Array + model_class: Ioki::Model::Passenger::NotificationSettings ), Endpoints::ShowSingular.new( :available_notification_settings, path: %w[passenger notification_settings available], base_path: [API_BASE_PATH], - model_class: Array + model_class: Ioki::Model::Passenger::NotificationSettings ), Endpoints.crud_endpoints( :notification_setting, diff --git a/lib/ioki/model/base.rb b/lib/ioki/model/base.rb index d4576471..8cac8917 100644 --- a/lib/ioki/model/base.rb +++ b/lib/ioki/model/base.rb @@ -60,13 +60,26 @@ def attribute_definitions .collect(&:class_instance_attribute_definitions) .reduce(&:merge) end + + def base(class_name, item_class_name: nil) + define_method :set_base_class do + @_base_class_name = class_name + @_item_class_name = item_class_name + end + end end attr_accessor :_raw_attributes, :_attributes, :_etag - def initialize(raw_attributes = {}, etag = nil) + def initialize(raw_attributes = base_class.new, etag = nil) + set_base_class if respond_to?(:set_base_class) + @_initial_attributes = raw_attributes - @_raw_attributes = (raw_attributes || {}).transform_keys(&:to_sym) + @_raw_attributes = if raw_attributes.is_a?(Hash) + (raw_attributes || {}).transform_keys(&:to_sym) + else + raw_attributes || base_class.new + end @_etag = etag reset_attributes! end @@ -88,6 +101,26 @@ def attributes(**attributes) @_attributes end + def data + return attributes if base_class == Hash + + case base_class.to_s + when 'Array' + _raw_attributes.map do |item| + class_name = class_name_from_value_type(@_item_class_name, item) + model_class = constantize_in_module(class_name) + + model_class.new(item) + end + else + _raw_attributes + end + end + + def base_class + Object.const_get(@_base_class_name || 'Hash') + end + def type_cast_attribute_value(attribute, value) type = self.class.attribute_definitions.dig(attribute, :type) class_name = self.class.attribute_definitions.dig(attribute, :class_name) @@ -136,6 +169,12 @@ def type_cast_attribute_value(attribute, value) # may get overridden in hard ways by subclasses. This default # implementation should bring us a long way. def serialize(usecase = :read) + if !@_raw_attributes.is_a?(Hash) + return @_raw_attributes.map { |object| object.serialize(usecase) } if @_raw_attributes.is_a?(Array) + + return @_raw_attributes + end + self.class.attribute_definitions.each_with_object({}) do |(attribute, definition), data| value = public_send(attribute) @@ -173,6 +212,8 @@ def ==(other) def reset_attributes! @_attributes = {} + return if !@_raw_attributes.is_a?(Hash) + self.class.attribute_definitions.each do |attribute, definition| value = @_raw_attributes.key?(attribute) ? @_raw_attributes[attribute] : definition[:default] public_send("#{attribute}=", value) diff --git a/lib/ioki/model/passenger/notification_settings.rb b/lib/ioki/model/passenger/notification_settings.rb index ffaeb546..0c91e9a7 100644 --- a/lib/ioki/model/passenger/notification_settings.rb +++ b/lib/ioki/model/passenger/notification_settings.rb @@ -4,13 +4,7 @@ module Ioki module Model module Passenger class NotificationSettings < Base - attribute :root, on: [:read, :update], type: :array - - def serialize(usecase = :read) - serialized_data = super(usecase) - - serialized_data[:root] - end + base 'Array', item_class_name: 'NotificationSetting' end end end diff --git a/spec/ioki/endpoints/show_spec.rb b/spec/ioki/endpoints/show_spec.rb index ca170fa3..986bf7b0 100644 --- a/spec/ioki/endpoints/show_spec.rb +++ b/spec/ioki/endpoints/show_spec.rb @@ -42,7 +42,7 @@ before { model._etag = 'ETAG' } - it "passes on model'the s etag to the request method via headers" do + it "passes on the model's etag to the request method via headers" do expect(client).to receive(:request).with( url: url, headers: { 'If-None-Match': model._etag }, @@ -52,4 +52,82 @@ endpoint.call(client, ['0815'], { model: model, params: params }) end end + + context 'with an array based response' do + let(:parsed_data) do + { 'data' => [{ id: 'ride', name: 'ride', channels: ['sms'], type: 'notification_setting' }] } + end + let(:response) do + instance_double( + Faraday::Response, + 'response double', + status: 200, + body: parsed_data, + headers: { etag: 'ETAG' } + ) + end + let(:endpoint) do + described_class.new( + 'notification_settings', + base_path: ['base'], + model_class: Ioki::Model::Passenger::NotificationSettings + ) + end + + it 'parses data and etag' do + expect(client).to receive(:request).and_return([parsed_data, response]) + + notification_settings = endpoint.call(client, ['0815']) + + expect(notification_settings).to be_a Ioki::Model::Passenger::NotificationSettings + expect(notification_settings._etag).to eq 'ETAG' + expect(notification_settings._raw_attributes).to eq parsed_data['data'] + expect(notification_settings.data).to eq [ + Ioki::Model::Passenger::NotificationSetting.new( + id: 'ride', + name: 'ride', + channels: ['sms'], + type: 'notification_setting' + ) + ] + end + end + + context 'with a string response' do + let(:model_class) do + Class.new(Ioki::Model::Base) do + base 'String' + end + end + let(:parsed_data) do + { 'data' => 'test string' } + end + let(:response) do + instance_double( + Faraday::Response, + 'response double', + status: 200, + body: parsed_data, + headers: { etag: 'ETAG' } + ) + end + let(:endpoint) do + described_class.new( + 'example_class', + base_path: ['base'], + model_class: model_class + ) + end + + it 'parses data and etag' do + expect(client).to receive(:request).and_return([parsed_data, response]) + + response = endpoint.call(client, ['0815']) + + expect(response).to be_a model_class + expect(response._etag).to eq 'ETAG' + expect(response._raw_attributes).to eq parsed_data['data'] + expect(response.data).to eq 'test string' + end + end end diff --git a/spec/ioki/model/base_spec.rb b/spec/ioki/model/base_spec.rb index 797ea602..8148714e 100644 --- a/spec/ioki/model/base_spec.rb +++ b/spec/ioki/model/base_spec.rb @@ -222,6 +222,7 @@ expect(model.foo).to eq('21') expect(model._attributes).to eq(foo: '21') expect(model._raw_attributes).to eq(foo: 21, baz: 42) + expect(model.data).to eq(foo: '21') end end end @@ -732,4 +733,88 @@ end end end + + describe 'array body' do + let(:example_class) { Ioki::Model::Passenger::NotificationSettings } + + it 'allows an array' do + model = example_class.new([]) + + expect(model.serialize).to eq [] + end + + context 'with hash objects in array' do + let(:attributes) do + [ + { + 'id' => 'ride', + 'name' => 'ride', + 'channels' => %w[sms email], + 'type' => 'notification_setting' + }, + { + 'id' => 'booking', + 'name' => 'booking', + 'channels' => %w[sms], + 'type' => 'notification_setting' + } + ] + end + + it 'parses objects in array' do + expect(model.data).to eq [ + Ioki::Model::Passenger::NotificationSetting.new( + id: 'ride', + name: 'ride', + channels: %w[sms email], + type: 'notification_setting' + ), + Ioki::Model::Passenger::NotificationSetting.new( + id: 'booking', + name: 'booking', + channels: %w[sms], + type: 'notification_setting' + ) + ] + end + end + + it 'serializes objects in array' do + model = example_class.new([ + Ioki::Model::Passenger::NotificationSetting.new( + name: 'ride', + channels: %w[sms email] + ), + Ioki::Model::Passenger::NotificationSetting.new( + name: 'booking', + channels: %w[sms] + ) + ]) + + expect(model.serialize(:update)).to eq [ + { + name: 'ride', + channels: %w[sms email] + }, + { + name: 'booking', + channels: %w[sms] + } + ] + end + end + + describe 'string body' do + let(:example_class) do + Class.new(Ioki::Model::Base) do + base 'String' + end + end + + it 'allows a string' do + model = example_class.new('test') + + expect(model.serialize).to eq 'test' + end + end end diff --git a/spec/ioki/passenger_api_spec.rb b/spec/ioki/passenger_api_spec.rb index e3937ba6..068d295e 100644 --- a/spec/ioki/passenger_api_spec.rb +++ b/spec/ioki/passenger_api_spec.rb @@ -381,7 +381,7 @@ expect(params[:url].to_s).to eq('passenger/notification_settings/available') [result_with_array_data, full_response] end - expect(passenger_client.available_notification_settings).to be_a Array + expect(passenger_client.available_notification_settings).to be_a Ioki::Model::Passenger::NotificationSettings end end @@ -391,7 +391,7 @@ expect(params[:url].to_s).to eq('passenger/notification_settings/defaults') [result_with_array_data, full_response] end - expect(passenger_client.default_notification_settings).to be_a Array + expect(passenger_client.default_notification_settings).to be_a Ioki::Model::Passenger::NotificationSettings end end