Проблема с правильным отображением моего API-вызова в Rails в представлениях (с использованием HTTParty)

Я использую гем HTTParty в Rails, чтобы сделать вызов API, и он был успешным. Я не получаю никаких ошибок, однако после определения переменных в моем контроллере я не могу заставить свои циклы что-либо производить.

Если я наберу ‹%= @variable %>, он покажет только тот объект Active Record, который я ожидал. Если я сделаю то же самое, но включу variable.name, он предоставит строку, но не совсем то, на что я надеялся. Я просмотрел несколько руководств по HTTParty и провел много исследований API, но, похоже, не могу понять это. Я пытаюсь получить список закусок из внешнего API. Вот что у меня есть (некоторые вещи, на мой взгляд, просто для проверки).

Вызов API (выношу в отдельную папку services):

    class SnackAPI
  include HTTParty
  base_uri 'https://api-snacks.nerderylabs.com/v1/'
  SNACK_ACCESS = "/snacks?ApiKey=#{ENV['SNACK_API_KEY']}"

  def get_snacks
    response = self.class.get(SNACK_ACCESS)
    JSON.parse(response.body)
  end
end

Первый раз публикую фрагменты кода, поэтому не знаю, как публиковать с правильным отступом, но в моем приложении это правильно.

Контроллер:

class SnacksController < ApplicationController
  def index
    @snacks = Snack.all
    @permanent_snacks = Snack.where(optional: false)
    @optional_snacks = Snack.where(optional: true)
  end
end

Модель:

    class Snack < ApplicationRecord
  validates :name, presence: true
  validates_uniqueness_of :name
end

Просмотр (index.html.erb) в папке закусок:

    <h1> Welcome to SnaFoo! </h1>

<!-- I am attempting to get these loops to display each snack item included in the API but nothing appears -->

<% @permanent_snacks.each do |snack| %>
  <%= snack.name %>
<% end %>

<% @optional_snacks.each do |snack| %>
  <%= snack.name %>
<% end %>

<%= @snacks %>
<%= @optional_snacks %>
<%= @permanent_snacks %>

<br  />

Snack with name paramater (to test):

<%= @snacks.name %>

<!-- So it recognizes the fields and datatypes -->

Результаты в представлении: сами объекты (для каждого. Я ожидал этого, это было просто для проверки того, что API извлекает информацию) Для ‹%= @snacks.name %> он выводит Snack, поэтому он, по крайней мере, распознает мои поля и типы данных в моей схеме. Основная проблема заключается в том, чтобы заставить циклы работать и отображать каждую закуску в API.

Я часами пытался решить эту проблему самостоятельно, но я в отчаянии и был бы признателен за любую помощь, которая могла бы указать мне правильное направление.

Если схема необходима/полезна, чтобы помочь решить эту проблему, я тоже могу опубликовать ее.

Вывод терминала, когда я загружаю страницу (обычно код для Ubuntu, но я кодирую это для Windows из-за некоторых проблем с моим разделом:

Начат GET "/" для 10.0.2.2 в 2017-09-11 21:49:49 +0000 Невозможно отобразить консоль из 10.0.2.2! Разрешенные сети: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 Обработка SnacksController#index как HTML Отрисовка закусок/index.html.erb в макетах/приложениях Загрузка закусок (0,3 мс) SELECT "snacks".* FROM "закуски" ГДЕ "закуски"."необязательный" = $1 [["необязательный", "f"]] Загрузка закуски (0,5 мс) ВЫБЕРИТЕ "закуски".* ИЗ "закуски" ГДЕ "закуски"."необязательный" = $1 [["необязательный", "t"]] Отрендеренные закуски/index.html.erb в макетах/приложении (3,5 мс) Завершено 200 OK за 401 мс (Просмотры: 374,1 мс | ActiveRecord: 0,8 мс)


person David Stonikas    schedule 11.09.2017    source источник


Ответы (1)


Начните с исправления клиента:

# place this in /lib or app/clients as it is not a service object.
# app/clients/snack_api.rb
class SnackAPI
  include HTTParty
  base_uri 'https://api-snacks.nerderylabs.com/v1/'
  format :json

  def initialize(*opts)
    @options = opts.reverse_merge({
      ApiKey: ENV['SNACK_API_KEY']
    })
  end
  def get_snacks
    response = self.class.get('/snacks', @options)
  end
end

Используйте format :json вместо разбора ответа JSON вручную. Это очень важно, поскольку, если API выдает ошибку и возвращает пустой ответ, JSON.parse взрывается:

irb(main):002:0> JSON.parse('')
JSON::ParserError: 745: unexpected token at ''

Если вы просто хотите отображать статьи прямо из API, вам не нужна модель:

class SnacksController < ApplicationController
  def index
    response = SnackAPI.get_snacks

    if response.success?
      @snacks = response[:snacks]
    else
      flash.now[:error] = "Could not fetch snacks"
      @snacks = []
    end
  end
end

В противном случае создайте объект службы для использования API:

# app/services/snack_import_service
class SnackImportService

  attr_accessor :client

  def intialize(client = nil, **opts)
    # this is a trick that lets you inject a spy or double in tests
    @client = client || SnackAPI.new(opts)
  end

  def perform
    response = client.get_snacks
    # Remember that HTTP requests can and will fail
    if response.success?
      response[:snacks].map do |data|
        Snack.find_or_create_by!(name: data[:name])
      end
    else
      Rails.logger.error "SnackAPI request was unsuccessful #{response.code}"
    end
  end
end

Иметь это как отдельный сервисный объект от клиента — хорошая идея, поскольку он придерживается принципа единой ответственности и позволяет даже извлекать клиент в гем как повторно используемый компонент, который отделен от логики вашего приложения.

class SnacksController < ApplicationController
  def index
    @snacks = SnackImportService.new.perform
    @permanent_snacks = Snack.where(optional: false)
    @optional_snacks = Snack.where(optional: true)
  end
end
person max    schedule 12.09.2017
comment
Большое спасибо за всю информацию. Один вопрос об используемом методе reverse_merge. Я не мог найти подробных сведений о том, что именно он делает в целом, а тем более в этом случае. Не могли бы вы объяснить, что он делает в клиенте? - person David Stonikas; 12.09.2017
comment
Также просто для уточнения: либо поместите вызов API в клиент или библиотеку, либо сделайте его службой. Или оба? Просто любопытно, поскольку я не вижу вызова get в SnackImportService. Еще раз большое спасибо. Я не видел, чтобы это делалось так во всех моих поисках ответов и руководств. Я проверю это как можно скорее - person David Stonikas; 12.09.2017
comment
Извините за спам в комментариях, последнее, что вылетело из головы. Как только я реализую вызов API таким образом, мои циклы для показа закусок должны работать правильно? - person David Stonikas; 12.09.2017
comment
Опять другой вопрос. Не могли бы вы уточнить свой комментарий в SnackImportService (# это уловка, которая позволяет вам ввести шпиона или двойника в тестах). Еще раз спасибо, надеюсь, я смогу начать собирать эти вещи самостоятельно - person David Stonikas; 13.09.2017
comment
Хорошо, поэтому я реализовал каждое из этих изменений, за исключением вашего изменения в контроллере, поскольку мне нужно кое-что сделать позже с дополнительными закусками, и все еще ничего не отображается из циклов в моем представлении. Также в моем контроллере с @snacks = SnackImportService.new.perform он возвращается с ошибкой, говорящей о том, что неопределенный метод «выполнить». Нужно ли мне помещать require './services/snack_import_service' в мой контроллер или размещать какие-либо операторы require в любом месте, учитывая, что я помещаю классы за пределы готового каталога rails? Основная проблема в том, что закуски по-прежнему ничего не показывают. - person David Stonikas; 13.09.2017
comment
.reverse_merge похоже на слияние, но наоборот. Это действительно удобно для заполнения переданных параметров значениями по умолчанию. api.rubyonrails.org/classes/Hash.html#method-i- обратное_слияние - person max; 13.09.2017
comment
response = client.get_snacks - это первая строка - person max; 13.09.2017
comment
По сути, это просто внедрение зависимостей — внедрив клиента, вы можете заменить его заглушкой в ​​модульном тесте, не копаясь во внутренностях класса. - person max; 13.09.2017
comment
Если вы поместите файлы в /app/services, то rails автоматически загрузит файлы, поэтому они вам не нужны. Если файл не был загружен, вы получите ошибку aUninitialized Constant. Скорее, возможно, вам следует попытаться решить проблему и выполнить базовую отладку. Вы можете использовать byebug, чтобы установить точку останова, а затем проверить переменную @snacks. Также обратите внимание, что я понятия не имею, что возвращает API - вам нужно на самом деле доработать код самостоятельно. - person max; 13.09.2017
comment
После выполнения проверки закусок на выходе был объект активной записи со всеми полями в моей схеме, установленными как нулевые. Вот пример того, что содержит API Sample: [ { "id": 6, "name": "Gorp", "optional": true, "purchaseLocations": "John's Grocery, Corner C-Store", "purchaseCount": 0, "lastPurchaseDate": null }, { "id": 17, "name": "Bacon Jerky", "optional": false, "purchaseLocations": "John's Grocery", "purchaseCount": 542, "lastPurchaseDate": "9/11/2017" } ] - person David Stonikas; 13.09.2017
comment
Я хочу понять это самостоятельно, но я нахожусь в неизведанных водах, поэтому ищу небольшую помощь в движении в правильном направлении. Постоянные закуски имеют необязательные: ложные и необязательные закуски как истинные. Я создал миграцию, чтобы добавить поля вручную, поэтому я полагаю, что это может сыграть роль, поскольку я предполагаю, что API должен предоставлять правильные данные. После отката всех миграций у меня теперь ничего нет в моей схеме, и я удалил модель Snack, однако, похоже, мне может понадобиться модель Snack или что-то в этом роде, так как теперь я получаю неинициализированную константу для Snack. - person David Stonikas; 13.09.2017
comment
Я также буду выполнять пост-запрос для обновления API в качестве функции предложения закуски, используя раскрывающийся список, а также текстовое поле с кнопкой отправки. Я думаю, как только я пойму это, я смогу реализовать это, надеюсь. Сейчас я просто хочу сосредоточиться на том, чтобы содержимое API отображало то, что мне нужно. Я понимаю, что я совсем новичок, когда дело доходит до использования API, поэтому спасибо, что миритесь с моим отсутствием опыта. И я все еще не понимаю, нужен ли мне как вызов API в клиенте, так и SnackImportService, или мне нужен только один или другой? - person David Stonikas; 13.09.2017
comment
Я читал Джобса в руководствах по рельсам. Я никогда не использовал их раньше, но, похоже, они могут быть полезны. Я действительно потерян в этом пункте в противном случае. Моя схема принимает данные API, поэтому @snacks.inspect просто приводит к нулю, поскольку в моей базе данных нет закусок. Кажется, я не могу понять, как получить SELECT "snacks".*FROM "snacks" WHERE "snacks"."optional" = true/false из закусок API, а не несуществующие в базе данных. Также не уверен, почему, но в консоли оператор select загружает закуску, где "optional" = $1 [["optional","f"]] - person David Stonikas; 13.09.2017
comment
Хотел бы я заплатить тебе за это. Если у вас есть криптовалюта/кошельки для них, я был бы рад. В нынешнем виде, учитывая, что я не могу заставить GET работать, я чувствую, что мне понадобится руководство по отправке новых закусок в API, поскольку это должно быть сделано через текстовую форму, которую я полагаю, мне нужно будет сделать действие формы, поэтому я рассмотрю это подробнее. Но для контроллера у меня есть фрагмент кода, и мне было интересно, не могли бы вы сказать мне, будет ли он работать должным образом с учетом моей настройки. Я опубликую в следующем комментарии, потому что это не позволяет мне добавить достаточно символов вместе с этим. Итак, вот оно - person David Stonikas; 13.09.2017
comment
` def post_snack(snacks) закуска.каждый do |snack| options = {body {name:snack.name, location:snack.purchase_location, last_purchased: закуски.last_purchased}.to_json, headers: { 'Content-Type' =› 'application/json'}} response = self.class.post (SNACK_ACCESS, параметры)` С правильным отступом. Кстати, если вы устали от меня, я полностью понимаю. Вы познакомили меня с некоторыми новыми концепциями, которые должны помочь мне в долгосрочной перспективе. И снова я был бы рад заплатить вам за ваше время и помощь. В идеале через криптовалюту. - person David Stonikas; 13.09.2017