Подпишитесь на рассылку о самых интересных материалах в мире веб-разработки :)

Как реализовать safe navigation operator


#1

Скажите, а почему я не могу опубликовать здесь код и описание проблемы? получаю ошибку “Извините, новые пользователи могут упоминать только 2 пользователей в сообщении.”, что за бред? Никого не упоминал, только код и description.


(Evgeniy) #2

Потому что форум воспринимает что-то в коде как меншен юзера. Используйте gist.github.com


(Kvokka) #3

переменные в коде переименуй и будет радость


#4

Ок, попробую через pastebin.

Здравствуйте, прошу помочь разобраться с возникшей проблемой. В трех словах описание: добавляю в информер, который вытаскивает с API погодной станции погоду для ряда городов, формочку, посредством которой возможно ввести идентификатор любого другого города и получить, таким образом, состояние погоды и для него. Это работает, с одним только “но”: если введенный ID некорректен, Rails вываливает ошибку. Что, в общем-то, вполне логично:

undefined method each for nil: NilClass

Фрагменты контроллера и модели:

Фрагмент контроллера
 
@array = [703448,6058560,1819729] # ID cities
 
if params[:q].nil?
  @cities = @array.join(",")
else
  @array = @array << params[:q]
  @cities = @array.join(",")
end
 
@lookup = Weather.call(@cities)
 
 
Фрагмент модели:
 
class Weather
  include HTTParty
  base_uri "http://api.openweathermap.org/data/2.5/group?appid=***********************
  format :json
 
  #call the api with HTTParty and parse the JSON response
  def self.call list_ids
    response = HTTParty.get(base_uri + '&id=' + list_ids)
    body = JSON.parse(response.body)
    list = body["list"]
  end

Т.е. если уже получили по GET из формы идентификатор города, то добавляем его в массив, который затем делаем строкой и с ним работаем, если нет - значит, работаем только с имеющимися по-дефолту ID.

В качестве решения, вероятно, оптимально было бы использовать fetch, также try или &, safe navigation operator, но не могу на данный момент это реализовать. Был бы благодарен за дельный совет. Попутно второй вопрос: мне рекомендуют логировать body и list output: “just p before list like p body and you will see the output in console”, также не понимаю, увы… вывел бы на страничку через inspect любую переменную контроллера, или из модели вот так: Rails.logger.debug response.body , но как вывести в консоль? Очень прошу объяснить чайнику, regards.


(Kvokka) #5

кусков кода не достаточно. но есть 2 очевидных пути: 1- забабахать rescue оттуда уже генерировать просто список всего (не самый секбюрный) ну и сохранить в кеше все города, что есть, и с ними сравнивать (кеш, чтоб не через одно место делать сразу и не переделывать). типа return render cities_url if (Array(params[:user_cities_ids]) - all_cities_ids).any?

ну и облагородить эту идею. было б больше кода, можно было б больше придумывать.


#6

Куски кода не проблема, попробую выложить здесь и полную версию, надеюсь, у меня получится, всего-то контроллер и модель (вьюха, полагаю, не в счет). Если нет, тогда на herokuapp…

Можно подробнее? с описанной методой у меня явно пробел. Вы считаете, что rescue - оптимальное средство решения описанной задачи? safe navigation operator здесь не катит?

controller

class TestController < ApplicationController
  def search
    @array = [703448,6058560,1819729] #524901
  if params[:q].nil?
   @cities = @array.join(",")
  else
       @array = @array << params[:q]
@cities = @array.join(",")
  end
    
    @lookup = Weather.call(@cities)
    @temp = Weather.max_value(Weather.make_hash(@lookup, "temp"))
    @pressure = Weather.max_value(Weather.make_hash(@lookup, "pressure"))
    @humidity = Weather.min_value(Weather.make_hash(@lookup, "humidity"))
    @clouds = Weather.min_value(Weather.cloud_hash(@lookup))
    @best = [@temp, @pressure, @humidity, @clouds]
  end
end

model

class Weather
  include HTTParty
  base_uri "http://api.openweathermap.org/data/2.5/group?appid=************************************&units=metric"
  format :json

  #call the api with HTTParty and parse the JSON response 
  def self.call list_ids
    response = HTTParty.get(base_uri + '&id=' + list_ids)
    body = JSON.parse(response.body)
    # Rails.logger.debug response.body 
    list = body["list"]
  end

  #create a hash to store city : temp values 
  def self.make_hash list, value
    hash = Hash.new
      list.each do |l|
        temp = l["main"][value]
        city = l["name"]
        hash[city] = temp
      end
    return hash
  end

  #create hash with city: cloud cover
  def self.cloud_hash list
    hash = Hash.new
      list.each do |l|
        cloud = l["clouds"]["all"]
        city = l["name"]
        hash[city] = cloud
      end
    return hash
  end

  #returns the highest value from a hash
  def self.max_value hash
    hash.max_by{|k,v| v}
  end  

  #returns the lowest value from a hash
  def self.min_value hash
    hash.min_by{ |k,v| v}
  end    

end

#7

Попробовать что ли для начала реализовать вашу идею о том, чтобы вести еще одно условие, ища вводимый id в массиве всех id… попробую, здесь не должно быть проблем.


(Kvokka) #8

во-первых, если в #search params == nil то он один фиг попадет в результат, грязно это. Во-вторых, ругань и, косяк идет от последней строки метода Weather::call , я бы его закончил чем-то вроде
body.fetch('list'){[]}.compact

методы def self.min_value hash не относятся к модели! это грязь, это методы хэша, которые надо либо убрать в core_ext либо сделать отдельный refine.

в методе self.cloud_hash list последняя строка лишняя.

короче, тут кода не много, а до чего докопаться еще есть.


#9

так в обоих методах последние строки лишние?

# hash[city] = temp

# hash[city] = cloud


(Kvokka) #10

нет, я про return hash. это тебе не js :wink:


#11

я понял, но если закоментировать четыре строчки (с указанными выше) а не две, то тоже пашет…


#12

Как думаешь, что здесь использовать для fetch в качестве альтернативы? по ряду причин (пробую, например, юзать здесь еще и гем sypex_geo, что вносит опять-таки ряд проблем) хотелось бы полностью застраховаться от несуществующего list, compact также не спасет.

body.fetch('list', '???'){[]}.compact


(Kvokka) #13

body&.fetch('list'){[]} || [] для руби 2.3+
ну или body.try(:fetch, 'list', []) || []
можно body.to_h.fetch 'list, []
можно через rescue работать (но это тупо медленнее)


#14

kvokka, походу ты был прав, а я ошибался -

body.fetch('list'){[]}.compact

полностью вылечивает в моем случае от “undefined method each for nil: NilClass”. Вероятно, кое-что иное привносило ругань, стоило подчистить - все заработало, даже строить массив из ID и сравнивать с введенным значением не пришлось, пашет без ошибок, в случае неполучения данных из API выводится пустая таблица, соответственно несложно проверить на пустоту и вывести варнинг.

Не подскажете, чем разнится метод

with_indifferent_access() public

от иных способов достать данные из JSON , не прибегая к циклам или итераторам? В чем плюсы использования этого метода?


(Kvokka) #15

есть такая альтернатива Hash в рельсе как ActiveSupport::HashWithIndifferentAccess
пример:

a={foo: :bar};
a[:foo] # => :bar 
a['foo'] # => nil
b=ActiveSupport::HashWithIndifferentAccess.new({foo: :bar})
b[:foo] # => :bar
b['foo'] # => :bar  

ну а в целом же API doc поможет быстрее форума. там же все это есть.
а еще лучше просто глянуть в исходники метода :wink:


#16

Есть некоторые вариации на тему.
В том виде, как задача описана в начале топика, она решена: пришедшие из формы данные, даже если некорректны, не крэшат приложение. Я опять попробую показать только фрагменты кода, так короче:

# controller (вот так, совсем по-простому)

