undefined method `map' api request [closed] - ruby-on-rails

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 4 years ago.
Improve this question
I followed tutorial how to integrate 3rd party api with a ruby on rails but I get an error
undefined method `map' for
{"number"=>12} permitted: false>:ActionController::Parameters
which points to request.rb
query_string = query.map{|k,v| "#{k}=#{v}"}.join("&")
Full code
recipes_controller.rb
class RecipesController < ApplicationController
def index
#tag = query.fetch(:tags, 'all')
#refresh_params = refresh_params
#recipes, #errors = Spoonacular::Recipe.random(query, clear_cache)
end
def show
#recipe = Spoonacular::Recipe.find(params[:id])
end
private
def query
params.permit(:query).fetch(:query, {})
end
def clear_cache
params[:clear_cache].present?
end
def refresh_params
refresh = { clear_cache: true }
refresh.merge!({ query: query }) if query.present?
refresh
end
end
app/services/spoonacular/recipes.rb
module Spoonacular
class Recipe < Base
attr_accessor :aggregate_likes,
:dairy_free,
:gluten_free,
:id,
:image,
:ingredients,
:instructions,
:ready_in_minutes,
:title,
:vegan,
:vegetarian
MAX_LIMIT = 12
CACHE_DEFAULTS = { expires_in: 7.days, force: false }
def self.random(query = {}, clear_cache)
cache = CACHE_DEFAULTS.merge({ force: clear_cache })
response = Spoonacular::Request.where('recipes/random', cache, query.merge({ number: MAX_LIMIT }))
recipes = response.fetch('recipes', []).map { |recipe| Recipe.new(recipe) }
[ recipes, response[:errors] ]
end
def self.find(id)
response = Spoonacular::Request.get("recipes/#{id}/information", CACHE_DEFAULTS)
Recipe.new(response)
end
def initialize(args = {})
super(args)
self.ingredients = parse_ingredients(args)
self.instructions = parse_instructions(args)
end
def parse_ingredients(args = {})
args.fetch("extendedIngredients", []).map { |ingredient| Ingredient.new(ingredient) }
end
def parse_instructions(args = {})
instructions = args.fetch("analyzedInstructions", [])
if instructions.present?
steps = instructions.first.fetch("steps", [])
steps.map { |instruction| Instruction.new(instruction) }
else
[]
end
end
end
end
app/services/spoonacular/base.rb
module Spoonacular
class Base
attr_accessor :errors
def initialize(args = {})
args.each do |name, value|
attr_name = name.to_s.underscore
send("#{attr_name}=", value) if respond_to?("#{attr_name}=")
end
end
end
end
app/services/spoonacular/request.rb
module Spoonacular
class Request
class << self
def where(resource_path, cache, query = {}, options = {})
response, status = get_json(resource_path, cache, query)
status == 200 ? response : errors(response)
end
def get(id, cache)
response, status = get_json(id, cache)
status == 200 ? response : errors(response)
end
def errors(response)
error = { errors: { status: response["status"], message: response["message"] } }
response.merge(error)
end
def get_json(root_path, cache, query = {})
query_string = query.map{|k,v| "#{k}=#{v}"}.join("&")
path = query.empty?? root_path : "#{root_path}?#{query_string}"
response = Rails.cache.fetch(path, expires_in: cache[:expires_in], force: cache[:force]) do
api.get(path)
end
[JSON.parse(response.body), response.status]
end
def api
Connection.api
end
end
end
end
app/services/spoonacular/connection.rb
require 'faraday'
require 'json'
module Spoonacular
class Connection
BASE = 'https://spoonacular-recipe-food-nutrition-v1.p.mashape.com'
def self.api
Faraday.new(url: BASE) do |faraday|
faraday.response :logger
faraday.adapter Faraday.default_adapter
faraday.headers['Content-Type'] = 'application/json'
faraday.headers['X-Mashape-Key'] ='key'
end
end
end
end
Thank you for any help.

