-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from nebulab/g-merchant-generator
Implement Google merchant generator
- Loading branch information
Showing
6 changed files
with
292 additions
and
2 deletions.
There are no files selected for viewing
13 changes: 11 additions & 2 deletions
13
lib/generators/solidus_feeds/install/templates/initializer.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
132
spec/lib/solidus_feeds/generators/google_merchant_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |