sell_object 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4275ee21382736fccb2d3d09307970eaca2b679e
4
+ data.tar.gz: 816e1a34a75ea6124086bfc8445aa1f73a905105
5
+ SHA512:
6
+ metadata.gz: 143e61bbd203fdfe73fc87f83533b57bc13048ed28980ed49f32af990da86acdd8a5823633f19c8e90a686379af3d05956ef122350582d28c2ad54751468b554
7
+ data.tar.gz: ef4d0e4f751f60fe67b8957460d6bde6d53bce205636f324af952d2ae6b8cc99c55ae4b5114c1b66bc18f889b1ee09fbec368169063fcb5386051ceaff5c0756
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## v0.1.0
2
+
3
+ * initial release with shopping engines: ShoppingUOL (Brazil) and Buscape (Brazil)
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sell_object.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Daniel Ferraz
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # SellObject
2
+
3
+ By [Ima Bold](http://imabold.com).
4
+
5
+ [![Code Climate](https://codeclimate.com/github/imaboldcompany/sell_object.png)](https://codeclimate.com/github/imaboldcompany/sell_object)
6
+
7
+ SellObject is an extensible solution to make it easy exporting ruby objects to be used on price comparison shopping engines. The gem adds helper methods that format your objects, making them ready to be consumable by the supported price comparison engines.
8
+
9
+ Currently, the following shopping engines are supported:
10
+
11
+ * Shopping UOL (Brazil)
12
+ * Buscape (Brazil)
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'sell_object'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ $ bundle
25
+
26
+ Or install it yourself as:
27
+
28
+ $ gem install sell_object
29
+
30
+ After you install SellObject and add it to your Gemfile, you need to run the generator:
31
+
32
+ $ rails generate sell_object:install
33
+
34
+ This will add the SellObject initializer into your Rails config/initializers folder.
35
+
36
+ ## Usage
37
+
38
+ ### Setting up the class
39
+
40
+ Suppose you have a class `Product` and want to make its objects sellable through Shopping UOL.
41
+ You do that by using `sell_through` as shown below:
42
+
43
+ ```ruby
44
+ class Product
45
+ include SellObject
46
+
47
+ sell_through :shopping_uol
48
+ end
49
+ ```
50
+
51
+ After that, to export a given `product` object into a consumable format used in Shopping UOL, just call:
52
+
53
+ ```ruby
54
+ product.to_shopping_uol
55
+ ```
56
+
57
+ In this case, this will generate the XML used by Shopping UOL in its search engine, based on the `product` attributes.
58
+
59
+ Now, take `products` as collection of `Product` and you want to export all the collection into a consumable format used in Shopping UOL. All you have to do is use the class method passing the `products` collection, as shown:
60
+
61
+ ```ruby
62
+ Product.to_shopping_uol products
63
+ ```
64
+
65
+ The same approach works for all the other supported shopping engines.
66
+
67
+ ### Mapping the attributes
68
+
69
+ SellObject comes with a default mapping to be applied through the exporting process. Continuing the Shopping UOL example, the mapping is used to grab the `product` attributes and build the XML tags. The default mapping is defined in the following module:
70
+
71
+ ```ruby
72
+ module SellObject
73
+ module DefaultMappings
74
+ def self.shopping_uol
75
+ {
76
+ :CODIGO => :id,
77
+ :DESCRICAO => :description,
78
+ :PRECO => :price,
79
+ :URL => :url,
80
+ :URL_IMAGEM => :image_url,
81
+ :DEPARTAMENTO => :category
82
+ }
83
+ end
84
+ end
85
+ end
86
+ ```
87
+ If you want to use the default mapping, just make sure that the object responds to the required methods. In our example, `product` would have to respond to `:id`, `:description`, `:price` and so forth.
88
+
89
+ If you want to create your own mapping, you can define a module named with the object's class name + 'Mappings'. For our `product` example, we could define like this:
90
+
91
+ ```ruby
92
+ module SellObject
93
+ module ProductMappings
94
+ def self.shopping_uol
95
+ {
96
+ :CODIGO => :code,
97
+ :DESCRICAO => :details,
98
+ :URL => :web_page
99
+ }
100
+ end
101
+ end
102
+ end
103
+ ```
104
+ In this case, `product` would have to respond to `:code`, `:details` and `:web_page`. Note that we didn't overwrite all the attribute mappings. The leftovers will fall back to the default mapping. So in this example, `product` would still have to respond to the `:price`, `:image_url` and `:category` methods.
105
+
106
+ When defining you own mappings, you can easily unit test them using Test::Unit, Rspec or other testing solutions.
107
+
108
+ ### Store name
109
+
110
+ Some shopping engines require a store name to be included in the final output after the exporting process is over. You can define the store name globally in the SellObject initializer, for example:
111
+
112
+ ```ruby
113
+ SellObject.setup do |config|
114
+ config.store_name = 'Ima Bold Store'
115
+ end
116
+ ```
117
+
118
+ If you want to set the store name dynamically, you can pass it to the exporting methods, for example:
119
+
120
+ ```ruby
121
+ Product.to_buscape products, 'Ima Bold Store'
122
+
123
+ product.to_buscape 'Ima Bold Store'
124
+ ```
125
+
126
+ If you do not pass the store name neither set it up in the initializer, an error is going to be raised when calling those methods for engines that require a store name.
127
+
128
+ Here's the list of shopping engines that require a store name for the exporting process:
129
+
130
+ * Buscape (Brazil)
131
+
132
+ ## Contributing
133
+
134
+ Questions or problems? Please post them on the [issue tracker](https://github.com/imaboldcompany/sell_object/issues).
135
+
136
+ You can contribute by doing the following:
137
+
138
+ 1. Fork it
139
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
140
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
141
+ 4. Push to the branch (`git push origin my-new-feature`)
142
+ 5. Create new Pull Request
143
+
144
+ ## License
145
+
146
+ MIT License.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,16 @@
1
+ require 'rails/generators/base'
2
+ require 'securerandom'
3
+
4
+ module SellObject
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ source_root File.expand_path("../../templates", __FILE__)
8
+
9
+ desc "Creates a SellObject initializer."
10
+
11
+ def copy_initializer
12
+ template "sell_object.rb", "config/initializers/sell_object.rb"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ # Use this hook to configure SellObject settings
2
+ SellObject.setup do |config|
3
+ # Set up the store name to be used on engines that wrap up their outputs under this single name.
4
+ config.store_name = '<%= Rails.application.class.parent_name %>'
5
+ end
@@ -0,0 +1,11 @@
1
+ module SellObject
2
+ module Buscape
3
+ class FormatterProxy < SellObject::FormatterProxy
4
+ def preco(target_method)
5
+ target_value = target.send target_method
6
+ raise ArgumentError, "method expects a number, got #{target_value.class.name}: #{target_value}" unless target_value.is_a? Numeric
7
+ ('%.2f' % target_value).gsub '.', ','
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,43 @@
1
+ module SellObject
2
+ module Buscape
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ def self.wrap_xml(elements, store_name = nil)
8
+ store_name ||= SellObject::Config.store_name
9
+ raise ArgumentError, 'No store name found (nil). You have to either pass it as an argument or set it up in SellObject::Config' if store_name.nil?
10
+ %Q{
11
+ <?xml version="1.0" encoding="UTF-8" ?>
12
+ <!-- #{timestamp} -->
13
+ <#{store_name}>
14
+ <produtos>
15
+ #{elements}
16
+ </produtos>
17
+ </#{store_name}>
18
+ }
19
+ end
20
+
21
+ def self.timestamp
22
+ now = Time.now
23
+ zone_diff = now.strftime("%z").to_i / 100
24
+ time = now.strftime "%Y-%m-%dT%H:%M:%SGMT#{'+' if zone_diff >= 0}#{zone_diff}"
25
+ "Generated at #{time}"
26
+ end
27
+
28
+ module ClassMethods
29
+ # Class methods added on inclusion
30
+
31
+ def to_buscape(objects, store_name = nil)
32
+ elements = objects.map {|obj| SellObject::XmlFormatter.format obj, :buscape, :produto, SellObject::Buscape::FormatterProxy.new(obj) }.join ''
33
+ SellObject::Buscape.wrap_xml elements, store_name
34
+ end
35
+ end
36
+
37
+ # Instance methods added on inclusion
38
+
39
+ def to_buscape(store_name = nil)
40
+ self.class.to_buscape [self], store_name
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,25 @@
1
+ module SellObject
2
+ module DefaultMappings
3
+ def self.shopping_uol
4
+ {
5
+ :CODIGO => :id,
6
+ :DESCRICAO => :description,
7
+ :PRECO => :price,
8
+ :URL => :url,
9
+ :URL_IMAGEM => :image_url,
10
+ :DEPARTAMENTO => :category
11
+ }
12
+ end
13
+
14
+ def self.buscape
15
+ {
16
+ :id_oferta => :id,
17
+ :descricao => :description,
18
+ :preco => :price,
19
+ :link_prod => :url,
20
+ :imagem => :image_url,
21
+ :categoria => :category
22
+ }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ module SellObject
2
+ class FormatterProxy
3
+ attr_accessor :target
4
+
5
+ def initialize(target_object)
6
+ self.target = target_object
7
+ end
8
+
9
+ private
10
+
11
+ def method_missing(method, *args, &block)
12
+ target.send(args.first).to_s
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,30 @@
1
+ module SellObject
2
+ module ShoppingUol
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ def self.wrap_xml(elements)
8
+ %Q{
9
+ <?xml version="1.0" encoding="iso-8859-1" ?>
10
+ <PRODUTOS>
11
+ #{elements}
12
+ </PRODUTOS>
13
+ }
14
+ end
15
+ module ClassMethods
16
+ # Class methods added on inclusion
17
+
18
+ def to_shopping_uol(objects)
19
+ elements = objects.map {|obj| SellObject::XmlFormatter.format obj, :shopping_uol, :PRODUTO }.join ''
20
+ SellObject::ShoppingUol.wrap_xml elements
21
+ end
22
+ end
23
+
24
+ # Instance methods added on inclusion
25
+
26
+ def to_shopping_uol
27
+ self.class.to_shopping_uol [self]
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ module SellObject
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,18 @@
1
+ require 'nokogiri'
2
+
3
+ module SellObject
4
+ module XmlFormatter
5
+ def self.format(obj, engine, xml_root, formatter_proxy = nil)
6
+ mapping = SellObject.mapping_for obj, engine
7
+ formatter_proxy ||= SellObject::FormatterProxy.new obj
8
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
9
+ xml.send xml_root do
10
+ mapping.each do |tag, mapped_method|
11
+ xml.send tag, formatter_proxy.send(tag, mapped_method)
12
+ end
13
+ end
14
+ end
15
+ xml_builder.doc.root.to_xml
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,59 @@
1
+ require 'sell_object/version'
2
+ require 'sell_object/default_mappings'
3
+ require 'sell_object/xml_formatter'
4
+ require 'sell_object/formatter_proxy'
5
+ require 'sell_object/buscape/formatter_proxy'
6
+ require 'sell_object/shopping_uol'
7
+ require 'sell_object/buscape'
8
+
9
+ module SellObject
10
+ def self.included(base)
11
+ base.extend ClassMethods
12
+ end
13
+
14
+ def self.setup
15
+ yield SellObject::Config
16
+ end
17
+
18
+ def self.supported_engines
19
+ %w(shopping_uol buscape)
20
+ end
21
+
22
+ def self.validate_engine(engine)
23
+ raise ArgumentError.new("invalid shopping engine #{engine}") unless supported_engines.include? engine.to_s
24
+ end
25
+
26
+ def self.mapping_for(obj, engine)
27
+ validate_engine engine
28
+ begin
29
+ custom_mappings_hash = eval "SellObject::#{obj.class.name}Mappings.#{engine}"
30
+ rescue
31
+ custom_mappings_hash = {}
32
+ end
33
+ default_mappings_hash = SellObject::DefaultMappings.send engine
34
+ default_mappings_hash.merge custom_mappings_hash
35
+ end
36
+
37
+ module ClassMethods
38
+ def sell_through(*engines)
39
+ raise ArgumentError.new('must pass at least one shopping engine') if engines.empty?
40
+ engines.each do |engine|
41
+ SellObject.validate_engine engine
42
+ camelized_engine = engine.to_s.split('_').map {|w| w.capitalize}.join
43
+ include eval("SellObject::#{camelized_engine}")
44
+ end
45
+ end
46
+ end
47
+
48
+ module Config
49
+ @@store_name = nil
50
+
51
+ def self.store_name
52
+ @@store_name
53
+ end
54
+
55
+ def self.store_name=(name)
56
+ @@store_name = name.gsub(/ +/, '_')
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sell_object/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sell_object"
8
+ spec.version = SellObject::VERSION
9
+ spec.authors = ["Daniel Ferraz"]
10
+ spec.email = ["d.ferrazm@gmail.com"]
11
+ spec.description = %q{Extensible solution to make it easy to export ruby objects to be used on price comparison shopping engines}
12
+ spec.summary = %q{Sell your Ruby on Rails objects on price comparison shopping engines}
13
+ spec.homepage = "http://github.com/imaboldcompany/sell_object"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.required_ruby_version = '>= 1.9.3p125'
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec"
26
+
27
+ spec.add_dependency "nokogiri"
28
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe SellObject::Buscape::FormatterProxy do
4
+ let(:target_object) { double price: 10.5, description: 'Lorem ipsum' }
5
+ let(:formatter) { SellObject::Buscape::FormatterProxy.new target_object }
6
+
7
+ it 'extends from SellObject::FormatterProxy' do
8
+ expect(formatter.is_a? SellObject::FormatterProxy).to be_true
9
+ end
10
+
11
+ describe '#preco' do
12
+ it 'formats the value from the target object to include two decimal cases with comma as sepparator' do
13
+ expect(formatter.preco :price).to eq '10,50'
14
+ end
15
+
16
+ it 'returns an error when the value from the target object is not a number' do
17
+ expect { formatter.preco :description }.to raise_error ArgumentError, 'method expects a number, got String: Lorem ipsum'
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+ include EngineMacros::Buscape
3
+
4
+ class Product
5
+ include SellObject::Buscape
6
+ end
7
+
8
+ describe SellObject::Buscape do
9
+ it_behaves_like 'shopping_engine', :buscape
10
+
11
+ context 'still behaving as shopping_engine' do
12
+ let(:product) { Product.new }
13
+
14
+ before do
15
+ SellObject::XmlFormatter.stub(:format).and_return 'xml element'
16
+ end
17
+
18
+ describe 'class_methods' do
19
+ describe "to_buscape" do
20
+ context 'passing the store name' do
21
+ it 'generates the XML with the given store name as root' do
22
+ expect(remove_xml_noise Product.to_buscape([product], 'great')).to match /<great><produtos>.*<\/produtos><\/great>/
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ describe 'instance methods' do
29
+ describe "to_buscape" do
30
+ context 'passing the store name' do
31
+ it 'generates the XML with the given store name as root' do
32
+ expect(remove_xml_noise product.to_buscape('great')).to match /<great><produtos>.*<\/produtos><\/great>/
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ describe '#wrap_xml' do
40
+ before do
41
+ SellObject::Config.stub(:store_name).and_return 'awesome'
42
+ end
43
+
44
+ it 'inserts the timestamp into the xml' do
45
+ timestamp = Time.now.to_s
46
+ subject.stub(:timestamp).and_return '2014-03-29 13:28:32 -0300'
47
+ expect(subject.wrap_xml 'xml elements').to include '<!-- 2014-03-29 13:28:32 -0300 -->'
48
+ end
49
+
50
+ it 'inserts the given elements inside the <produtos> tag' do
51
+ expect(remove_xml_noise subject.wrap_xml('xml elements')).to include '<produtos>xml elements</produtos>'
52
+ end
53
+
54
+ it 'wraps the <produtos> tag with the store name set up in config' do
55
+ SellObject::Config.stub(:store_name).and_return 'awesome'
56
+ expect(remove_xml_noise subject.wrap_xml('elements')).to match /<awesome><produtos>.*<\/produtos><\/awesome>/
57
+ end
58
+
59
+ it 'raises an error if theres no store name set up' do
60
+ SellObject::Config.stub(:store_name).and_return nil
61
+ expect { subject.wrap_xml 'elements' }.to raise_error ArgumentError, 'No store name found (nil). You have to either pass it as an argument or set it up in SellObject::Config'
62
+ end
63
+
64
+ context 'when passing the store name as an argument' do
65
+ it 'wraps the <produtos> tag with the given store name' do
66
+ expect(remove_xml_noise subject.wrap_xml('elements', 'boring')).to match /<boring><produtos>.*<\/produtos><\/boring>/
67
+ end
68
+ end
69
+ end
70
+
71
+ describe '#timestamp' do
72
+ it 'returns the time now correctly formatted' do
73
+ Time.stub(:now).and_return Time.new(2014,3,28,10,42,48,'-03:00')
74
+ expect(subject.timestamp).to eq 'Generated at 2014-03-28T10:42:48GMT-3'
75
+ Time.stub(:now).and_return Time.new(2011,1,4,17,22,58,'+01:00')
76
+ expect(subject.timestamp).to eq 'Generated at 2011-01-04T17:22:58GMT+1'
77
+ Time.stub(:now).and_return Time.new(2011,1,4,17,22,58,'+00:00')
78
+ expect(subject.timestamp).to eq 'Generated at 2011-01-04T17:22:58GMT+0'
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe SellObject::DefaultMappings do
4
+ describe '#shopping_uol' do
5
+ let(:mapping) { subject.shopping_uol }
6
+
7
+ it 'maps :CODIGO to :id' do
8
+ expect(mapping[:CODIGO]).to eq :id
9
+ end
10
+
11
+ it 'maps :DESCRICAO to :description' do
12
+ expect(mapping[:DESCRICAO]).to eq :description
13
+ end
14
+
15
+ it 'maps :PRECO to :price' do
16
+ expect(mapping[:PRECO]).to eq :price
17
+ end
18
+
19
+ it 'maps :URL to :url' do
20
+ expect(mapping[:URL]).to eq :url
21
+ end
22
+ end
23
+
24
+ describe '#buscape' do
25
+ let(:mapping) { subject.buscape }
26
+
27
+ it 'maps :id_oferta to :id' do
28
+ expect(mapping[:id_oferta]).to eq :id
29
+ end
30
+
31
+ it 'maps :descricao to :description' do
32
+ expect(mapping[:descricao]).to eq :description
33
+ end
34
+
35
+ it 'maps :preco to :price' do
36
+ expect(mapping[:preco]).to eq :price
37
+ end
38
+
39
+ it 'maps :link_prod to :url' do
40
+ expect(mapping[:link_prod]).to eq :url
41
+ end
42
+
43
+ it 'maps :imagem to :image_url' do
44
+ expect(mapping[:imagem]).to eq :image_url
45
+ end
46
+
47
+ it 'maps :categoria to :category' do
48
+ expect(mapping[:categoria]).to eq :category
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe SellObject::FormatterProxy do
4
+ let(:target_object) { double description: 'Lorem ipsum' }
5
+ let(:formatter) { SellObject::FormatterProxy.new target_object }
6
+
7
+ it 'sets the target object on initialization' do
8
+ expect(formatter.target).to eq target_object
9
+ end
10
+
11
+ it 'delegates any method missing to the target method' do
12
+ expect(formatter.foo :description).to eq 'Lorem ipsum'
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+ include EngineMacros::ShoppingUol
3
+
4
+ class Product
5
+ include SellObject::ShoppingUol
6
+ end
7
+
8
+ describe SellObject::ShoppingUol do
9
+ it_behaves_like 'shopping_engine', :shopping_uol
10
+
11
+ describe '#wrap_xml' do
12
+ it 'inserts the given elements inside the <PRODUTOS> tag' do
13
+ expect(remove_xml_noise subject.wrap_xml('xml elements')).to include '<PRODUTOS>xml elements</PRODUTOS>'
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe SellObject::XmlFormatter do
4
+ describe '#format' do
5
+ let(:target_object) { double some_value: 'some value' }
6
+
7
+ before do
8
+ SellObject.stub(:mapping_for).with(target_object, :foo).and_return({some_tag: :some_value})
9
+ end
10
+
11
+ it 'formats an object into a xml element based on its mapping, given an engine and a xml root' do
12
+ expect(remove_xml_noise subject.format(target_object, :foo, :xml_root)).to eq remove_xml_noise %q{
13
+ <xml_root>
14
+ <some_tag>some value</some_tag>
15
+ </xml_root>
16
+ }
17
+ end
18
+
19
+ context 'passing a formatter proxy' do
20
+ let(:proxy) { double some_tag: 'proxied value' }
21
+
22
+ it 'uses the proxy on the format process' do
23
+ expect(remove_xml_noise subject.format(target_object, :foo, :xml_root, proxy)).to eq remove_xml_noise %q{
24
+ <xml_root>
25
+ <some_tag>proxied value</some_tag>
26
+ </xml_root>
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,120 @@
1
+ require 'spec_helper'
2
+
3
+ describe SellObject do
4
+ context 'on inclusion' do
5
+ before do
6
+ class TargetClass
7
+ extend SellObject::ClassMethods
8
+ end
9
+ end
10
+
11
+ after do
12
+ Object.send :remove_const, :TargetClass
13
+ end
14
+
15
+ it 'makes the target class extend SellObject::ClassMethods' do
16
+ expect(TargetClass.is_a? SellObject::ClassMethods).to be_true
17
+ end
18
+
19
+ describe 'ClassMethods' do
20
+ describe '#sell_through' do
21
+ context 'passing invalid arguments' do
22
+ it 'raises ArgumentError with an invalid engine' do
23
+ expect { TargetClass.sell_through :foo }.to raise_error ArgumentError, 'invalid shopping engine foo'
24
+ end
25
+
26
+ it 'raises ArgumentError with a blank arguments' do
27
+ expect { TargetClass.sell_through }.to raise_error ArgumentError, 'must pass at least one shopping engine'
28
+ end
29
+ end
30
+
31
+ context 'buscape' do
32
+ it 'includes the module SellObject::Buscape in the target class' do
33
+ TargetClass.sell_through :buscape
34
+ expect(TargetClass.included_modules).to include SellObject::Buscape
35
+ end
36
+ end
37
+
38
+ context 'shopping_uol' do
39
+ it 'includes the module SellObject::ShoppingUol in the target class' do
40
+ TargetClass.sell_through :shopping_uol
41
+ expect(TargetClass.included_modules).to include SellObject::ShoppingUol
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ describe '#supported_engines' do
49
+ it 'returns the supported shopping engines array' do
50
+ expect(subject.supported_engines).to match_array %w(shopping_uol buscape)
51
+ end
52
+ end
53
+
54
+ describe '#setup' do
55
+ it 'yields the SellObject::Config module' do
56
+ config_module = nil
57
+ subject.setup do |config|
58
+ config_module = config
59
+ end
60
+ expect(config_module.name).to eq 'SellObject::Config'
61
+ end
62
+ end
63
+
64
+ describe '#mapping_for' do
65
+ before do
66
+ SellObject.stub(:supported_engines).and_return %w(bar)
67
+ SellObject::DefaultMappings.stub(:bar).and_return({t1: :v1, t2: :v2})
68
+
69
+ class TargetObject
70
+ end
71
+ end
72
+
73
+ after do
74
+ Object.send :remove_const, :TargetObject
75
+ end
76
+
77
+ it 'raises an error if the given engine is invalid' do
78
+ expect { subject.mapping_for TargetObject.new, :foo }.to raise_error ArgumentError, 'invalid shopping engine foo'
79
+ end
80
+
81
+ context 'when there\s no custom mapping defined' do
82
+ it 'returns the default mapping hash for the given object' do
83
+ expect(subject.mapping_for TargetObject.new, :bar).to eq({t1: :v1, t2: :v2})
84
+ end
85
+ end
86
+
87
+ context 'when there\s a custom mapping defined' do
88
+ before do
89
+ module SellObject::TargetObjectMappings
90
+ def self.bar
91
+ {t2: :v2_custom, t3: :v3}
92
+ end
93
+ end
94
+ end
95
+
96
+ after do
97
+ SellObject.send :remove_const, :TargetObjectMappings
98
+ end
99
+
100
+ it 'returns the default mapping hash merged with the custom mapping for the given object' do
101
+ expect(subject.mapping_for TargetObject.new, :bar).to eq({t1: :v1, t2: :v2_custom, t3: :v3})
102
+ end
103
+ end
104
+ end
105
+
106
+ describe 'Config' do
107
+ let(:subject) { SellObject::Config }
108
+
109
+ describe '#store_name' do
110
+ it 'initializes it with nil' do
111
+ expect(subject.store_name).to be_nil
112
+ end
113
+
114
+ it 'underscores the given assigned name' do
115
+ subject.store_name = 'My Store Name'
116
+ expect(subject.store_name).to eq 'My_Store_Name'
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,11 @@
1
+ require 'sell_object'
2
+
3
+ ENV["RAILS_ENV"] ||= 'test'
4
+
5
+ # Requires supporting ruby files with custom matchers and macros, etc,
6
+ # in spec/support/ and its subdirectories.
7
+ Dir["./spec/support/**/*.rb"].sort.each {|f| require f}
8
+
9
+ RSpec.configure do |config|
10
+ config.include SpecMacros
11
+ end
@@ -0,0 +1,127 @@
1
+ module EngineMacros
2
+ module Buscape
3
+ DEFAULT_MAPPING_FIXTURE_ONE = %q{
4
+ <?xml version="1.0" encoding="UTF-8" ?>
5
+ <awesome_store>
6
+ <produtos>
7
+ <produto>
8
+ <id_oferta>PR1</id_oferta>
9
+ <descricao>Some lame product</descricao>
10
+ <preco>10,50</preco>
11
+ <link_prod>http://example.com/lame-product</link_prod>
12
+ <imagem>http://example.com/images/lame-product.png</imagem>
13
+ <categoria>Electronics</categoria>
14
+ </produto>
15
+ </produtos>
16
+ </awesome_store>
17
+ }
18
+
19
+ DEFAULT_MAPPING_FIXTURE_MANY = %q{
20
+ <?xml version="1.0" encoding="UTF-8" ?>
21
+ <awesome_store>
22
+ <produtos>
23
+ <produto>
24
+ <id_oferta>PR1</id_oferta>
25
+ <descricao>Some lame product</descricao>
26
+ <preco>10,50</preco>
27
+ <link_prod>http://example.com/lame-product</link_prod>
28
+ <imagem>http://example.com/images/lame-product.png</imagem>
29
+ <categoria>Electronics</categoria>
30
+ </produto>
31
+ <produto>
32
+ <id_oferta>PR2</id_oferta>
33
+ <descricao>Some boring product</descricao>
34
+ <preco>7,00</preco>
35
+ <link_prod>http://example.com/boring-product</link_prod>
36
+ <imagem>http://example.com/images/boring-product.png</imagem>
37
+ <categoria>Kitchenware</categoria>
38
+ </produto>
39
+ </produtos>
40
+ </awesome_store>
41
+ }
42
+
43
+ CUSTOM_MAPPING_FIXTURE_MANY = %q{
44
+ <?xml version="1.0" encoding="UTF-8" ?>
45
+ <awesome_store>
46
+ <produtos>
47
+ <produto>
48
+ <id_oferta>PR1</id_oferta>
49
+ <descricao>My custom lame product description</descricao>
50
+ <preco>10,50</preco>
51
+ <link_prod>http://example.com/custom-lame-product</link_prod>
52
+ <imagem>http://example.com/images/lame-product.png</imagem>
53
+ <categoria>Electronics</categoria>
54
+ </produto>
55
+ <produto>
56
+ <id_oferta>PR2</id_oferta>
57
+ <descricao>My custom boring product description</descricao>
58
+ <preco>7,00</preco>
59
+ <link_prod>http://example.com/custom-boring-product</link_prod>
60
+ <imagem>http://example.com/images/boring-product.png</imagem>
61
+ <categoria>Kitchenware</categoria>
62
+ </produto>
63
+ </produtos>
64
+ </awesome_store>
65
+ }
66
+ end
67
+
68
+ module ShoppingUol
69
+ DEFAULT_MAPPING_FIXTURE_ONE = %q{
70
+ <?xml version="1.0" encoding="iso-8859-1" ?>
71
+ <PRODUTOS>
72
+ <PRODUTO>
73
+ <CODIGO>PR1</CODIGO>
74
+ <DESCRICAO>Some lame product</DESCRICAO>
75
+ <PRECO>10.5</PRECO>
76
+ <URL>http://example.com/lame-product</URL>
77
+ <URL_IMAGEM>http://example.com/images/lame-product.png</URL_IMAGEM>
78
+ <DEPARTAMENTO>Electronics</DEPARTAMENTO>
79
+ </PRODUTO>
80
+ </PRODUTOS>
81
+ }
82
+
83
+ DEFAULT_MAPPING_FIXTURE_MANY = %q{
84
+ <?xml version="1.0" encoding="iso-8859-1" ?>
85
+ <PRODUTOS>
86
+ <PRODUTO>
87
+ <CODIGO>PR1</CODIGO>
88
+ <DESCRICAO>Some lame product</DESCRICAO>
89
+ <PRECO>10.5</PRECO>
90
+ <URL>http://example.com/lame-product</URL>
91
+ <URL_IMAGEM>http://example.com/images/lame-product.png</URL_IMAGEM>
92
+ <DEPARTAMENTO>Electronics</DEPARTAMENTO>
93
+ </PRODUTO>
94
+ <PRODUTO>
95
+ <CODIGO>PR2</CODIGO>
96
+ <DESCRICAO>Some boring product</DESCRICAO>
97
+ <PRECO>7</PRECO>
98
+ <URL>http://example.com/boring-product</URL>
99
+ <URL_IMAGEM>http://example.com/images/boring-product.png</URL_IMAGEM>
100
+ <DEPARTAMENTO>Kitchenware</DEPARTAMENTO>
101
+ </PRODUTO>
102
+ </PRODUTOS>
103
+ }
104
+
105
+ CUSTOM_MAPPING_FIXTURE_MANY = %q{
106
+ <?xml version="1.0" encoding="iso-8859-1" ?>
107
+ <PRODUTOS>
108
+ <PRODUTO>
109
+ <CODIGO>PR1</CODIGO>
110
+ <DESCRICAO>My custom lame product description</DESCRICAO>
111
+ <PRECO>10.5</PRECO>
112
+ <URL>http://example.com/custom-lame-product</URL>
113
+ <URL_IMAGEM>http://example.com/images/lame-product.png</URL_IMAGEM>
114
+ <DEPARTAMENTO>Electronics</DEPARTAMENTO>
115
+ </PRODUTO>
116
+ <PRODUTO>
117
+ <CODIGO>PR2</CODIGO>
118
+ <DESCRICAO>My custom boring product description</DESCRICAO>
119
+ <PRECO>7</PRECO>
120
+ <URL>http://example.com/custom-boring-product</URL>
121
+ <URL_IMAGEM>http://example.com/images/boring-product.png</URL_IMAGEM>
122
+ <DEPARTAMENTO>Kitchenware</DEPARTAMENTO>
123
+ </PRODUTO>
124
+ </PRODUTOS>
125
+ }
126
+ end
127
+ end
@@ -0,0 +1,76 @@
1
+ shared_examples_for 'shopping_engine' do |engine|
2
+ camelized_engine = engine.to_s.split('_').map {|w| w.capitalize}.join
3
+
4
+ let(:macros) { eval("EngineMacros::#{camelized_engine}") }
5
+ let(:lame_product) { Product.new }
6
+ let(:boring_product) { Product.new }
7
+ let(:products) { [lame_product, boring_product] }
8
+
9
+ before do
10
+ lame_product.stub(:id).and_return 'PR1'
11
+ lame_product.stub(:description).and_return 'Some lame product'
12
+ lame_product.stub(:price).and_return 10.5
13
+ lame_product.stub(:category).and_return 'Electronics'
14
+ lame_product.stub(:url).and_return 'http://example.com/lame-product'
15
+ lame_product.stub(:image_url).and_return 'http://example.com/images/lame-product.png'
16
+
17
+ boring_product.stub(:id).and_return 'PR2'
18
+ boring_product.stub(:description).and_return 'Some boring product'
19
+ boring_product.stub(:price).and_return 7
20
+ boring_product.stub(:category).and_return 'Kitchenware'
21
+ boring_product.stub(:url).and_return 'http://example.com/boring-product'
22
+ boring_product.stub(:image_url).and_return 'http://example.com/images/boring-product.png'
23
+
24
+ SellObject::Config.stub(:store_name).and_return 'awesome_store'
25
+ end
26
+
27
+ describe 'class_methods' do
28
+ describe "#to_#{engine}" do
29
+ context 'using default mappings' do
30
+ it 'generates the XML accordingly with all the objects attributes' do
31
+ expect(remove_xml_noise Product.send("to_#{engine}", products)).to eq remove_xml_noise macros::DEFAULT_MAPPING_FIXTURE_MANY
32
+ end
33
+ end
34
+
35
+ context 'using custom mappings' do
36
+ before do
37
+ lame_product.stub(:custom_description).and_return 'My custom lame product description'
38
+ lame_product.stub(:custom_url).and_return 'http://example.com/custom-lame-product'
39
+ boring_product.stub(:custom_description).and_return 'My custom boring product description'
40
+ boring_product.stub(:custom_url).and_return 'http://example.com/custom-boring-product'
41
+
42
+ module SellObject::ProductMappings
43
+ def self.buscape
44
+ { :descricao => :custom_description, :link_prod => :custom_url }
45
+ end
46
+
47
+ def self.shopping_uol
48
+ { :DESCRICAO => :custom_description, :URL => :custom_url }
49
+ end
50
+ end
51
+ end
52
+
53
+ after do
54
+ SellObject.send :remove_const, :ProductMappings
55
+ end
56
+
57
+ it 'generates the XML accordingly with all the objects attributes and the custom mappings' do
58
+ expect(remove_xml_noise Product.send("to_#{engine}", products)).to eq remove_xml_noise macros::CUSTOM_MAPPING_FIXTURE_MANY
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ describe 'instance methods' do
65
+ describe "#to_#{engine}" do
66
+ it 'generates the XML accordingly with the object attributes' do
67
+ expect(remove_xml_noise lame_product.send("to_#{engine}")).to eq remove_xml_noise macros::DEFAULT_MAPPING_FIXTURE_ONE
68
+ end
69
+
70
+ # it "calls the #to_#{engine} class method passing the instance wrapped in an array" do
71
+ # Product.should_receive(:"to_#{engine}").with [lame_product]
72
+ # lame_product.send "to_#{engine}"
73
+ # end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,5 @@
1
+ module SpecMacros
2
+ def remove_xml_noise(xml)
3
+ xml.gsub("\n",'').gsub("\t",'').gsub(/<!--.*-->/, '').gsub />[ \t]+</, '><'
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,141 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sell_object
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Ferraz
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-03-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: nokogiri
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Extensible solution to make it easy to export ruby objects to be used
70
+ on price comparison shopping engines
71
+ email:
72
+ - d.ferrazm@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - .gitignore
78
+ - .rspec
79
+ - CHANGELOG.md
80
+ - Gemfile
81
+ - LICENSE.txt
82
+ - README.md
83
+ - Rakefile
84
+ - lib/generators/sell_object/install_generator.rb
85
+ - lib/generators/templates/sell_object.rb
86
+ - lib/sell_object.rb
87
+ - lib/sell_object/buscape.rb
88
+ - lib/sell_object/buscape/formatter_proxy.rb
89
+ - lib/sell_object/default_mappings.rb
90
+ - lib/sell_object/formatter_proxy.rb
91
+ - lib/sell_object/shopping_uol.rb
92
+ - lib/sell_object/version.rb
93
+ - lib/sell_object/xml_formatter.rb
94
+ - sell_object.gemspec
95
+ - spec/sell_object/buscape/formatter_proxy_spec.rb
96
+ - spec/sell_object/buscape_spec.rb
97
+ - spec/sell_object/default_mappings_spec.rb
98
+ - spec/sell_object/formatter_proxy_spec.rb
99
+ - spec/sell_object/shopping_uol_spec.rb
100
+ - spec/sell_object/xml_formatter_spec.rb
101
+ - spec/sell_object_spec.rb
102
+ - spec/spec_helper.rb
103
+ - spec/support/engine_macros.rb
104
+ - spec/support/shared_examples/shopping_engine.rb
105
+ - spec/support/spec_macros.rb
106
+ homepage: http://github.com/imaboldcompany/sell_object
107
+ licenses:
108
+ - MIT
109
+ metadata: {}
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - '>='
117
+ - !ruby/object:Gem::Version
118
+ version: 1.9.3p125
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - '>='
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 2.0.14
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: Sell your Ruby on Rails objects on price comparison shopping engines
130
+ test_files:
131
+ - spec/sell_object/buscape/formatter_proxy_spec.rb
132
+ - spec/sell_object/buscape_spec.rb
133
+ - spec/sell_object/default_mappings_spec.rb
134
+ - spec/sell_object/formatter_proxy_spec.rb
135
+ - spec/sell_object/shopping_uol_spec.rb
136
+ - spec/sell_object/xml_formatter_spec.rb
137
+ - spec/sell_object_spec.rb
138
+ - spec/spec_helper.rb
139
+ - spec/support/engine_macros.rb
140
+ - spec/support/shared_examples/shopping_engine.rb
141
+ - spec/support/spec_macros.rb