goods 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/goods/xml.rb ADDED
@@ -0,0 +1,129 @@
1
+ require "nokogiri"
2
+
3
+ module Goods
4
+ class XML
5
+ class InvalidFormatError < StandardError; end
6
+
7
+ def initialize(string, url = nil, encoding = nil)
8
+ @xml_source = Nokogiri::XML::Document.parse(string, url, encoding)
9
+ end
10
+
11
+ def categories
12
+ @categories ||= extract_categories
13
+ end
14
+
15
+ def currencies
16
+ @currencies ||= extract_currencies
17
+ end
18
+
19
+ def offers
20
+ @offers ||= extract_offers
21
+ end
22
+
23
+
24
+ private
25
+
26
+
27
+ def catalog_node
28
+ @xml_source / "yml_catalog"
29
+ end
30
+
31
+ def shop_node
32
+ catalog_node / "shop"
33
+ end
34
+
35
+ # Categories part
36
+ def categories_node
37
+ shop_node / "categories" / "category"
38
+ end
39
+
40
+ def extract_categories
41
+ categories_node.map do |category|
42
+ category_node_to_hash(category)
43
+ end
44
+ end
45
+
46
+ def category_node_to_hash(category)
47
+ category_hash = {
48
+ id: category.attribute("id").value,
49
+ name: category.text
50
+ }
51
+ category_hash[:parent_id] = if category.attribute("parentId")
52
+ category.attribute("parentId").value
53
+ else
54
+ nil
55
+ end
56
+ category_hash
57
+ end
58
+
59
+ # Currencies part
60
+ def currencies_node
61
+ shop_node / "currencies" / "currency"
62
+ end
63
+
64
+ def extract_currencies
65
+ currencies_node.map do |currency|
66
+ currency_node_to_hash(currency)
67
+ end
68
+ end
69
+
70
+ def currency_node_to_hash(currency)
71
+ currency_hash = {
72
+ id: currency.attribute("id").value
73
+ }
74
+
75
+ attributes_with_defaults = {
76
+ rate: "1",
77
+ plus: "0"
78
+ }
79
+ attributes_with_defaults.each do |attr, default|
80
+ currency_hash[attr] = if currency.attribute(attr.to_s)
81
+ currency.attribute(attr.to_s).value
82
+ else
83
+ default
84
+ end
85
+ end
86
+
87
+ currency_hash
88
+ end
89
+
90
+ #Offers part
91
+ def offers_node
92
+ shop_node / "offers" / "offer"
93
+ end
94
+
95
+ def extract_offers
96
+ offers_node.map do |offer|
97
+ offer_node_to_hash(offer)
98
+ end
99
+ end
100
+
101
+ def offer_node_to_hash(offer)
102
+ offer_hash = {
103
+ id: offer.attribute("id").value
104
+ }
105
+
106
+ offer_hash[:available] = if attr = offer.attribute("available")
107
+ !! (attr.value =~ /true/)
108
+ else
109
+ true
110
+ end
111
+ {
112
+ url: "url",
113
+ currency_id: "currencyId",
114
+ category_id: "categoryId",
115
+ picture: "picture",
116
+ description: "description",
117
+ name: "name",
118
+ vendor: "vendor",
119
+ model: "model"
120
+ }.each do |property, node|
121
+ offer_hash[property] = (el = offer.xpath(node).first) ? el.text.strip : nil
122
+ end
123
+
124
+ offer_hash[:price] = offer.xpath("price").first.text.to_f
125
+
126
+ offer_hash
127
+ end
128
+ end
129
+ end
data/lib/goods.rb ADDED
@@ -0,0 +1,42 @@
1
+ require 'open-uri'
2
+ require "goods/version"
3
+ require "goods/xml/validator"
4
+ require "goods/xml"
5
+ require "goods/containable"
6
+ require "goods/container"
7
+ require "goods/category"
8
+ require "goods/categories_list"
9
+ require "goods/offer"
10
+ require "goods/offers_list"
11
+ require "goods/currency"
12
+ require "goods/currencies_list"
13
+ require "goods/catalog"
14
+
15
+ module Goods
16
+ def self.from_string(xml_string, url=nil, encoding=nil)
17
+ validator = XML::Validator.new
18
+ if validator.valid? xml_string
19
+ Catalog.new(string: xml_string, url: url, encoding: encoding)
20
+ else
21
+ raise XML::InvalidFormatError, validator.error
22
+ end
23
+ end
24
+
25
+ def self.from_url(url, encoding=nil)
26
+ xml_string = self.load url
27
+ from_string(xml_string, url, encoding)
28
+ end
29
+
30
+ def self.from_file(file, encoding=nil)
31
+ xml_string = self.load file
32
+ from_string(xml_string, nil, encoding)
33
+ end
34
+
35
+ private
36
+
37
+ def self.load(source)
38
+ open(source) do |f|
39
+ f.read
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,149 @@
1
+ <!ELEMENT yml_catalog (shop)>
2
+ <!ATTLIST yml_catalog
3
+ date CDATA #REQUIRED
4
+ count CDATA #IMPLIED>
5
+
6
+ <!ELEMENT shop (name, company, url, phone?, platform?, version?, agency?, email*, currencies, categories, store?, pickup?, delivery?, deliveryIncluded?, local_delivery_cost?, adult?, offers)>
7
+ <!ELEMENT company (#PCDATA)>
8
+ <!ELEMENT phone (#PCDATA)>
9
+
10
+ <!ELEMENT currencies (currency+)>
11
+ <!ELEMENT currency EMPTY>
12
+ <!ATTLIST currency
13
+ id (RUR|RUB|USD|BYR|KZT|EUR|UAH) #REQUIRED
14
+ rate CDATA "1"
15
+ plus CDATA "0">
16
+
17
+
18
+ <!ELEMENT categories (category+)>
19
+ <!ELEMENT category (#PCDATA)>
20
+ <!ATTLIST category
21
+ id CDATA #REQUIRED
22
+ parentId CDATA #IMPLIED
23
+ tid CDATA #IMPLIED
24
+ yid CDATA #IMPLIED>
25
+
26
+ <!ELEMENT offers (offer+)>
27
+ <!ELEMENT offer (url?, buyurl?, price, wprice?, currencyId, xCategory?, categoryId+, market_category?,
28
+ picture*, store?, pickup?, delivery?, deliveryIncluded?, local_delivery_cost?, orderingTime?,
29
+ ((typePrefix?, vendor, vendorCode?, model, (provider, tarifplan?)?) |
30
+ (author?, name, publisher?, series?, year?, ISBN?, volume?, part?, language?, binding?, page_extent?, table_of_contents?) |
31
+ (author?, name, publisher?, series?, year?, ISBN?, volume?, part?, language?, table_of_contents?, performed_by?, performance_type?, storage?, format?, recording_length?) |
32
+ (artist?, title, year?, media?, starring?, director?, originalName?, country?) |
33
+ (worldRegion?, country?, region?, days, dataTour*, name, hotel_stars?, room?, meal?, included, transport, price_min?, price_max?, options?) |
34
+ (name, place, hall?, hall_part?, date, is_premiere?, is_kids?) |
35
+ (name, vendor?, vendorCode?)
36
+ ),
37
+ aliases?, additional*, description?, sales_notes?, promo?,
38
+ manufacturer_warranty?, country_of_origin?, downloadable?, adult?,
39
+ age?,
40
+ barcode*,
41
+ param*,
42
+ related_offer*
43
+ )>
44
+ <!ATTLIST offer
45
+ id CDATA #IMPLIED
46
+ group_id CDATA #IMPLIED
47
+ type (vendor.model | book | audiobook | artist.title | tour | ticket | event-ticket) #IMPLIED
48
+ available (true | false) #IMPLIED
49
+ bid CDATA #IMPLIED
50
+ cbid CDATA #IMPLIED>
51
+
52
+ <!ELEMENT url (#PCDATA)>
53
+ <!ELEMENT store (#PCDATA)>
54
+ <!ELEMENT email (#PCDATA)>
55
+ <!ELEMENT platform (#PCDATA)>
56
+ <!ELEMENT version (#PCDATA)>
57
+ <!ELEMENT agency (#PCDATA)>
58
+ <!ELEMENT buyurl (#PCDATA)>
59
+ <!ELEMENT picture (#PCDATA)>
60
+ <!ELEMENT pickup (#PCDATA)>
61
+ <!ELEMENT delivery (#PCDATA)>
62
+ <!ELEMENT deliveryIncluded EMPTY>
63
+ <!ELEMENT local_delivery_cost (#PCDATA)>
64
+ <!ELEMENT orderingTime (onstock?, ordering, deliveryTime?)>
65
+ <!ELEMENT onstock EMPTY>
66
+ <!ELEMENT ordering (#PCDATA)>
67
+ <!ATTLIST ordering
68
+ hours CDATA #IMPLIED>
69
+
70
+ <!ELEMENT deliveryTime EMPTY>
71
+ <!ELEMENT price (#PCDATA)>
72
+ <!ELEMENT wprice (#PCDATA)>
73
+ <!ELEMENT currencyId (#PCDATA)>
74
+ <!ELEMENT categoryId (#PCDATA)>
75
+ <!ATTLIST categoryId
76
+ type (Yandex | Torg | Own) "Own" >
77
+
78
+ <!ELEMENT market_category (#PCDATA)>
79
+ <!ELEMENT typePrefix (#PCDATA)>
80
+ <!ELEMENT vendor (#PCDATA)>
81
+ <!ELEMENT vendorCode (#PCDATA)>
82
+ <!ELEMENT model (#PCDATA)>
83
+ <!ELEMENT author (#PCDATA)>
84
+ <!ELEMENT name (#PCDATA)>
85
+ <!ELEMENT publisher (#PCDATA)>
86
+ <!ELEMENT ISBN (#PCDATA)>
87
+ <!ELEMENT volume (#PCDATA)>
88
+ <!ELEMENT part (#PCDATA)>
89
+ <!ELEMENT language (#PCDATA)>
90
+ <!ELEMENT binding (#PCDATA)>
91
+ <!ELEMENT page_extent (#PCDATA)>
92
+ <!ELEMENT table_of_contents (#PCDATA)>
93
+ <!ELEMENT performed_by (#PCDATA)>
94
+ <!ELEMENT performance_type (#PCDATA)>
95
+ <!ELEMENT storage (#PCDATA)>
96
+ <!ELEMENT format (#PCDATA)>
97
+ <!ELEMENT recording_length (#PCDATA)>
98
+ <!ELEMENT series (#PCDATA)>
99
+ <!ELEMENT year (#PCDATA)>
100
+ <!ELEMENT artist (#PCDATA)>
101
+ <!ELEMENT title (#PCDATA)>
102
+ <!ELEMENT media (#PCDATA)>
103
+ <!ELEMENT starring (#PCDATA)>
104
+ <!ELEMENT director (#PCDATA)>
105
+ <!ELEMENT originalName (#PCDATA)>
106
+ <!ELEMENT country (#PCDATA)>
107
+ <!ELEMENT description (#PCDATA)>
108
+ <!ELEMENT sales_notes (#PCDATA)>
109
+ <!ELEMENT promo (#PCDATA)>
110
+ <!ELEMENT aliases (#PCDATA)>
111
+ <!ELEMENT provider (#PCDATA)>
112
+ <!ELEMENT tarifplan (#PCDATA)>
113
+ <!ELEMENT xCategory (#PCDATA)>
114
+ <!ELEMENT additional (#PCDATA)>
115
+ <!ELEMENT worldRegion (#PCDATA)>
116
+ <!ELEMENT region (#PCDATA)>
117
+ <!ELEMENT days (#PCDATA)>
118
+ <!ELEMENT dataTour (#PCDATA)>
119
+ <!ELEMENT hotel_stars (#PCDATA)>
120
+ <!ELEMENT room (#PCDATA)>
121
+ <!ELEMENT meal (#PCDATA)>
122
+ <!ELEMENT included (#PCDATA)>
123
+ <!ELEMENT transport (#PCDATA)>
124
+ <!ELEMENT price_min (#PCDATA)>
125
+ <!ELEMENT price_max (#PCDATA)>
126
+ <!ELEMENT options (#PCDATA)>
127
+ <!ELEMENT manufacturer_warranty (#PCDATA)>
128
+ <!ELEMENT country_of_origin (#PCDATA)>
129
+ <!ELEMENT downloadable (#PCDATA)>
130
+ <!ELEMENT adult (#PCDATA)>
131
+ <!ELEMENT age (#PCDATA)>
132
+ <!ELEMENT barcode (#PCDATA)>
133
+ <!ELEMENT param (#PCDATA)>
134
+ <!ATTLIST param
135
+ name CDATA #REQUIRED
136
+ unit CDATA #IMPLIED>
137
+ <!ELEMENT related_offer (#PCDATA)>
138
+
139
+ <!ELEMENT place (#PCDATA)>
140
+ <!ELEMENT hall (#PCDATA)>
141
+ <!ATTLIST hall
142
+ plan CDATA #IMPLIED>
143
+
144
+ <!ELEMENT hall_part (#PCDATA)>
145
+ <!ELEMENT is_premiere (#PCDATA)>
146
+ <!ELEMENT is_kids (#PCDATA)>
147
+ <!ELEMENT date (#PCDATA)>
148
+
149
+
@@ -0,0 +1,15 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!DOCTYPE yml_catalog SYSTEM "/Users/rainx/WebstormProjects/partner/pages/help/shops.dtd">
3
+ <yml_catalog date="2010-04-01 17:00">
4
+ <shop>
5
+ <currencies>
6
+ </currencies>
7
+
8
+ <categories>
9
+ </categories>
10
+
11
+ <offers>
12
+ </offers>
13
+
14
+ </shop>
15
+ </yml_catalog>
@@ -0,0 +1,74 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!DOCTYPE yml_catalog SYSTEM "/Users/rainx/WebstormProjects/partner/pages/help/shops.dtd">
3
+ <yml_catalog date="2010-04-01 17:00">
4
+ <shop>
5
+ <name>Magazin</name>
6
+ <company>Magazin</company>
7
+ <url>http://www.magazin.ru/</url>
8
+
9
+ <currencies>
10
+ <currency id="RUR" rate="1" plus="0"/>
11
+ <currency id="USD" rate="30"/>
12
+ <currency id="KZT"/>
13
+ </currencies>
14
+
15
+ <categories>
16
+ <category id="1">Оргтехника</category>
17
+ <category id="10" parentId="1">Принтеры</category>
18
+ <category id="100" parentId="10">Струйные принтеры</category>
19
+ <category id="101" parentId="10">Лазерные принтеры</category>
20
+
21
+ <category id="2">Фототехника</category>
22
+ <category id="11" parentId="2">Фотоаппараты</category>
23
+ <category id="12" parentId="2">Объективы</category>
24
+
25
+ <category id="3">Книги</category>
26
+ <category id="13" parentId="3">Детективы</category>
27
+ </categories>
28
+
29
+ <local_delivery_cost>300</local_delivery_cost>
30
+
31
+ <offers>
32
+ <offer id="123" type="vendor.model" bid="13" cbid="20">
33
+ <url>http://magazin.ru/product_page.asp?pid=14344</url>
34
+ <price>15000</price>
35
+ <currencyId>RUR</currencyId>
36
+ <categoryId type="Own">100</categoryId>
37
+ <categoryId type="Own">101</categoryId>
38
+ <picture>http://magazin.ru/img/device1.jpg</picture>
39
+ <picture>http://magazin.ru/img/device2.jpg</picture>
40
+ <delivery>true</delivery>
41
+ <local_delivery_cost>300</local_delivery_cost>
42
+ <typePrefix>Принтер</typePrefix>
43
+ <vendor>НP</vendor>
44
+ <vendorCode>Q7533A</vendorCode>
45
+ <model>Color LaserJet 3000</model>
46
+ <description>A4, 64Mb, 600x600 dpi, USB 2.0, 29стр/мин ч/б / 15стр/мин цв, лотки на 100л и 250л, плотность до 175г/м, до 60000 стр/месяц </description>
47
+ <manufacturer_warranty>true</manufacturer_warranty>
48
+ <country_of_origin>Япония</country_of_origin>
49
+ </offer>
50
+
51
+ <offer id="1234" type="book" bid="17" available="false">
52
+ <price>100</price>
53
+ <currencyId>RUR</currencyId>
54
+ <categoryId type="Own">13</categoryId>
55
+ <delivery>true</delivery>
56
+ <local_delivery_cost>300</local_delivery_cost>
57
+ <author>Александра Маринина</author>
58
+ <name>Все не так. В 2 томах. Том 1</name>
59
+ <publisher>ЭКСМО - Пресс</publisher>
60
+ <series>А. Маринина - королева детектива</series>
61
+ <year>2009</year>
62
+ <ISBN>978-5-699-23647-3</ISBN>
63
+ <volume>2</volume>
64
+ <part>1</part>
65
+ <language>rus</language>
66
+ <binding>70x90/32</binding>
67
+ <page_extent>288</page_extent>
68
+ <description>Все прекрасно в большом патриархальном семействе Руденко. Но — увы! — впечатление это обманчиво: каждого из многочисленных представителей семьи обуревают свои потаенные страсти и запретные желания.</description>
69
+ <downloadable>false</downloadable>
70
+ </offer>
71
+ </offers>
72
+
73
+ </shop>
74
+ </yml_catalog>
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+
3
+ describe Goods::Catalog do
4
+ describe "#initialize" do
5
+ it "should call #from_string when string is passed" do
6
+ expect_any_instance_of(Goods::Catalog).to receive(:from_string).
7
+ with("string", "url", "UTF-8").once
8
+ Goods::Catalog.new(string: "string", url: "url", encoding: "UTF-8")
9
+ end
10
+
11
+ it "should raise error when none of 'string', 'url', 'file' params is passed" do
12
+ expect{ Goods::Catalog.new({}) }.to raise_error(ArgumentError)
13
+ end
14
+ end
15
+
16
+ describe "#from_string" do
17
+ class NullObject
18
+ def method_missing(name, *args)
19
+ self
20
+ end
21
+ end
22
+
23
+ [Goods::XML, Goods::CategoriesList, Goods::CurrenciesList, Goods::OffersList].each do |part|
24
+ it "should create #{part}" do
25
+ expect(part).to receive(:new).and_return(NullObject.new).once
26
+ Goods::Catalog.new(string: "xml")
27
+ end
28
+ end
29
+ end
30
+
31
+ describe "#prune" do
32
+ let(:xml) {
33
+ File.read(File.expand_path("../../fixtures/simple_catalog.xml", __FILE__))
34
+ }
35
+ let(:catalog) { Goods::Catalog.new string: xml}
36
+
37
+ it "should prune offers and categories" do
38
+ level = 2
39
+ expect(catalog.offers).to receive(:prune_categories).with(level)
40
+ expect(catalog.categories).to receive(:prune).with(level)
41
+ catalog.prune(level)
42
+ end
43
+
44
+ it "should replace categories of offers" do
45
+ catalog.prune(0)
46
+ expect(catalog.offers.find("123").category).to be(
47
+ catalog.categories.find("1")
48
+ )
49
+ end
50
+
51
+ it "should remove categories affected by prunning" do
52
+ catalog.prune(0)
53
+ expect(catalog.categories.size).to eql(3)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,42 @@
1
+ require "spec_helper"
2
+
3
+ describe Goods::CategoriesList do
4
+ it_should_behave_like "a container", Goods::Category, Goods::Category.new(id: "1", name: "Name")
5
+
6
+ describe "#add" do
7
+ it "should setup parent for category" do
8
+ parent = Goods::Category.new(id: "1", name: "Parent")
9
+ child = Goods::Category.new(id: "2", name: "Child", parent_id: "1")
10
+ subject.add(parent); subject.add(child)
11
+
12
+ expect(child.parent).to be(parent)
13
+ end
14
+ end
15
+
16
+ describe "#prune" do
17
+ let(:list) do
18
+ list = Goods::CategoriesList.new [
19
+ {id: "1", name: "root"},
20
+ {id: "11", name: "root", parent_id: "1"},
21
+ {id: "12", name: "root", parent_id: "2"}
22
+ ]
23
+ list
24
+ end
25
+
26
+ it "should raise error if prune level < 0" do
27
+ expect{ list.prune(-1) }.to raise_error(ArgumentError)
28
+ end
29
+
30
+ it "should not make any changes to nodes with level lesser than target level" do
31
+ orig = list.dup
32
+ list.prune(3)
33
+ expect(list.size).to eq(orig.size)
34
+ end
35
+
36
+ it "should remove nodes with level greater than target level" do
37
+ list.prune(1)
38
+ expect(list.size).to eq(2)
39
+ list.each { |item| expect(item.level < 2).to eql(true) }
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,90 @@
1
+ require "spec_helper"
2
+
3
+ describe Goods::Category do
4
+ let(:valid_description) { {id: "1", name: "Name"} }
5
+ let(:root) { Goods::Category.new valid_description }
6
+
7
+ it_should_behave_like "containable" do
8
+ let(:element) { Goods::Category.new(valid_description) }
9
+ end
10
+
11
+ describe "#valid?" do
12
+ it "should reject categories with no root when one should be" do
13
+ invalid_child = Goods::Category.new(id: "3", name: "Invalid child", parent_id: "2")
14
+ expect(invalid_child).not_to be_valid
15
+ end
16
+
17
+ it "should reject categories with incorrect parrent" do
18
+ invalid_child = Goods::Category.new(id: "3", name: "Invalid child", parent_id: "2")
19
+ invalid_child.parent = root
20
+ expect(invalid_child).not_to be_valid
21
+ end
22
+
23
+ it "should accept categories with no root" do
24
+ child = Goods::Category.new(valid_description)
25
+ child.valid?
26
+ expect(child).to be_valid
27
+ end
28
+
29
+ it "should accept categories with correct root" do
30
+ valid_child = Goods::Category.new(id: "3", name: "Invalid child", parent_id: "1")
31
+ valid_child.parent = root
32
+ expect(valid_child).to be_valid
33
+ end
34
+ end
35
+
36
+ context "on graph properties" do
37
+ let(:first_level) do
38
+ c = Goods::Category.new valid_description.merge parent_id: root.id
39
+ c.parent = root
40
+ c
41
+ end
42
+ let(:second_level) do
43
+ c = Goods::Category.new valid_description.merge parent_id: first_level.id
44
+ c.parent = first_level
45
+ c
46
+ end
47
+
48
+ describe "#level" do
49
+ it "should be 0 for root nodes" do
50
+ expect(root.level).to eq(0)
51
+ end
52
+
53
+ it "should be parent level + for for child nodes" do
54
+ expect(first_level.level).to eq(root.level + 1)
55
+ end
56
+ end
57
+
58
+ describe "#root" do
59
+ it "should be root itself if called on root" do
60
+ expect(root.root).to be(root)
61
+ end
62
+
63
+ it "should be root of a branch if called on child" do
64
+ expect(second_level.root).to be(root)
65
+ end
66
+ end
67
+
68
+ describe "#parent_at_level" do
69
+
70
+ it "should raise error if level < 0" do
71
+ expect { root.parent_at_level(-1) }.to raise_error(ArgumentError)
72
+ end
73
+
74
+ it "should return self if called for object's level" do
75
+ expect(first_level.parent_at_level(first_level.level)).to be(first_level)
76
+ end
77
+
78
+ it "should raise error if level > object level" do
79
+ expect { first_level.parent_at_level first_level.level + 1 }.
80
+ to raise_error(ArgumentError)
81
+ end
82
+
83
+ it "should return parent at specified level from the 'root'" do
84
+ expect(second_level.parent_at_level(1)).to be(first_level)
85
+ expect(second_level.parent_at_level(0)).to be(root)
86
+ end
87
+ end
88
+ end
89
+
90
+ end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe Goods::CurrenciesList do
4
+ it_should_behave_like "a container", Goods::Currency, Goods::Currency.new(id: "1", name: "RUR", rate: 1, plus: 0)
5
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe Goods::Currency do
4
+ let(:valid_description) { {id: "VAL_CUR", rate: 1, plus: 0} }
5
+ let(:valid_currency) { Goods::Currency.new(valid_description) }
6
+
7
+ it_should_behave_like "containable" do
8
+ let(:element) { valid_currency }
9
+ end
10
+
11
+ describe "#valid?" do
12
+ let(:invalid_description) { {id: "", rate: 0, plus: -1} }
13
+
14
+ it "should return true for valid currency" do
15
+ expect(Goods::Currency.new(valid_description).valid?).to be true
16
+ end
17
+
18
+ it "should return false for invalid currency" do
19
+ expect(Goods::Currency.new(invalid_description).valid?).to be false
20
+ end
21
+
22
+ it "should remember invalid fields" do
23
+ invalid_currency = Goods::Currency.new(invalid_description)
24
+ invalid_currency.valid?
25
+ [:id, :rate, :plus].each do |field|
26
+ expect(invalid_currency.invalid_fields).to include(field)
27
+ end
28
+ end
29
+ end
30
+
31
+ describe "#in" do
32
+ let(:rur) { Goods::Currency.new(id: "RUR", rate: 1, plus: 0) }
33
+ let(:usd) { Goods::Currency.new(id: "USD", rate: 30, plus: 0) }
34
+
35
+ it "should convert to another currency" do
36
+ expect(usd.in(rur)).to eql(30.0)
37
+ end
38
+ end
39
+ end