class FormController < ApplicationController
  def weather
      
    @array = []
   # @array = [703448,6058560,1819729,524901]
  if params[:q].nil?
   @cities = @array.join(",")
  else
       @array = @array << params[:q]
@cities = @array.join(",")
  end
  @lookup = Form.call(@cities)
    @temp = Form.max_value(Form.make_hash(@lookup, "temp"))
    @pressure = Form.max_value(Form.make_hash(@lookup, "pressure"))
    @humidity = Form.min_value(Form.make_hash(@lookup, "humidity"))
    @clouds = Form.min_value(Form.cloud_hash(@lookup))
    @best = [@temp, @pressure, @humidity, @clouds]
#  model

class Form < ApplicationRecord
    
  include HTTParty
  
  base_uri "********************"
  format :json

  def self.call list_ids
    response = HTTParty.get(base_uri + '&id=' + list_ids)
    body = JSON.parse(response.body)
    body.fetch('list'){[]}.compact
  end

Если же пробую использовать два API вместо одного (сперва получаем lat/lon по ip, затем уже получаем погоду по координатам), то снова напарываемся на тот же самый nill, причем в этом случае fetch и compact не спасают. Не хочу сейчас даже заглядывать в лог, чтобы увидеть, какой именно API не возвращает ответ, теоретически это всегда может быть один из двух. Посоветуешь, где покопать?

controller # Вот такой вариант кода время от времени (нечасто, но бывает) приводит к ошибке 

class SypexgeoController < ApplicationController
  def index
      
      ip = request.remote_ip
      response = HTTParty.get('http://api.sypexgeo.net/json/' + ip)
      r = response.parsed_response.with_indifferent_access
     @cities = "lat=#{r[:city][:lat]}&lon=#{r[:city][:lon]}"
    @lookup = WeatherSxgeo.call(@cities)
    @temp = WeatherSxgeo.max_value(WeatherSxgeo.make_hash(@lookup, "temp"))
    @pressure = WeatherSxgeo.max_value(WeatherSxgeo.make_hash(@lookup, "pressure"))
    @humidity = WeatherSxgeo.min_value(WeatherSxgeo.make_hash(@lookup, "humidity"))
    @clouds = WeatherSxgeo.min_value(WeatherSxgeo.cloud_hash(@lookup))
    

  end
end
model # почти не отличается от первого варианта

class WeatherSxgeo < ApplicationRecord

  include HTTParty
  base_uri "***************************************"
  format :json
  
  
  def self.call list_ids
    response = HTTParty.get(base_uri + list_ids)
    body = JSON.parse(response.body)
    body.fetch('list'){[]}.compact
  end

#17

Да, и еще итерация во вьюхе:

<% @lookup.each do |l| %>
  <tr>
    <td><%= l["name"] %></td>
    <td><%= l["main"]["pressure"] %></td>
    <td><%= l["main"]["temp"] %></td>
    <td><%= l["main"]["humidity"] %></td>
    <td><%= l["clouds"]["all"] %></td>
    <td><%= l["wind"]["speed"] %></td>
    <td><%= l["wind"]["deg"] %></td>
    <td><%= l["weather"][0]["main"] %></td>
  </tr>

(Kvokka) #18

ну, во-первых, неплохо бы код, перед выкладыванием приводить в красивый вид. ибо это не удобно
причина проста- JSON.parse("null") # nil со всеми вытекающими
а эта строка должна выдавать всегда Hash
пмсм чтот вроде

  def response_body reload: false, reraise: false
    return @_response_body if !reload && @_response_body
    @_response_body = JSON.parse(response.body) || {}
  rescue => e
    reraise ? raise(e) : {}
  end

не проверял, но, суть должна быть ясна


#19

Попробую, спс. Но вроде и без того разобрался, откуда в основном лилась грязь: при написании гема sypex_geo и портировании алгоритма с PHP был “зацеплен” баг с бинарным поиском, в дальнейшем баг был пофиксен одним из пользователей GitHub, что опять-таки не дало, как мне представляется, кардинального улучшения. Отказ от гема sypex_geo и переход с локальной db на Rest API все упростил.