Я определил следующие классы:
магазин.рб:
class Shop
field: :reputation, Float
embeds_one :location, class_name: "Location"
accepts_nested_attributes_for :location
end
location.rb:
class Location
include Mongoid::Document
field :address, type: String
field :coordinates, type: Array
field :place_id, type: String
validate :coordinates_must_be_pair_of_float
private
def coordinates_must_be_pair_of_float
unless coordinates.is_a?(Array) && coordinates.size == 2
errors.add(:coordinates, "must be an array with exactly two elements")
return
end
coordinates.each do |coord|
unless coord.is_a?(Float)
errors.add(:coordinates, "must contain only integers")
return
end
end
end
end
В shop_controller.rb:
def create
shop = Shop.new(shop_params)
if shop.save
render json: { message: 'Shop created successfully', shop: shop }, status: :created
else
render json: { errors: shop.errors.full_messages }, status: :unprocessable_entity
end
end
private
def shop_params
params.require(:shop).permit(
:reputation,
location_attributes: [:address, :place_id, coordinates: []],
)
end
Наконец, в shop_spect.rb
:
let(:location) { { address: "St. My Street", coordinates: [-100.0, 100.0], place_id: "12345" } }
describe "POST /shop" do
it "creates a new shop" do
shop_data = {
reputation: 800
location_attributes: location,
}
post "/shop", params: { shop: shop_data }
if response.status == 422
errors = JSON.parse(response.body)["errors"]
puts "Validation errors: #{errors.join(', ')}" # Display the error messages
end
expect(response).to have_http_status(201)
Когда я делаю POST, используя завиток, как показано ниже:
curl -X POST \
-H "Content-Type: application/json" \
-d '{
"shop": {
"reputation": 800,
"location_attributes": {
"address": "My Street",
"coordinates": [-100.0, 100.0],
"place_id": "12345"
},
}
}' \
"http://localhost:3000/shop"
Все работает нормально, но тест провалился с кодом ошибки 422
, т. е. экземпляр не удалось сохранить. Через некоторое время я понял проблему: массив координат не обрабатывался так же, как обрабатывалась репутация; тип значений, содержащихся в массиве координат, был: Кодировка, UTF8.
.
Также это значение параметров в тесте:
{:shop=>{:price=>800, :location_attributes=>{:address=>"My Street", :coordinates=>[-100.0, 100.0], :place_id=>"12345"}}}
и это значение параметров в контроллере:
{"shop"=>{"reputation"=>"800", "location_attributes"=>{"address"=>"My Street", "coordinates"=>["-100.0", "100.0"], "place_id"=>"12345"} }, "price"=>"800"}, "controller"=>"advert", "action"=>"create"}
наконец, это значение параметров в контроллере, когда я делаю запрос с помощью curl
:
{"shop"=>{"reputation"=>800, "location_attributes"=>{"address"=>"My Street", "coordinates"=>[-100.0, 100.0], "place_id"=>"12345"}}, "controller"=>"advert", "action"=>"create"}
Очевидно, что теги преобразуются в строки, но почему целые числа и числа с плавающей запятой также преобразуются в строки при использовании post
в rspecs
?
Таким образом, проверка в классе местоположения не удалась. Чтобы решить эту проблему, мне пришлось изменить контроллер следующим образом:
shop_controller.rb
:
def create
shop = Shop.new(shop_params)
shop.location.coordinates.map!(&:to_f)
if shop.save
render json: { message: 'Shop created successfully', shop: shop }, status: :created
else
render json: { errors: shop.errors.full_messages }, status: :unprocessable_entity
end
end
private
def shop_params
params.require(:shop).permit(
:reputation,
location_attributes: [:address, :place_id, coordinates: []],
)
end
Я не понимаю, почему это происходит. Почему анализатор интерпретирует содержимое массива как данные в кодировке UTF8, а не как значения с плавающей запятой, как это происходит с полем репутации?
И еще, есть ли способ определить shop_params
? Почему следующее определение неверно:
def shop_params
params.require(:shop).permit(
:reputation,
location_attributes: [:address, :place_id, :coordinates],
)
end
@engineersmnky, я ответил на вопрос
Я думаю, что уже знаю, что происходит и как это исправить, но хочу продолжить расследование.
Вам следует устранить очевидную проблему field: :reputation
, вызывающую синтаксическую ошибку, и убедиться, что код вашего примера действительно работает, чтобы нам не пришлось тратить время.
Но почему целые числа и числа с плавающей запятой также преобразуются в строки при использовании post в rspecs?
Это имеет мало общего с RSpec.
В вашей спецификации вы на самом деле не отправляете запрос JSON, поскольку по умолчанию для метода post
используется application/x-www-form-urlencoded
(который в Rails рассматривается как формат :html).
Для отправки JSON используйте:
post "/shop", params: { shop: shop_data }, format: :json
На самом деле это помощник, предоставляемый базовым ActionDispatch::IntegrationTest, который RSpec просто оборачивает.
Причина, по которой вы теперь получаете строки, заключается в том, что параметры данных формы HTTP на самом деле не типизированы. Это просто пары ключей и значений в виде строк.
Более того, ваш контроллер на самом деле не ограничивает формат запроса JSON, что позволяет этой ошибке проскользнуть. Я бы использовал MimeResponds, чтобы вместо этого получить исключение ActionController::UnknownFormat
.
class ShopsController < ApplicationController
# ...
def create
shop = Shop.new(shop_params)
# don't do this - it's just silly to make the controller fix
# bad modeling
# shop.location.coordinates.map!(&:to_f)
# this will raise if the client requests HTML
respond_to :json do
if shop.save
render json: { message: 'Shop created successfully', shop: shop }, status: :created
else
render json: { errors: shop.errors.full_messages }, status: :unprocessable_entity
end
end
end
end
Использование типа массива — просто плохая идея. Я бы просто определил два поля типа с плавающей запятой, поскольку это намного менее шатко и дает вам приведение типов и два отдельных атрибута, так что вы действительно можете получить lat или lng в своем коде, не вытаскивая их из массива.
class Location
include Mongoid::Document
field :address, type: String
field :place_id, type: String
field :latitude, type: Float
field :longitude, type: Float
validates :longitude, :latitude, presence: true,
numericality: true
# Sets the latitude and longitude from an array or list
def coordinates=(*args)
self.latitude, self.longitude = *args.flatten
end
def coordinates
[self.latitude, self.longitude]
end
end
Если вы действительно хотите принять параметр как массив, вам необходимо внести его в белый список как таковой, поскольку массивы не являются разрешенным скалярным типом.
def shop_params
params.require(:shop).permit(
:reputation,
location_attributes: [
:address,
:place_id,
coordinates: []
]
)
end
Если ОП действительно хочет coordinates
в качестве Array
, тогда def coordinates=(value); write_attribute(:coordinates,value.map(&:to_f));end
тоже подойдет.
@engineersmnky, ты мог бы, но в этом нет никаких преимуществ.
@engineersmnky, ты прав. Я исправил пример.