How do I access a variable inside the method I'm calling in a block I'm passing to it? - ruby

I'm writing a wrapper for an XML API using Nokogiri to build the XML for submission.
In order to keep my code DRY, I'm using custom blocks for the first time and just getting to grips with how to pass variables back and forth and how that works.
What I'm doing at the moment is this:
# Generic action
def action(xml, action_title, test=false)
xml.request do
xml.login do
xml.username("my_user")
xml.password("my_pass")
end
xml.action(action_title)
xml.params do
yield
end
end
end
# Specific action
def get_users(city = "", gender = "")
build = Nokogiri::XML::Builder.new do |xml|
action(xml, "getusers") do
xml.city(city) unless city.blank?
xml.gender(gender) unless gender.blank?
end
end
do_stuff_to(build)
end
Ideally, I'd like to the specific action method to look like this:
def get_users(city = "", gender = "")
action("getusers") do |xml|
xml.city(city) unless city.blank?
xml.gender(gender) unless gender.blank?
end
end
In doing so, I'd want the other logic currently in the specific action method to be moved to the generic action method with the generic action method returning the results of do_stuff_to(build).
What I'm struggling with is how to pass the xml object from action() back to get_users(). What should action() look like in order to achieve this?

Turns out this was quite simple. The action method needs to be changed so it looks like this:
def action(action_title)
build = Nokogiri::XML::Builder.new do |xml|
xml.request do
xml.login do
xml.username("my_user")
xml.password("my_pass")
end
xml.action(action_title)
xml.params do
yield xml
end
end
end
do_stuff_to(build)
end
That meant the specific action method could be called like this to the same effect:
def get_users(city = "", gender = "")
action("getusers") do |xml|
xml.city(city) unless city.blank?
xml.gender(gender) unless gender.blank?
end
end

Related

How to create instance variables with iterator

I have a lab through school where I need to create a form that takes in basketball team attributes, team name, coach, point guard etc., and want to know if there is any way to dynamically create instance variables and symbols using an iterator of sorts instead of hard coding them?
Here is the hard-coded version of what I mean:
post "/team" do
#name = params["name"]
#coach = params["coach"]
#pg = params["pg"]
#sg = params["sg"]
#pf = params["pf"]
#sf = params["sf"]
#c = params["c"]
erb :team
end
I want to use something similar to this:
post '/team' do
params.each do |parameter|
#[parameter] = params["#{parameter}"]
end
erb :team
end
When I run the above code I receive a unexpected end-of-input syntax error.
Try to use instance_variable_set,
something like this:
post '/team' do
params.each do |key, value|
self.instance_variable_set("##{key}", value)
end
erb :team
end

AFMotion HTTP GET request syntax for setting variable

My goal is to set an instance variable using AFMotion's AFMotion::HTTP.get method.
I've set up a Post model. I would like to have something like:
class Post
...
def self.all
response = AFMotion::HTTP.get("localhost/posts.json")
objects = JSON.parse(response)
results = objects.map{|x| Post.new(x)}
end
end
But according to the docs, AFMotion requires some sort of block syntax that looks and seems to behave like an async javascript callback. I am unsure how to use that.
I would like to be able to call
#posts = Post.all in the ViewController. Is this just a Rails dream? Thanks!
yeah, the base syntax is async, so you don't have to block the UI while you're waiting for the network to respond. The syntax is simple, place all the code you want to load in your block.
class Post
...
def self.all
AFMotion::HTTP.get("localhost/posts.json") do |response|
if result.success?
p "You got JSON data"
# feel free to parse this data into an instance var
objects = JSON.parse(response)
#results = objects.map{|x| Post.new(x)}
elsif result.failure?
p result.error.localizedDescription
end
end
end
end
Since you mentioned Rails, yeah, this is a lil different logic. You'll need to place the code you want to run (on completion) inside the async block. If it's going to change often, or has nothing to do with your Model, then pass in a &block to yoru method and use that to call back when it's done.
I hope that helps!

