How can I refactor these common controller methods? - ruby-on-rails

I have a few controller methods that are extremely similar and I was wondering what the best way to refactor them would be. First thing that comes to mind would be somehow passing in two blocks to a helper method, but I'm not sure how to do that either.
def action_a
if #last_updated.nil?
#variable_a = #stuff_a
else
#variable_a = (#stuff_a.select{ |item| item.updated_at > #last_updated }
end
end
def action_b
if #last_updated.nil?
#variable_b = #stuff_b.some_method
else
#variable_b = #stuff_b.some_method.select{ |stuff| item.updated_at > #last_updated }
end
end
It just seems like I'm constantly checking if #last_updated is nil (I set the #last_updated instance variable in a before_filter. If I could somehow pass the stuff inside the if as a block and the stuff in the else as another block, then I could remove the if #last_updated.nil? duplication?
What is the best way of accomplishing this for many methods?
Update
Where I specify #stuff_a and #stuff_b, they are always returning an array (since I use .select).

Take a look at this. It's DRYer and should yield identical results.
def action_a
do_the_processing :"#variable_a", #stuff_a
end
def action_b
do_the_processing :"#variable_b", #stuff_b.some_method
end
private
def do_the_processing var_name, collection
if #last_updated.nil?
instance_variable_set var_name, collection
else
instance_variable_set var_name, collection.select{ |item| item.updated_at > #last_updated }
end
end
Update
And here's the two blocks approach (just for fun) (uses 1.9's stabby lambda syntax)
def action_a
check_last_updated is_nil: -> { #variable_a = #stuff_a },
is_not_nil: -> { #variable_a = (#stuff_a.select{ |item| item.updated_at > #last_updated } }
end
def action_b
check_last_updated is_nil: -> { #variable_b = #stuff_b.some_method },
is_not_nil: -> { #variable_b = #stuff_b.some_method.select{ |stuff| item.updated_at > #last_updated } }
end
private
def check_last_updated blocks = {}
if #last_updated.nil?
blocks[:is_nil].try(:call)
else
blocks[:is_not_nil].try(:call)
end
end

You need to extract your condition in a separate def block and use it later on:
def select_updates a
#last_updated.nil? ? a : a.select{ |item| item.updated_at > #last_updated }
end
def action_a; #variable_a = select_updates(#stuff_a) end
def action_b; #variable_b = select_updates(#stuff_b.some_method) end

AS I can see, you could do the followings
have two scope for each
Ex:
class Stuff < ActiveRecord::Base
scope :updated_at, lambda {|updated_date|
{:conditions => "updated_at > #{updated_date}"}
}
end
class Item < ActiveRecord::Base
scope :updated_at, lambda {|updated_date|
{:conditions => "updated_at > #{updated_date}"}
}
end
in your controller do this
def action_a
#variable_a = update_method(#stuff_a)
end
def action_b
#variable_b = update_method(#stuff_b)
end
private
def update_method(obj)
result = nil
if #last_updated.nil?
result = obj.some_method
else
result = obj.some_method.updated_at(#last_updated)
end
result
end
HTH

Related

How to pass :current_user in Graphql resolver

I have QueryType
Types::QueryType = GraphQL::ObjectType.define do
name 'Query'
field :allProjects, function: Resolvers::Projects
end
And Resolver like this
require 'search_object/plugin/graphql'
module Resolvers
class Projects
include SearchObject.module(:graphql)
type !types[Types::ProjectType]
scope { Project.all }
ProjectFilter = GraphQL::InputObjectType.define do
name 'ProjectFilter'
argument :OR, -> { types[ProjectFilter] }
argument :description_contains, types.String
argument :title_contains, types.String
end
option :filter, type: ProjectFilter, with: :apply_filter
option :first, type: types.Int, with: :apply_first
option :skip, type: types.Int, with: :apply_skip
def apply_first(scope, value)
scope.limit(value)
end
def apply_skip(scope, value)
scope.offset(value)
end
def apply_filter(scope, value)
branches = normalize_filters(value).reduce { |a, b| a.or(b) }
scope.merge branches
end
def normalize_filters(value, branches = [])
scope = Project.all
scope = scope.where('description ILIKE ?', "%#{value['description_contains']}%") if value['description_contains']
scope = scope.where('title ILIKE ?', "%#{value['title_contains']}%") if value['title_contains']
branches << scope
value['OR'].reduce(branches) { |s, v| normalize_filters(v, s) } if value['OR'].present?
branches
end
end
end
I want to access current_user in the resolver so i can access current_user.projects not Project.all. I am very new to graphql and learning.
Everything works but i just need to understand the whole flow on how i can get old of the ctx in the resolver.
First you need to set the current_user in the context. This happens in your GraphqlController.
class GraphqlController < ApplicationController
before_action :authenticate_user!
def execute
variables = ensure_hash(params[:variables])
query = params[:query]
operation_name = params[:operationName]
context = {
current_user: current_user,
}
result = HabitTrackerSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
render json: result
rescue => e
raise e unless Rails.env.development?
handle_error_in_development e
end
# ...
end
Once it's done, you can access the current_user from a query (or a mutation) simply by writing:
context[:current_user]
To make things even simpler, you can add a current_user method toTypes::BaseObject (app/graphql/types/base_object.rb) and you'll be able to call current_user from the #resolve methods.
module Types
class BaseObject < GraphQL::Schema::Object
field_class Types::BaseField
def current_user
context[:current_user]
end
end
end

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

How to refactor ActiveRecord query?

I have a code in controller:
def latest
#latest_articles = user_signed_in? ? Article.limit(10).order(id: :desc).pluck(:id, :title) : Article.where("status = ?", Article.statuses[:public_article]).limit(10).order(id: :desc).pluck(:id, :title)
render json: #latest_articles
end
How to refactor it to looks elegant?
I tried using lambda:
extract = lambda {|a| a.order(id: :desc).pluck(:id, :title)}
Article.limit(10) {|a| a.extract}
but it returns only Article.limit(10)
UPD: I need to get last 10 of all articles if user is signed in, and last 10 of only public ones if not.
I would create an initial scope, and modify it based on some conditions:
def latest
scope = Article.order(id: :desc)
scope = scope.where(status: Article.statuses[:public_article]) if user_signed_in?
render json: scope.limit(10).pluck(:id, :title)
end
You could refactor as
#lates_articles = Article.all
#lates_articles = #latest_articles.where("status = ?", Article.statuses[:public_article]) unless user_signed_in?
render json: #latest_articles.limit(10).order(id: :desc).pluck(:id, :title)
But it would be better to create model method
class Article < ActiveRecord::Base
...
scope :latest, -> {last(10).order(id: :desc)}
def self.public user_signed
if user_signed
all
else
where("status = ?", statuses[:public_article])
end
end
...
end
Then you would use it like
def latest
render json: Article.public(user_signed_in?).latest.pluck(:id, :title)
end
final version:
def latest
scope = Article.order(id: :desc)
scope = scope.shared unless user_signed_in?
render json: scope.limit(10), except: [:body, :created_at, :updated_at]
end

More ruby-like way of writing simple ActiveRecord code

Here is some fairly standard Ruby on Rails 4 ActiveRecord code:
def hide(user)
self.hidden = true
self.hidden_on = DateTime.now
self.hidden_by = user.id
end
def unhide
self.hidden = false
self.hidden_on = nil
self.hidden_by = nil
end
def lock(user)
self.locked = true
self.locked_on = DateTime.now
self.locked_by = user.id
end
def unlock
self.locked = false
self.locked_on = nil
self.locked_by = nil
end
# In effect this is a soft delete
def take_offline(user)
hide(user)
lock(user)
end
The code is easy to understand and doesn't try to be clever. However it feels verbose. What would be a more succinct or canonical way of specifying this code/behaviour?
Well, it's a trade-off, but if you want to be more clever, you can do something like:
def self.def_toggle(type, field)
define_method(type) do |user|
send("#{field}=", true)
send("#{field}_on=", DateTime.now)
send("#{field}_by=", user.id)
end
define_method("un#{type}") do
send("#{field}=", false)
send("#{field}_on=", nil)
send("#{field}_by=", nil)
end
end
def_toggle(:hide, :hidden)
def_toggle(:lock, :locked)
It's a bit extreme unless you have a lot of these or you want to encapsulate a bit more logic. But you can do something like the following using composed_of
class Model < ActiveRecord::Base
composed_of :hidden, class_name: 'State', mapping: %w(hidden, hidden_on, hidden_by)
composed_of :locked, class_name: 'State', mapping: %w(locked, locked_on, locked_by)
def hide(user)
hidden.on
end
def unhide
hidden.off
end
def lock(user)
locked.on
end
def unlock
locked.off
end
end
class State < Struct.new(:state, :on, :by)
def on(user)
set(true, user)
end
def off
set(false, nil, nil)
end
def on?
state
end
def off?
!on
end
private
def set(state, by, on = Time.current)
self.state = state
self.by = by
self.on = on
end
end

Resources