goods 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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