Creating Message Object Parser - ruby-on-rails

I'm receiving results from a web service like this:
result.body returns:
[2] pry(#<User::EmailSettingsController>)> result.body
=> {"RESULT"=>
{"MESSAGES"=>
[{"MESSAGE"=>
{"TYPE"=>"E",
"ID"=>"HRRCF_WD_UI",
"NUMBER"=>"025",
"MESSAGE"=>"U kunt maximaal \"5\" jobagents creëren 1",
"LOG_NO"=>"",
"LOG_MSG_NO"=>"000000",
"MESSAGE_V1"=>"5",
"MESSAGE_V2"=>"1",
"MESSAGE_V3"=>"",
"MESSAGE_V4"=>"",
"PARAMETER"=>"",
"ROW"=>"0",
"FIELD"=>"",
"SYSTEM"=>""}},
{"MESSAGE"=>
{"TYPE"=>"E",
"ID"=>"HRRCF_WD_UI",
"NUMBER"=>"025",
"MESSAGE"=>"U kunt maximaal \"5\" jobagents creëren 2",
"LOG_NO"=>"",
"LOG_MSG_NO"=>"000000",
"MESSAGE_V1"=>"5",
"MESSAGE_V2"=>"2",
"MESSAGE_V3"=>"",
"MESSAGE_V4"=>"",
"PARAMETER"=>"",
"ROW"=>"0",
"FIELD"=>"",
"SYSTEM"=>""}},
{"MESSAGE"=>
{"TYPE"=>"E",
"ID"=>"HRRCF_WD_UI",
"NUMBER"=>"025",
"MESSAGE"=>"U kunt maximaal \"5\" jobagents creëren 3",
"LOG_NO"=>"",
"LOG_MSG_NO"=>"000000",
"MESSAGE_V1"=>"5",
"MESSAGE_V2"=>"3",
"MESSAGE_V3"=>"",
"MESSAGE_V4"=>"",
"PARAMETER"=>"",
"ROW"=>"0",
"FIELD"=>"",
"SYSTEM"=>""}}]}}
Is it possible to create something ParseMessageObject(result.body) that returns that I can do something like this.
message_list = ParseMessageObject(result.body)
message_list.each do |message|
puts message.message
puts message.type
end
I have no idea if this is possible or how to do this any suggestions to get me started are welcome!
EDIT 1:
Created my class in lib:
class MessageParser
def self.parse(result)
end
end

This should basically do what you want, using a simple open struct to create a message class which has accessors for each of the keys in your message hash
require 'ostruct'
class MessageParser
Message = Struct.new(:type, :id, :number, :message, :log_no, :log_msg_no, :message_v1, :message_v2, :message_v3, :message_v4, :parameter, :row, :field, :system)
attr_reader :messages
def initialize(data)
#data = data.fetch("MESSAGES",[])
#messages = []
parse_data
end
private
def parse_data
#data.each do | msg |
message = Message.new
msg.fetch("MESSAGE",{}).each do |key, value|
message[key.downcase.to_sym] = value
end
#messages << message
end
end
end
parser = MessageParser.new(result.body["RESULT"])
parser.messages.each do |message|
puts message.message
puts message.type
end

Something like this should work:
class ParsedMessages
include Enumerable
attr_reader :messages
def initialize(data)
#messages = extract_messages_from_data(data)
end
def extract_messages_from_data(data)
# TODO: Parse data and return message objects
end
def each &block
#messages.each do |message|
if block_given?
block.call message
else
yield message
end
end
end
end
Now you can use all methods from Enumerable on ParsedMessages, like each, find, map etc etc.

Related

Get “wrong number of arguments” on ruby on rails

wrong number of arguments (given 2, expected 1)
SportsController
class SportsController < ApplicationController
def index
#sport = Sport.all
#events, #errors = Bapi::Inplay.all(query)
end
private
def query
params[:query, {}]
end
end
Sport index.html.erb
<% #sports.each do |sport| %>
<% #events(:sport_id => sport.id).each_slice(2) do |events| %>
I want send each sport.id to #enevts instance variable
Edited :
When send query as hash in SportsController its work!!
class SportsController < ApplicationController
def index
#sport = Sport.all
query = {:sport_id => 1}
#events, #errors = Bapi::Inplay.all(query)
end
private
def query
params[:query, {}]
end
end
Index.html.erb
<% #sports.each do |sport| %>
<% #events.each_slice(2) do |events| %>
params is a hash and method :[] can accept only 1 argument.
def query
params[:query] || {} # Will return :query part or empty Hash if it has nothing
end

wrapping code that runs in Rails environment into a module

I have this code and it works perfectly
require "date"
#past = []
#future = []
#artist = Artist.find(2)
def sort_by_date(artist)
artist.events.each do |event|
if event.date < DateTime.now
#past << event.id
else
#future << event.id
end
end
end
def event_title(arr)
arr.each do |event_id|
e = Event.find(event_id)
artist_names = []
e.artists.each do |artist|
unless artist.name == #artist.name
artist_names << artist.name
end
end
puts "#{e.name} with #{artist_names.join(", ")} at #{(Venue.find(e.venue_id)).name}"
end
end
sort_by_date(#artist)
puts "Upcoming Events: "
event_title(#future)
puts "Past Events: "
event_title(#past)
I want to run wrap this operation into a module, but I'm having trouble understanding how to pass artist_id to it properly. With this command rails runner app/modules/artist_event_sort.rb, I'm getting this error: ``': undefined method sort_by_date' for SortedArtistEvents:Module (NoMethodError). The two methods sort_by_date and event_title worked as they should before I tried wrapping this whole operation up into a module, so that's where I know I've missed something.
module SortedArtistEvents
require "date"
attr_accessor :artist_id
def initialize(artist_id)
#past = []
#future = []
#artist = Artist.find(artist_id)
end
def sort_by_date(artist)
artist.events.each do |event|
if event.date < DateTime.now
#past << event.id
else
#future << event.id
end
end
end
def event_title(arr)
arr.each do |event_id|
e = Event.find(event_id)
artist_names = []
e.artists.each do |artist|
unless artist.name == #artist.name
artist_names << artist.name
end
end
puts "#{e.name} with #{artist_names.join(", ")} at #{(Venue.find(e.venue_id)).name}"
end
end
sort_by_date(#artist)
puts "Upcoming Events: "
self.event_title(#future)
puts "Past Events: "
event_title(#past)
end
class LetsSort
include SortedArtistEvents
end
test_artist_sort = LetsSort.new(2)
It looks like there are a couple things wrong here. You are trying to initialize a module, you can only initialize a class, e.g. class SortedArtistEvents.
If you have this:
module Foo
def bar; end
end
bar is only accessible by including or extending a module or class with Foo. With your error undefined method sort_by_date' for SortedArtistEvents:Module you would have to do
module SortedArtistsEvents
def self.sort_by_date; end
end
to get behavior like SortedArtistsEvents.sort_by_date

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.

Manually assigning parent ID with has_many/belongs_to association in custom class

I'm using a custom class to make AR instances from Feedjirra. I can't get the children instances to relate to their parent objects.
Show has_many :episodes -
Episode belongs_to :show -
show_id is always nil.
RSpec logs #show.id and #episode.show_id as equal to one another. However when I run episode = Episode.first after running an import in development, the episode has its show_id set to nil.
#show = Show.new
#show.name = #feed.title
#show.description = #feed.description
...
if #show.save
puts "#show.id: #{#show.id}"
end
#episodes = []
#feed.entries.each do |item|
#episodes.push(item)
end
#episodes.each do |item|
#episode = #show.episodes.new
#episode.name = item.title
#episode.description = item.summary
...
if #episode.save
puts "#episode.show_id: #{#episode.show_id}"
end
end
I tried using #episode = #show.episodes.create, as well as #episode = Episode.new with #episode.show_id = #show.id. They all log matching IDs but show_id is still nil on the instances. Every other column is filled in correctly.
I thought the issue may have had to do with using add_foreign_key:
class AddShowToEpisodes < ActiveRecord::Migration
def change
add_reference :episodes, :show, index: true
add_foreign_key :episodes, :shows, column: :show_id
end
end
So I removed that and used the standard foreign_key: true but it had no effect.
class RemoveShowFromEpisodes < ActiveRecord::Migration
def change
remove_column :episodes, :show_id
end
end
class AddShowBackToEpisodes < ActiveRecord::Migration
def change
add_reference :episodes, :show, index: true, foreign_key: true
end
end
Here's the full code in case it helps.
importers_controller.rb:
class Admin::ImportersController < Admin::ApplicationController
before_action :set_importer, only: [:show, :edit, :update, :destroy]
def index
#importers = policy_scope(Importer)
end
def show
end
def new
#importer = Importer.new
authorize #importer
end
def create
#importer = Importer.new(importer_params)
authorize #importer
if #importer.save
require "subscription_importer"
SubscriptionImporter.new(#importer)
flash[:notice] = "Importer added."
redirect_to admin_importers_path
else
flash[:error] = "Importer not added."
render "new"
end
end
def edit
end
def update
end
def destroy
end
private
def set_importer
#importer = Importer.find(params[:id])
authorize #importer
end
def importer_params
params.require(:importer).permit(:name, :url, :source)
end
end
subscription_importer.rb:
class SubscriptionImporter
def initialize(importer)
#importer = importer
#feed = Feedjira::Feed.fetch_and_parse #importer.url
if #importer.source === "iTunes"
itunes_parser(#importer)
end
end
def itunes_parser(importer)
#importer = importer
# Parser
#feed = Feedjira::Feed.fetch_and_parse #importer.url
# Show
#show = Show.new
#show.name = #feed.title
#show.description = #feed.description
#show.logo = #feed.itunes_image
#show.explicit = explicit_check(#feed.itunes_explicit)
#show.genre = #feed.itunes_categories
#show.tags = #feed.itunes_keywords
#show.url = #feed.url
#show.language = #feed.language
if #show.save
puts "Show import succeeded"
puts "#show.id: #{#show.id}"
else
puts "Show import failed"
end
# Episodes
#episodes = []
#feed.entries.each do |item|
#episodes.push(item)
end
#episodes.each do |item|
#episode = #show.episodes.new
#episode.name = item.title
#episode.description = item.summary
#episode.release = item.published
#episode.image = item.itunes_image
#episode.explicit = explicit_check(item.itunes_explicit)
#episode.tags = item.itunes_keywords
#episode.url = item.enclosure_url
#episode.duration = item.itunes_duration
if #episode.save
puts "Episode import succeeded"
puts "#episode.show_id: #{#episode.show_id}"
else
puts "Episode import failed"
end
end
end
def explicit_check(string)
if string == "yes" || "Yes"
true
else
false
end
end
end
create_importer_spec.rb:
require "rails_helper"
RSpec.feature "Admins can create importers" do
let(:user) { FactoryGirl.create(:user, :admin) }
context "admins" do
before do
login_as(user)
visit "/"
click_link "Admin"
click_link "Importers"
click_link "New Importer"
end
scenario "with valid credentials" do
fill_in "Name", with: "The Stack Exchange Podcast"
fill_in "Url", with: "https://blog.stackoverflow.com/feed/podcast/" # Needs stubbing
select "iTunes", from: "Source"
click_button "Create Importer"
expect(page).to have_content "Importer added"
expect(page).to have_content "The Stack Exchange Podcast"
end
scenario "with invalid credentials" do
fill_in "Name", with: ""
fill_in "Url", with: ""
click_button "Create Importer"
expect(page).to have_content "Importer not added"
end
end
end
I think the episodes functionality in your SubscriptionImporter class is causing the problem...
#episodes = []
#feed.entries.each do |item|
#episodes.push(item) #-> each "#episodes" is a FeedJirra object
end
#episodes.each do |episode|
#-> you're now creating an episode in the same call as show, which will either mean that show is not persisted or perhaps some other error
end
I would personally limit the SubscriptionImporter functionality to only return data. You should be parsing that data through the respective models:
#app/controllers/admin/importers_controller.rb
class Admin::ImportersController < Admin::ApplicationController
def create
#import = Importer.new import_params
if #import.save
#import.parse_show if #import.itunes?
end
end
private
def import_params
params.require(:importer).permit(:name, :url, :source)
end
end
#app/models/importer.rb
class Importer < ActiveRecord::Base
def feed
return false unless itunes?
origin = Feedjirra::Feed.fetch_and_parse(self.url)
return {
name: origin.title,
description: origin.description,
logo: origin.itunes_image,
explicit: explicit_check(origin.itunes_explicit),
genre: origin.itunes_categories,
tags: origin.itunes_keywords,
url: origin.url,
language: origin.language,
entries: origin.entries
}
end
def parse_show
Show.create(feed)
end
def itunes?
self.source == "iTunes" #-> true/false
end
private
def explicit_check
string == "yes" || "Yes" #-> true/false
end
end
#app/models/show.rb
class Show < ActiveRecord::Base
has_many :episodes
attr_accessor :entries
after_create :create_episodes #-> might not persist entries
def create_episodes
if self.entries.any?
self.entries.each do |item|
self.episodes.create({
name: item.title
description: item.summary,
release: item.published,
image: item.itunes_image,
explicit: explicit_check?(item.itunes_explicit),
tags: item.itunes_keywords,
url: item.enclosure_url,
duration: item.itunes_duration
})
end
end
end
private
def explicit_check?
string == "yes" || "Yes"
end
end
The above will allow you to create an #importer, pull the feed from it, and populate Show & Episode models with the returned data.
Whilst this should resolve your issue, you need to consider OOP -- making each element an object.
Update
If you wanted to objectify this even more, there is a simple pattern to adopt:
Importer is all you need to save -- everything else should happen around this
Show + Episode could be the same class / table for all I know
With this in mind, you could do the following:
#app/controllers/admin/importers_controller.rb
class Admin::ImportersController < Admin::ApplicationController
def create
#import = Importer.new import_params
#import.save
end
private
def import_params
params.require(:importer).permit(:name, :url, :source)
end
end
#app/services/feed.rb
class Feed
attr_reader :params, :show, :episode, :origin
def initialize(params)
#params = params
end
def origin
#origin = Feedjirra::Feed.fetch_and_parse params[:source]
end
def show
#show = ShowHelper.new #origin
end
def episodes
#show.episodes
end
end
#app/services/show_helper.rb
class ShowHelper
attr_reader :origin
def initialize(origin)
#origin = origin
end
def name
#origin.title
end
def description
#origin.summary || #origin.description
end
def logo
#origin.itunes_image
end
def explicit
%r{^yes$} =~ #origin.itunes_explicit
end
def genre
#origin.itunes_categories
end
def tags
#origin.itunes_keywords
end
def url
#origin.url
end
def language
#origin.language
end
def episodes
#origin.entries
end
end
#app/models/importer.rb
class Importer < ActiveRecord::Base
after_create :parse_show, if: "itunes?"
validates :source, :url, :name, presence: true
def itunes?
source == "iTunes"
end
def feed
#feed = Feed.new(self)
end
private
def parse_show
#show = Show.new(feed.show) if feed && feed.show
if #show.save && #show.entries.any?
#show.entries.each do |entry|
#show.episodes.create ShowHelper.new(entry)
end
end
end
end

block methods in ruby

I am using a block method to print a list, but it is generating error.
class MyDataListBuilder
attr_accessor :object
def initialize(object)
#object = object
end
def column (&block)
content_tag :li, block.call
end
end
and using it as
<%= my_data_list_for #leads, [" :10", "Age:30", "Contact:140", "Phone:140", "Email:180", "Company:100", ""] do |l| %>
<%= l.column do %>
<%= object.age %>
<% end %>
<% end %>
other methods are
def list_headers(args=[])
args = Array.new(args)
columns = []
args.map { |o| columns << content_tag(:li, o.split(":").first, :style=>"width:#{o.split(":").second}px;") }
content_tag(:ul, columns.join(" ").html_safe, :class=>"list-headers")
end
def my_data_list_for(object, headers=[], &block)
arr = []
object.each do |o|
arr = capture(DataListHelper::MyDataListBuilder.new(o), &block)
end
content_tag(:ol, list_headers(headers) + arr, :class=>"data-list")
end
it is generating an error and i can not figure out why:
ActionView::Template::Error (undefined local variable or method `object' for #<#<Class:0xcaa1ca0>:0xca9ebf4>):
Please help me in it.
This solves the issue.
class MyDataListBuilder
include ActionView::Helpers::TagHelper
include ActionView::Helpers::CaptureHelper
attr_accessor :object, :output_buffer
def initialize(object)
#object = object
#output_buffer = nil
end
def column (&block)
if block_given?
content_tag(:li, capture(self, &block))
else
content_tag(:li, "")
end
end
end

Resources