Best way to DRY up code using procs and blocks and/or dynamic methods

I am writing a way to parse websites, each "scraper" has it's own way gather information, but there is plenty of common functionality between two methods.
Differences:
One scraper uses Nokogiri to open the page via css selectors
the other scraper uses an RSS feed to gather information
Similarities:
each scraper creates an "Event" object that has the following attributes:
title
date
description
if for the Nokogiri scraper, we do something like this:
event_selector = page.css(".div-class")
event_selector.each_with_index do |event, index|
date = Date.parse(event.text) #code I want to share
end
for the RSS scraper, we do something like this
open(url) do |rss|
feed = RSS::Parser.parse(rss)
feed.items.each do |event|
description = Sanitize.fragment(event.description)
date = description[/\d{2}-\d{2}-20\d{2}/]
date = Date.strptime(date, '%m-%d-%Y') #code I want to share
end
end
^^ The date is grabbed via a regex from the description and then converted into a Date object via the .strptime method
as you can see each scraper uses 2 different method calls/ways to find the date. How could I abstract this information into a class?
I was thinking of something like this:
class scrape
attr_accessor :scrape_url, :title, :description, :date, :url
def initialize(options = {})
end
def find_date(&block)
# Process the block??
end
end
and then in each of the scraper methods do something like
scrape = Scrape.new
date_proc = Proc.new {Date.parse(event.text)}
scrape.find_date(date_proc)
Is this the right way to go about this problem? In short I want to have common functionality of two website parsers to pass the desired code into a instance method of a "scrape" class. I would greatly appreciate any tips to tackle this scenario.
Edit: Maybe it would make more sense if I say that I want to find the "date" of an event, but the way I find it - the behavior - or the specific code that is run, is different.
You could use an Event builder. Something like this:
class Event::Builder
def date(raw)
#date = Date.strptime(raw, '%m-%d-%Y')
end
# ... more setters (title, description) ...
def build
Event.new(date: #date, ... more arguments ..)
end
end
And then, inside the scraper:
open(url) do |rss|
builder = Event::Builder.new
feed = RSS::Parser.parse(rss)
feed.items.each do |event|
description = Sanitize.fragment(event.description)
date = description[/\d{2}-\d{2}-20\d{2}/]
builder.date(date)
# ... set other attributes ...
event = builder.build
# do something with the event ...
end
end
You should look into the Strategy or Template patterns. These are ways of writing code that does different things depending on some state or configuration. Essentially you'd write a Scraper class and then sub class it as WebScraper and RssScraper. Each class would inherit from the Scraper class all the common functionality but only differ in their implementation of how to get the date, description, etc.

Trying to access a variable inside a block

I'm trying to make a simple rss generator. My initialize method works fine and the update method runs without an error too but the new item in the update method never get added to the rss feed. I think it has something to do with how i'm accessing the variable 'maker' but i'm not sure.
require "rss"
class RSS_Engine
def initialize
#rss = RSS::Maker.make("atom") do |maker|
maker.channel.author = "Jamie"
maker.channel.updated = Time.now.to_s
maker.channel.about = "http://www.ruby-lang.org/en/feeds/news.rss"
maker.channel.title = "Example Feed"
#maker = maker
end
end
def update
#maker.items.new_item do |item|
item.title = "Test"
item.updated = Time.now.to_s
end
end
def print_rss
puts #rss
end
end
rss = RSS_Engine.new
rss.update
rss.print_rss
I got the original code from this example:
rss = RSS::Maker.make("atom") do |maker|
maker.channel.author = "matz"
maker.channel.updated = Time.now.to_s
maker.channel.about = "http://www.ruby-lang.org/en/feeds/news.rss"
maker.channel.title = "Example Feed"
maker.items.new_item do |item|
item.link = "http://www.ruby-lang.org/en/news/2010/12/25/ruby-1-9-2-p136-is-released/"
item.title = "Ruby 1.9.2-p136 is released"
item.updated = Time.now.to_s
end
This code works fine but i want to be able to add new posts to the rss feed over time so i'm trying to put the 'new.item' bit into it's own method.
The problem is not #maker variable, you have to invoke to_feed method to regenerate the feed after you modify it out of the code block.
So you need to add #rss = #maker.to_feed at the end of your update method.
One more thing about creating a new feed entry, link or id attribute need to be set.
Below code will work for you:
def update
#maker.items.new_item do |item|
item.link = "http://test.com"
item.title = "Test"
item.updated = Time.now.to_s
end
#rss = #maker.to_feed
end
If you are interested about why, you can take a look ruby rss source code. And below code(under rss/maker/base.rb) is the root cause why you need to invoke to_feed method if you modify feed out of the block:
def make(*args, &block)
new(*args).make(&block)
end
def make
yield(self)
to_feed
end

Bubblewrap HTTP -> Table View; method returns bubblewrap query instead of response data

I'm trying out Rubymotion and can't seem to do figure how to accomplish what seems like a simple task.
I've set up a UITableView for a directory of people. I've created a rails back end that returns json.
Person model has a get_people class method defined:
def self.get_people
BubbleWrap::HTTP.get("http://myapp.com/api.json") do |response|
#people = BW::JSON.parse(response.body.to_str)
# p #people prints [{"id"=>10, "name"=>"Sam"}, {etc}] to the console
end
end
In the directory_controller I just want to set an instance variable for #data to the array that my endpoint returns such that I can populate the table view.
I am trying to do #data = Person.get_people in viewDidLoad, but am getting an error message that indicates the BW response object is being passed instead: undefined methodcount' for #BubbleWrap::HTTP::Query:0x8d04650 ...> (NoMethodError)`
So if I hard code my array into the get_people method after the BW response block everything works fine. But I find that I am also unable to persist an instance variable through the close of the BW respond block.
def self.get_people
BubbleWrap::HTTP.get("http://myapp.com/api.json") do |response|
#people = BW::JSON.parse(response.body.to_str)
end
p #people #prints nil to the console
# hard coding [{"id"=>10, "name"=>"Sam"}, {etc}] here puts my data in the table view correctly
end
What am I missing here? How do I get this data out of bubblewrap's response object and in to a usable form to pass to my controllers?
As explained in the BW documentation "BW::HTTP wraps NSURLRequest, NSURLConnection and friends to provide Ruby developers with a more familiar and easier to use API. The API uses async calls and blocks to stay as simple as possible."
Due to async nature of the call, in your 2nd snippet you are printing #people before you actually update it. THe right way is to pass the new data to the UI after parsing ended (say for instance #table.reloadData() if #people array is supposed to be displayed in a UITableView).
Here's an example:
def get_people
BubbleWrap::HTTP.get("http://myapp.com/api.json") do |response|
#people = BW::JSON.parse(response.body.to_str)
update_result()
end
end
def update_result()
p #people
# do stuff with the updated content in #people
end
Find a more complex use case with a more elaborate explanation at RubyMotion async programming with BubbleWrap
Personally, I'd skip BubbleWrap and go for something like this:
def self.get_people
people = []
json_string = self.get_json_from_http
json_data = json_string.dataUsingEncoding(NSUTF8StringEncoding)
e = Pointer.new(:object)
hash = NSJSONSerialization.JSONObjectWithData(json_data, options:0, error: e)
hash["person"].each do |person| # Assuming each of the people is stored in the JSON as "person"
people << person
end
people # #people is an array of hashes parsed from the JSON
end
def self.get_json_from_http
url_string = ("http://myapp.com/api.json").stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)
url = NSURL.URLWithString(url_string)
request = NSURLRequest.requestWithURL(url)
response = nil
error = nil
data = NSURLConnection.sendSynchronousRequest(request, returningResponse: response, error: error)
raise "BOOM!" unless (data.length > 0 && error.nil?)
json = NSString.alloc.initWithData(data, encoding: NSUTF8StringEncoding)
end

Resources