You have 2 separate errors here.
uninitialized constant Spoonacular::Recipe::Request
This one you can fix by explicitly setting top-level scope for Request class:
::Request.where(...)
It applies if you keep Request file in app/spoonacular/request.rb. But I suggest to move it to app/services/spoonacular/ where all your other spoonacular related classes are. So in this case you need to encircle class Request in module Spoonacular. After that you can call it like that:
Spoonacular::Request.where(...)
Same goes for class Connection.
SO answer about scope resolution operator
undefined method `map' for {"number"=>12} permitted:
false>:ActionController::Parameters
This one comes from private query method in recipes_controller.rb. params is ActionController::Parameters object and in order to retrieve values from it you need to permit them first:
def query
params.permit(:query).to_h
end
Now it should return Hash object.
Here is detailed answer on SO about that
RubyOnRails Guide about strong params

Related

Issue with HomeController showing undefined method

I am trying to pass stored_products from shopify into a Rails app but keep getting a home controller error at https://f588240c.ngrok.io/ i have made updates, with no luck and restarted the server a number of times with no luck.
Any help would be welcomed. Heres the code
class Api::V1::HomeController < ShopifyApp::AuthenticatedController
def index
#products = ShopifyAPI::Product.find(:all, params: { limit: 10 })
#products.each do |product|
StoredProduct.where(shopify_id: product.id)
.first_or_create do |stored_product|
stored_product.shopify_id = product.id
stored_product.shopify_title = product.title
stored_product.shopify_handle = product.handle
stored_product.shopify_image_url = product.image.src
stored_product.shop_id = #shop.id
stored_product.save
product.images.each do |image|
ProductImage.where(shopify_id: image.id)
.first_or_create do |product_image|
product_image.image_url = image.src
product_image.stored_product_id = stored_product_id
product_image.shopify_id = image.id
end
end
end
end
#stored_products = StoredProduct.belongs_to_shop(#shop.id)
end
end
From the authenticated controller
private
def set_shop
#shop = Shop.find_by(id: session[:shopify])
set_locale
end
from the store_products.rb file
class StoredProduct < ApplicationRecord
belongs_to :shop
has_many :product_images
scope :belongs_to_shop, -> shop_id { where(shop_id: shop_id) }
end
For this specific issue/code tutorial, the private set_shop method should be set like follows:
def set_shop
#shop = Shop.find_by(id: session[:shop_id])
set_locale
end
The other answer has params instead of session
The problem is that #shop is nil. The error message says it cannot call the method .id on NilClass.
In the image I can see that you have a shop_id in the params so you might just need to change your code here:
def set_shop
#shop = Shop.find_by(id: params[:shop_id])
set_locale
end
But that depends on your code, so please double check.

Rails class method not defined

I have a controller which calls a class method from a model. However, I got undefined method 'where' for Jira:Class.
controller:
module Api
module V1
class JiraController < ApplicationController
def index
jira = Jira.where()
jira_stat = JiraStat.new(jira)
render json: [
{
t('jira.api.status') => jira_stat.status,
t('jira.api.number_of_jiras') => jira_stat.jira_total
}
]
end
end
end
end
model:
# frozen_string_literal: true
require 'active_model'
class Jira
include ActiveModel::Model
include JiraKit
attr_accessor :status, :jira
def self.where(status_name = 'all')
if status_name == 'all'
jiras = JiraKit.where.jira_issues(status: ['open', 'submitted', 'in
progress', 'in review', 'closed'])
elsif
jiras = JiraKit.where.jira_issues(status: [status_name])
end
new(#status = status_name, #jira = jiras)
end
end
I think I have used self keyword. But I don't know why I can't access that method. If I create an instance of Jira model, I am able to access that method.

How to send a get to a server and wait a get in my app in Ruby on Rails

I am using Zapier to search some information in google sheets. I used Webhocks to send a GET to his server with a JSON information. The response of GET is an "OK" and I can't custom this.
So, they will execute a task, find what a I want and return a value, but the response must be a GET in my server, and I don't know how to intercept this response in my route.
I'm trying to study Rails Rack to intercept de request in my app, but I don't know how to send the response to the event that sent the first GET.
How is my middleware:
class DeltaLogger
def initialize app
#app = app
end
def call env
Rails.logger.debug "#{env['QUERY_STRING']}"
#status, #headers, #response = #app.call(env)
[#status, #headers, #response]
end
end
Thanks!
Example
So, to get the value returned from Zapier, I created two routes and a global class cache.
class Zapier
require 'httparty'
def initialize
#answer = ""
#id = 0
end
def request(uri, task)
last_event = Event.last
puts last_event.inspect
if last_event.nil?
last_id = 0
else
last_id = last_event.event_id
end
event_id = last_id + 1
Event.find_or_create_by(event_id: event_id)
result = HTTParty.post(uri.to_str,
:body => {id: event_id, task: task}.to_json,
:headers => {'content-Type' => 'application/json'})
#answer = ""
#id = event_id
end
def response(event_id, value)
if event_id != #id
#answer = ""
else
#answer = value
end
end
def get_answer
#answer
end
end
And my controller:
class ZapierEventsController < ApplicationController
require 'zapier_class'
before_action :get_task, only: [:get]
before_action :get_response, only: [:set]
##zapier ||= Zapier.new
def get
##zapier.request('https://hooks.zapier.com',#task)
sleep 10 #Wait for response
#value = ##zapier.get_answer
render json: { 'value': #value }, status:
end
def set
##zapier.response(#id, #value)
render json: { 'status': 'ok' }, status: 200
end
def get_task
#task = params["task"]
end
def get_response
#id = Integer(params["id"])
#value = params["value"]
end
end
Now i have to make a Task Mananger

How can I test these RSS parsing service objects?

I have some service objects that use Nokogiri to make AR instances. I created a rake task so that I can update the instances with a cron job. What I want to test is if it's adding items that weren't there before, ie:
Create an Importer with a url of spec/fixtures/feed.xml, feed.xml having 10 items.
Expect Show.count == 1 and Episode.count == 10
Edit spec/fixtures/feed.xml to have 11 items
Invoke rake task
Expect Show.count == 1 and Episode.count == 11
How could I test this in RSpec, or modify my code to be more testable?
# models/importer.rb
class Importer < ActiveRecord::Base
after_create :parse_importer
validates :title, presence: true
validates :url, presence: true
validates :feed_format, presence: true
private
def parse_importer
Parser.new(self)
end
end
# models/show.rb
class Show < ActiveRecord::Base
validates :title, presence: true
validates :title, uniqueness: true
has_many :episodes
attr_accessor :entries
end
# models/episode.rb
class Episode < ActiveRecord::Base
validates :title, presence: true
validates :title, uniqueness: true
belongs_to :show
end
#lib/tasks/admin.rake
namespace :admin do
desc "Checks all Importer URLs for new items."
task refresh: :environment do
#importers = Importer.all
#importers.each do |importer|
Parser.new(importer)
end
end
end
# services/parser.rb
class Parser
def initialize(importer)
feed = Feed.new(importer)
show = Show.where(rss_link: importer.url).first
if show # add new episodes
new_episodes = Itunes::Channel.refresh(feed.origin)
new_episodes.each do |new_episode|
show.episodes.create feed.episode(new_episode)
end
else # create a show and its episodes
new_show = Show.new(feed.show) if (feed && feed.show)
if (new_show.save && new_show.entries.any?)
new_show.entries.each do |entry|
new_show.episodes.create feed.episode(entry)
end
end
end
end
end
# services/feed.rb
class Feed
require "nokogiri"
require "open-uri"
require "formats/itunes"
attr_reader :params, :origin, :show, :episode
def initialize(params)
#params = params
end
def origin
#origin = Nokogiri::XML(open(params[:url]))
end
def format
#format = params[:feed_format]
end
def show
case format
when "iTunes"
Itunes::Channel.fresh(origin)
end
end
def episode(entry)
#entry = entry
case format
when "iTunes"
Itunes::Item.fresh(#entry)
end
end
end
# services/formats/itunes.rb
class Itunes
class Channel
def initialize(origin)
#origin = origin
end
def title
#origin.xpath("//channel/title").text
end
def description
#origin.xpath("//channel/description").text
end
def summary
#origin.xpath("//channel/*[name()='itunes:summary']").text
end
def subtitle
#origin.xpath("//channel/*[name()='itunes:subtitle']/text()").text
end
def rss_link
#origin.xpath("//channel/*[name()='atom:link']/#href").text
end
def main_link
#origin.xpath("//channel/link/text()").text
end
def docs_link
#origin.xpath("//channel/docs/text()").text
end
def release
#origin.xpath("//channel/pubDate/text()").text
end
def image
#origin.xpath("//channel/image/url/text()").text
end
def language
#origin.xpath("//channel/language/text()").text
end
def keywords
keywords_array(#origin)
end
def categories
category_array(#origin)
end
def explicit
explicit_check(#origin)
end
def entries
entry_array(#origin)
end
def self.fresh(origin)
#show = Itunes::Channel.new origin
return {
description: #show.description,
release: #show.release,
explicit: #show.explicit,
language: #show.language,
title: #show.title,
summary: #show.summary,
subtitle: #show.subtitle,
image: #show.image,
rss_link: #show.rss_link,
main_link: #show.main_link,
docs_link: #show.docs_link,
categories: #show.categories,
keywords: #show.keywords,
entries: #show.entries
}
end
def self.refresh(origin)
#show = Itunes::Channel.new origin
return #show.entries
end
private
def category_array(channel)
arr = []
channel.xpath("//channel/*[name()='itunes:category']/#text").each do |category|
arr.push(category.to_s)
end
return arr
end
def explicit_check(channel)
string = channel.xpath("//channel/*[name()='itunes:explicit']").text
if string === "yes" || string === "Yes"
true
else
false
end
end
def keywords_array(channel)
keywords = channel.xpath("//channel/*[name()='itunes:keywords']/text()").text
arr = keywords.split(",")
return arr
end
def entry_array(channel)
arr = []
channel.xpath("//item").each do |item|
arr.push(item)
end
return arr
end
end
class Item
def initialize(origin)
#origin = origin
end
def description
#origin.xpath("*[name()='itunes:subtitle']").text
end
def release
#origin.xpath("pubDate").text
end
def image
#origin.xpath("*[name()='itunes:image']/#href").text
end
def explicit
explicit_check(#origin)
end
def duration
#origin.xpath("*[name()='itunes:duration']").text
end
def title
#origin.xpath("title").text
end
def enclosure_url
#origin.xpath("enclosure/#url").text
end
def enclosure_length
#origin.xpath("enclosure/#length").text
end
def enclosure_type
#origin.xpath("enclosure/#type").text
end
def keywords
keywords_array(#origin.xpath("*[name()='itunes:keywords']").text)
end
def self.fresh(entry)
#episode = Itunes::Item.new entry
return {
description: #episode.description,
release: #episode.release,
image: #episode.image,
explicit: #episode.explicit,
duration: #episode.duration,
title: #episode.title,
enclosure_url: #episode.enclosure_url,
enclosure_length: #episode.enclosure_length,
enclosure_type: #episode.enclosure_type,
keywords: #episode.keywords
}
end
private
def explicit_check(item)
string = item.xpath("*[name()='itunes:explicit']").text
if string === "yes" || string === "Yes"
true
else
false
end
end
def keywords_array(item)
keywords = item.split(",")
return keywords
end
end
end
Before anything else, good for you for using service objects! I've been using this approach a great deal and find POROs preferable to fat models in many situations.
It appears the behavior you're interested in testing is contained in Parser.initialize.
First, I'd create a class method for Parser called parse. IMO, Parser.parse(importer) is clearer about what Parser is doing than is Parser.new(importer). So, it might look like:
#services/parser.rb
class Parser
class << self
def parse(importer)
#importer = importer
#feed = Feed.new(importer)
if #show = Show.where(rss_link: importer.url).first
create_new_episodes Itunes::Channel.refresh(#feed.origin)
else
create_show_and_episodes
end
end # parse
end
end
Then add the create_new_episodes and create_show_and_episodes class methods.
#services/parser.rb
class Parser
class << self
def parse(importer)
#importer = importer
#feed = Feed.new(importer)
if #show = Show.where(rss_link: #importer.url).first
create_new_episodes Itunes::Channel.refresh(#feed.origin)
else
create_show_and_episodes
end
end # parse
def create_new_episodes(new_episodes)
new_episodes.each do |new_episode|
#show.episodes.create #feed.episode(new_episode)
end
end # create_new_episodes
def create_show_and_episodes
new_show = Show.new(#feed.show) if (#feed && #feed.show)
if (new_show.save && new_show.entries.any?)
new_show.entries.each do |entry|
new_show.episodes.create #feed.episode(entry)
end
end
end # create_show_and_episodes
end
end
Now you have a Parser.create_new_episodes method that you can test independently. So, your test might look something like:
require 'rspec_helper'
describe Parser do
describe '.create_new_episodes' do
context 'when an initial parse has been completed' do
before(:each) do
first_file = Nokogiri::XML(open('spec/fixtures/feed_1.xml'))
#second_file = Nokogiri::XML(open('spec/fixtures/feed_2.xml'))
Parser.create_show_and_episodes first_file
end
it 'changes Episodes.count by 1' do
expect{Parser.create_new_episodes(#second_file)}.to change{Episodes.count}.by(1)
end
it 'changes Show.count by 0' do
expect{Parser.create_new_episodes(#second_file)}.to change{Show.count}.by(0)
end
end
end
end
Naturally, you'll need feed_1.xml and feed_2.xml in the spec\fixtures directory.
Apologies for any typos. And, I didn't run the code. So, might be buggy. Hope it helps.

How does Rails params parse hash from string

I'm learning Ruby on Rails and got curious how the params method works. I understand what it does, but how?
Is there a built-in method that takes a hash string like so
"cat[name]"
and translates it to
{ :cat => { :name => <assigned_value> } }
?
I have attempted to write the params method myself but am not sure how to write this functionality in ruby.
The GET parameters are set from ActionDispatch::Request#GET, which extends Rack::Request#GET, which uses Rack::QueryParser#parse_nested_query.
The POST parameters are set from ActionDispatch::Request#POST, which extends Rack::Request#POST, which uses Rack::Multipart#parse_multipart. That splays through several more files in lib/rack/multipart.
Here is a reproduction of the functionality of the method (note: this is NOT how the method works). Helper methods of interest: #array_to_hash and #handle_nested_hash_array
require 'uri'
class Params
def initialize(req, route_params = {})
#params = {}
route_params.keys.each do |key|
handle_nested_hash_array([{key => route_params[key]}])
end
parse_www_encoded_form(req.query_string) if req.query_string
parse_www_encoded_form(req.body) if req.body
end
def [](key)
#params[key.to_sym] || #params[key.to_s]
end
def to_s
#params.to_s
end
class AttributeNotFoundError < ArgumentError; end;
private
def parse_www_encoded_form(www_encoded_form)
params_array = URI::decode_www_form(www_encoded_form).map do |k, v|
[parse_key(k), v]
end
params_array.map! do |sub_array|
array_to_hash(sub_array.flatten)
end
handle_nested_hash_array(params_array)
end
def handle_nested_hash_array(params_array)
params_array.each do |working_hash|
params = #params
while true
if params.keys.include?(working_hash.keys[0])
params = params[working_hash.keys[0]]
working_hash = working_hash[working_hash.keys[0]]
else
break
end
break if !working_hash.values[0].is_a?(Hash)
break if !params.values[0].is_a?(Hash)
end
params.merge!(working_hash)
end
end
def array_to_hash(params_array)
return params_array.join if params_array.length == 1
hash = {}
hash[params_array[0]] = array_to_hash(params_array.drop(1))
hash
end
def parse_key(key)
key.split(/\]\[|\[|\]/)
end
end

Resources