Skip to content

Commit

Permalink
Merge pull request #6 from nebulab/g-merchant-generator
Browse files Browse the repository at this point in the history
Implement Google merchant generator
  • Loading branch information
nirebu authored Nov 27, 2020
2 parents 225063f + 831b179 commit 605ac70
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 2 deletions.
13 changes: 11 additions & 2 deletions lib/generators/solidus_feeds/install/templates/initializer.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
# frozen_string_literal: true

SolidusFeeds.configure do |config|
# TODO: Remember to change this with the actual preferences you have implemented!
# config.sample_preference = 'sample_value'
# The store name appearing on the feeds
# config.title = 'My store name'
#
# The URL appearing on the feeds
# config.link = 'https://example.com'
#
# The description appearing on the feeds
# config.description = 'Find out about new products on https://example.com'
#
# The language of the feeds
# config.language = 'en-us'
end
2 changes: 2 additions & 0 deletions lib/solidus_feeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@
require 'solidus_feeds/engine'

require 'solidus_feeds/publishers/s3'

require 'solidus_feeds/generators/google_merchant'
18 changes: 18 additions & 0 deletions lib/solidus_feeds/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ def feeds
end

class << self
attr_writer :title, :link, :description, :language

def configuration
@configuration ||= Configuration.new
end
Expand All @@ -37,5 +39,21 @@ def reset_config!
def configure
yield configuration
end

def title
@title ||= Spree::Store.default.name
end

def link
@link ||= "https://#{Spree::Store.default.url}"
end

def description
@description ||= "Find out about new products on https://#{Spree::Store.default.url} first!"
end

def language
@language ||= 'en-us'
end
end
end
106 changes: 106 additions & 0 deletions lib/solidus_feeds/generators/google_merchant.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# frozen_string_literal: true

require 'builder'

module SolidusFeeds
module Generators
# The GoogleMerchant XML feed as described in https://support.google.com/merchants/answer/7052112.
class GoogleMerchant
attr_accessor :products, :host

def initialize(products, host:)
self.products = products
self.host = host
end

def call(io)
builder = Builder::XmlMarkup.new(target: io, indent: 0)
render_template(builder)
end

def render_template(xml)
xml.rss version: "2.0", "xmlns:g" => "http://base.google.com/ns/1.0" do
xml.channel do
xml.title SolidusFeeds.title
xml.link SolidusFeeds.link
xml.description SolidusFeeds.description
xml.language SolidusFeeds.language
products.find_each do |product|
xml.item do
xml.tag! 'g:id', id(product)
xml.tag! 'g:title', title(product)
xml.tag! 'g:description', description(product)
xml.tag! 'g:link', link(product)
xml.tag! 'g:image_link', image_link(product)
xml.tag! 'g:condition', condition(product)
xml.tag! 'g:price', price(product)
xml.tag! 'g:availability', availability(product)
xml.tag! 'g:brand', brand(product)
xml.tag! 'g:mpn', mpn(product)
xml.tag! 'g:google_product_category', google_product_category_id(product)
end
end
end
end
end

def id(product)
product.id
end

def title(product)
product.name
end

def description(product)
product.description
end

def link(product)
spree_routes.product_url(product, host: host)
end

def image_link(product)
return unless product.images.any?

attachment_url = product.images.first.attachment.url(:large)
asset_host = ActionController::Base.asset_host

URI.join(asset_host, attachment_url).to_s
end

# Must be "new", "refurbished", or "used".
def condition(product)
product.property("condition") || "new"
end

def price(product)
Spree::Money.new(product.price).money.format(symbol: false, with_currency: true)
end

# Must be "in stock", "preorder" or "out of stock"
def availability(product)
product.master.in_stock? ? 'in stock' : 'out of stock'
end

def brand(product)
product.property("brand") || SolidusFeeds.title
end

def mpn(product)
product.master.sku
end

def google_product_category_id(product)
# Must be selected from https://support.google.com/merchants/answer/1705911
product.property("google_product_category_id")
end

private

def spree_routes
@spree_routes ||= Spree::Core::Engine.routes.url_helpers
end
end
end
end
132 changes: 132 additions & 0 deletions spec/lib/solidus_feeds/generators/google_merchant_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# frozen_string_literal: true

require 'spec_helper'
require 'stringio'

RSpec.describe SolidusFeeds::Generators::GoogleMerchant do
subject(:generator) { described_class.new(products, host: 'https://example.com') }

let(:products) do
Spree::Product.where(id: [
create(:product, id: 123, slug: 'pro1', name: 'A product', price: 123.45, description: 'product description', sku: 'PRD1'), # rubocop:disable Layout/LineLength
create(:product, id: 456, slug: 'pro2', name: 'Another product', price: 678.90, description: 'another product description', sku: 'PRD2'), # rubocop:disable Layout/LineLength
]).order(:id)
end
let(:product) { products.first }
let(:store) { instance_double(Spree::Store, name: "The Best Store", url: "example.com") }

before { allow(Spree::Store).to receive(:default).and_return(store) }

before(:each, with_images: true) do
allow(ActionController::Base).to receive(:asset_host).and_return('https://assets.example.com')
Spree::Image.create! viewable: products.first.master, attachment_file_name: 'foo.png', id: 234
end

describe '#call', :with_images do
describe 'generated XML' do
def expect_xml_content(xpath)
expect(xml.xpath(xpath, 'g' => "http://base.google.com/ns/1.0").text)
end

let(:xml) do
io = StringIO.new
generator.call(io)
Nokogiri::XML(io.string)
end

it 'includes the tags with the expected values' do
aggregate_failures do
expect_xml_content('//channel/title').to eq('The Best Store')
expect_xml_content('//channel/link').to eq('https://example.com')
expect_xml_content('//channel/description').to eq('Find out about new products on https://example.com first!')
expect_xml_content('//channel/language').to eq('en-us')
expect_xml_content('//channel/item[1]/g:id').to eq('123')
expect_xml_content('//channel/item[1]/g:title').to eq('A product')
expect_xml_content('//channel/item[1]/g:description').to eq('product description')
expect_xml_content('//channel/item[1]/g:link').to eq('https://example.com/products/pro1')
expect_xml_content('//channel/item[1]/g:image_link').to eq("https://assets.example.com/spree/products/234/large/foo.png") # rubocop:disable Layout/LineLength
expect_xml_content('//channel/item[1]/g:condition').to eq('new')
expect_xml_content('//channel/item[1]/g:price').to eq('123.45 USD')
expect_xml_content('//channel/item[1]/g:availability').to eq('out of stock')
expect_xml_content('//channel/item[1]/g:brand').to eq('The Best Store')
expect_xml_content('//channel/item[1]/g:mpn').to eq('PRD1')
expect_xml_content('//channel/item[1]/g:google_product_category').to eq('')
end
end
end
end

specify '#id' do
product = instance_double(Spree::Product, id: 789)

expect(generator.id(product)).to eq(789)
end

specify '#title' do
product = instance_double(Spree::Product, name: "Foo Bar")

expect(generator.title(product)).to eq("Foo Bar")
end

specify '#description' do
product = instance_double(Spree::Product, description: "Foo Bar")

expect(generator.description(product)).to eq("Foo Bar")
end

specify '#link' do
product = instance_double(Spree::Product, to_param: "123-my-product")

expect(generator.link(product)).to eq("https://example.com/products/123-my-product")
end

specify '#image_link', :with_images do
expect(generator.image_link(products.first)).to eq("https://assets.example.com/spree/products/234/large/foo.png")
expect(generator.image_link(products.second)).to eq(nil)
end

specify '#condition' do
products.first.set_property("condition", "foo-bar")

expect(generator.condition(product)).to eq("foo-bar")
end

specify '#price' do
product = instance_double(Spree::Product, price: 321.45);

expect(generator.price(product)).to eq("321.45 USD")
end

describe '#availability' do
it 'is "in stock" when available' do
allow(product.master).to receive(:in_stock?).and_return(true)

expect(generator.availability(product)).to eq("in stock")
end

it 'is "out of stock" when unavailable' do
allow(product.master).to receive(:in_stock?).and_return(false)

expect(generator.availability(product)).to eq("out of stock")
end
end

specify '#brand' do
product.set_property("brand", "foo-bar")

expect(generator.brand(product)).to eq("foo-bar")
expect(generator.brand(products.second)).to eq("The Best Store")
end

specify '#mpn' do
allow(product.master).to receive(:sku).and_return("FOOO_123_SKU")

expect(generator.mpn(product)).to eq("FOOO_123_SKU")
end

specify '#google_product_category_id' do
product.set_property("google_product_category_id", "123456")

expect(generator.google_product_category_id(product)).to eq("123456")
end
end
23 changes: 23 additions & 0 deletions spec/lib/solidus_feeds/solidus_feeds_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

RSpec.describe SolidusFeeds do
let(:store) { instance_double(Spree::Store, name: "The Best Store", url: "example.com") }

before { allow(Spree::Store).to receive(:default).and_return(store) }

specify '.title' do
expect(described_class.title).to eq("The Best Store")
end

specify '.link' do
expect(described_class.link).to eq("https://example.com")
end

specify '.description' do
expect(described_class.description).to eq("Find out about new products on https://example.com first!")
end

specify '.language' do
expect(described_class.language).to eq("en-us")
end
end

0 comments on commit 605ac70

Please sign in to comment.