goods 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +114 -0
- data/Rakefile +1 -0
- data/goods.gemspec +31 -0
- data/lib/goods/catalog.rb +32 -0
- data/lib/goods/categories_list.rb +26 -0
- data/lib/goods/category.rb +54 -0
- data/lib/goods/containable.rb +54 -0
- data/lib/goods/container.rb +58 -0
- data/lib/goods/currencies_list.rb +12 -0
- data/lib/goods/currency.rb +29 -0
- data/lib/goods/offer.rb +43 -0
- data/lib/goods/offers_list.rb +38 -0
- data/lib/goods/version.rb +3 -0
- data/lib/goods/xml/validator.rb +66 -0
- data/lib/goods/xml.rb +129 -0
- data/lib/goods.rb +42 -0
- data/lib/support/shops.dtd +149 -0
- data/spec/fixtures/empty_catalog.xml +15 -0
- data/spec/fixtures/simple_catalog.xml +74 -0
- data/spec/goods/catalog_spec.rb +56 -0
- data/spec/goods/categories_list_spec.rb +42 -0
- data/spec/goods/category_spec.rb +90 -0
- data/spec/goods/currencies_list_spec.rb +5 -0
- data/spec/goods/currency_spec.rb +39 -0
- data/spec/goods/offer_spec.rb +87 -0
- data/spec/goods/offers_list_spec.rb +80 -0
- data/spec/goods/xml/validator_spec.rb +25 -0
- data/spec/goods/xml_spec.rb +178 -0
- data/spec/goods_spec.rb +44 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/shared_examples_for_containable.rb +16 -0
- data/spec/support/shared_examples_for_container.rb +43 -0
- metadata +168 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bc907ce0650d82d39c2d077d63492e2ad9b1d4fa
|
4
|
+
data.tar.gz: 69b1b31e9bdfc828364fb7c16dda6c53545b3dfe
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 783ddac11cd0e9ac7648a2dc36c8f34573ad2d5935529e91e95ffd8ed2b9d76b7aedec8fdf63818f1a713facb284270e6b4e34c3dc6d6257ddac6dc341225ada
|
7
|
+
data.tar.gz: 37c18feb8ebe1e7f0108690ee662bb6f869726b1495277250d3fcbbf35a877200d02d3bb009777eac26800dfead0407848dcc75c97f5e8b22ebb93bdaae2a106
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Artem Pyanykh
|
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,114 @@
|
|
1
|
+
# Goods
|
2
|
+
|
3
|
+
The purpose of this gem is to provide simple, yet reliable solution for parsing
|
4
|
+
YML (Yandex Market Language) files, with clean and convenient interface,
|
5
|
+
and a few extra capabilites, such as categories prunning.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'goods'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install goods
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
1. **How to parse YML-catalog**:
|
24
|
+
|
25
|
+
begin
|
26
|
+
catalog = Goods.from_string(xml, url, encoding)
|
27
|
+
[ or Goods.from_url(url, encoding) ]
|
28
|
+
[ or Goods.from_file(io, encoding) ]
|
29
|
+
rescue Goods::XML::InvalidFormatError => e
|
30
|
+
#do something
|
31
|
+
end
|
32
|
+
|
33
|
+
* `encoding` is optional;
|
34
|
+
* `url` is optional when parsing string;
|
35
|
+
|
36
|
+
2. **How to iterate over offers**:
|
37
|
+
|
38
|
+
catalog.offers.each do |offer|
|
39
|
+
offer.id # => "some id"
|
40
|
+
offer.category # => <Goods::Category>
|
41
|
+
offer.currency # => <Goods::Currency>
|
42
|
+
offer.price # => 50.0 (for example)
|
43
|
+
etc...
|
44
|
+
end
|
45
|
+
3. **How to iterate over categories**:
|
46
|
+
|
47
|
+
catalog.categories.each do |category|
|
48
|
+
category.id # => "some id"
|
49
|
+
category.name # => "some name"
|
50
|
+
category.parent # => <Goods::Category> or nil
|
51
|
+
etc...
|
52
|
+
end
|
53
|
+
|
54
|
+
4. **How to iterate over currencies**:
|
55
|
+
|
56
|
+
catalog.currencies.each do |currency|
|
57
|
+
currency.id # => "RUR"
|
58
|
+
currency.rate # => 1.00 or 30.00 or some other float
|
59
|
+
etc...
|
60
|
+
end
|
61
|
+
|
62
|
+
5. **How to get a single element from the collection**:
|
63
|
+
|
64
|
+
If you know an ID of an object, you may act like this
|
65
|
+
|
66
|
+
(for example, let's take currencies)
|
67
|
+
rur = catalog.currencies.find("RUR") # => <Goods::Curency>
|
68
|
+
rur.rate # => 1.00
|
69
|
+
|
70
|
+
6. **How to prune categories in a whole catalog**:
|
71
|
+
|
72
|
+
catalog.prune(level_of_pruning)
|
73
|
+
|
74
|
+
It will replace all categories with level greater than `level_of_pruning` with their parents at that level.
|
75
|
+
|
76
|
+
What is the purpose of prunning? For example, with very deep categories structure it may be very costly in terms of performance to mirror this structure in your database. It may be sufficient to have a representation with lower level of details.
|
77
|
+
|
78
|
+
7. **How to convert currencies and prices for a whole catalog**:
|
79
|
+
|
80
|
+
rur = catalog.currencies.find("RUR")
|
81
|
+
catalog.convert_currency(rur)
|
82
|
+
|
83
|
+
It will convert all prices and change `currency` for every offer.
|
84
|
+
|
85
|
+
8. **But what's with invalid elements**:
|
86
|
+
|
87
|
+
General validation is performed according to DTD spec., and guarantees that you will have all the fields, that you're expecting to find in a YML-catalog. However, there can be somewhat trickier inconsistencies in YML-files, like offer having non-existing category_id, or currency_id.
|
88
|
+
|
89
|
+
All that valid (according to DTD), but defective elements are saved under `defectives` property of a collection. Each defective element has `invalid_field` property. For example:
|
90
|
+
|
91
|
+
defectives = catalog.offers.defectives # => Array of Goods::Offer
|
92
|
+
defectives.first.invalid_fields # => [:category_id, :currency_id]
|
93
|
+
9. **How can I manually validate YML-file against DTD?**:
|
94
|
+
|
95
|
+
validator = Goods::XML::Validator.new
|
96
|
+
validator.valid?(xml_string) # => true or false
|
97
|
+
validator.error # => first validation error
|
98
|
+
|
99
|
+
10. **Is that all?**
|
100
|
+
|
101
|
+
No, it is not. For more information look at the source code.
|
102
|
+
|
103
|
+
|
104
|
+
## Contributing
|
105
|
+
|
106
|
+
At current time, Goods::Offer is quite incomplete, and works only with properties, that I need. Generalization of Goods::Offer is welcome!
|
107
|
+
|
108
|
+
**So**:
|
109
|
+
|
110
|
+
1. Fork it
|
111
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
112
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
113
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
114
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/goods.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'goods/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "goods"
|
8
|
+
spec.version = Goods::VERSION
|
9
|
+
spec.authors = ["Artem Pyanykh"]
|
10
|
+
spec.email = ["artem.pyanykh@gmail.com"]
|
11
|
+
spec.description = <<DESC
|
12
|
+
The purpose of this gem is to provide simple, yet reliable solution for parsing
|
13
|
+
YML (Yandex Market Language) files, with clean and convenient interface,
|
14
|
+
and extra capabilites, such as categories prunning.
|
15
|
+
DESC
|
16
|
+
spec.summary = %q{Simple parser for YML (Yandex Market Language) files with a few twists.}
|
17
|
+
spec.homepage = "https://github.com/ArtemPyanykh/goods"
|
18
|
+
spec.license = "MIT"
|
19
|
+
|
20
|
+
spec.files = `git ls-files`.split($/)
|
21
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
22
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
26
|
+
spec.add_development_dependency "rake"
|
27
|
+
spec.add_development_dependency "rspec", "~> 3.0.0.beta1"
|
28
|
+
|
29
|
+
spec.add_runtime_dependency "libxml-ruby"
|
30
|
+
spec.add_runtime_dependency "nokogiri"
|
31
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Goods
|
2
|
+
class Catalog
|
3
|
+
attr_reader :categories, :currencies, :offers
|
4
|
+
|
5
|
+
def initialize(params)
|
6
|
+
if params[:string]
|
7
|
+
from_string(params[:string], params[:url], params[:encoding])
|
8
|
+
else
|
9
|
+
raise ArgumentError, "should provide either :string or :url param"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def prune(level)
|
14
|
+
@offers.prune_categories(level)
|
15
|
+
@categories.prune(level)
|
16
|
+
end
|
17
|
+
|
18
|
+
def convert_currency(other_currency)
|
19
|
+
@offers.convert_currency(other_currency)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def from_string(xml_string, url, encoding)
|
25
|
+
@xml_string = xml_string
|
26
|
+
@xml = XML.new(xml_string, url, encoding)
|
27
|
+
@categories = CategoriesList.new(@xml.categories)
|
28
|
+
@currencies = CurrenciesList.new(@xml.currencies)
|
29
|
+
@offers = OffersList.new(@categories, @currencies, @xml.offers)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Goods
|
2
|
+
class CategoriesList
|
3
|
+
extend Container
|
4
|
+
container_for Category
|
5
|
+
|
6
|
+
def initialize(categories = [])
|
7
|
+
categories.each do |category|
|
8
|
+
add category
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def prune(level)
|
13
|
+
level >= 0 or raise ArgumentError, "incorrect level"
|
14
|
+
|
15
|
+
items.delete_if { |k, v| v.level > level }
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def prepare(object_or_hash)
|
21
|
+
category = super
|
22
|
+
category.parent = find(category.parent_id) if category.parent_id
|
23
|
+
category
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Goods
|
2
|
+
class Category
|
3
|
+
include Containable
|
4
|
+
attr_accessor :parent
|
5
|
+
attr_field :parent_id
|
6
|
+
attr_field :name
|
7
|
+
|
8
|
+
def initialize(description)
|
9
|
+
self.description = description
|
10
|
+
@parent_at_level = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def level
|
14
|
+
@level ||=
|
15
|
+
begin
|
16
|
+
if parent
|
17
|
+
parent.level + 1
|
18
|
+
else
|
19
|
+
0
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def root
|
25
|
+
return self unless parent
|
26
|
+
@root ||= parent.root
|
27
|
+
end
|
28
|
+
|
29
|
+
def parent_at_level(level)
|
30
|
+
valid_parent_level?(level) or raise ArgumentError.new('incorrect level')
|
31
|
+
return self if self.level == level
|
32
|
+
|
33
|
+
@parent_at_level[level] ||= parent.parent_at_level(level)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def valid_parent_level?(level)
|
39
|
+
level >= 0 && level <= self.level
|
40
|
+
end
|
41
|
+
|
42
|
+
def apply_validation_rules
|
43
|
+
validate :id, proc { |val| !(val.nil? || val.empty?) }
|
44
|
+
validate :name, proc { |val| !(val.nil? || val.empty?) }
|
45
|
+
validate :parent_id, proc { |parent_id|
|
46
|
+
if !(parent_id.nil? || parent_id.empty?)
|
47
|
+
parent && (parent.id == parent_id)
|
48
|
+
else
|
49
|
+
parent == nil
|
50
|
+
end
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Goods
|
2
|
+
module Containable
|
3
|
+
def self.included(base)
|
4
|
+
# Class macro that defined mathod to access instance variable @field_name
|
5
|
+
# in the following way (illustrative example)
|
6
|
+
#
|
7
|
+
# def field_name
|
8
|
+
# @field_name ||= description[field_name]
|
9
|
+
# end
|
10
|
+
base.define_singleton_method :attr_field do |field_name|
|
11
|
+
define_method field_name do
|
12
|
+
if field = instance_variable_get("@#{field_name}")
|
13
|
+
field
|
14
|
+
else
|
15
|
+
instance_variable_set("@#{field_name}", description[field_name])
|
16
|
+
instance_variable_get("@#{field_name}")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def description
|
23
|
+
@description
|
24
|
+
end
|
25
|
+
|
26
|
+
def id
|
27
|
+
@id ||= description[:id]
|
28
|
+
end
|
29
|
+
|
30
|
+
def invalid_fields
|
31
|
+
@invalid_fields ||= []
|
32
|
+
end
|
33
|
+
|
34
|
+
def valid?
|
35
|
+
reset_validation
|
36
|
+
apply_validation_rules
|
37
|
+
invalid_fields.empty?
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def reset_validation
|
43
|
+
invalid_fields.clear
|
44
|
+
end
|
45
|
+
|
46
|
+
def validate(field, predicate)
|
47
|
+
invalid_fields << field unless predicate.call(send(field))
|
48
|
+
end
|
49
|
+
|
50
|
+
def description=(description)
|
51
|
+
@description = description
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Goods
|
2
|
+
module Container
|
3
|
+
def container_for(klass)
|
4
|
+
define_singleton_method :_containable_class do
|
5
|
+
klass
|
6
|
+
end
|
7
|
+
|
8
|
+
include ContainerMethods
|
9
|
+
include Enumerable
|
10
|
+
# Need to redefine Enumerable#find
|
11
|
+
include SearchMethods
|
12
|
+
end
|
13
|
+
|
14
|
+
module ContainerMethods
|
15
|
+
def add(object_or_hash)
|
16
|
+
element = prepare(object_or_hash)
|
17
|
+
|
18
|
+
if element.valid?
|
19
|
+
items[element.id] = element
|
20
|
+
else
|
21
|
+
defectives << element
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def defectives
|
26
|
+
@defectives ||= []
|
27
|
+
end
|
28
|
+
|
29
|
+
def each(&block)
|
30
|
+
items.values.each(&block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def size
|
34
|
+
items.size
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def items
|
40
|
+
@items ||= {}
|
41
|
+
end
|
42
|
+
|
43
|
+
def prepare(object_or_hash)
|
44
|
+
if object_or_hash.kind_of? self.class._containable_class
|
45
|
+
object_or_hash
|
46
|
+
else
|
47
|
+
self.class._containable_class.new(object_or_hash)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
module SearchMethods
|
53
|
+
def find(id)
|
54
|
+
items[id]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Goods
|
2
|
+
class Currency
|
3
|
+
include Containable
|
4
|
+
|
5
|
+
def initialize(description)
|
6
|
+
self.description = description
|
7
|
+
end
|
8
|
+
|
9
|
+
def rate
|
10
|
+
@rate ||= description[:rate].to_f
|
11
|
+
end
|
12
|
+
|
13
|
+
def plus
|
14
|
+
@plus ||= description[:plus].to_f
|
15
|
+
end
|
16
|
+
|
17
|
+
def in(other_currency)
|
18
|
+
self.rate/other_currency.rate
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def apply_validation_rules
|
24
|
+
validate :id, proc { |val| !(val.nil? || val.empty?) }
|
25
|
+
validate :rate, proc { |val| val >= 1 }
|
26
|
+
validate :plus, proc { |val| val >= 0 }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/goods/offer.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module Goods
|
2
|
+
class Offer
|
3
|
+
include Containable
|
4
|
+
attr_accessor :category, :currency, :price
|
5
|
+
attr_field :category_id
|
6
|
+
attr_field :currency_id
|
7
|
+
attr_field :available
|
8
|
+
attr_field :description
|
9
|
+
attr_field :model
|
10
|
+
attr_field :name
|
11
|
+
attr_field :picture
|
12
|
+
attr_field :vendor
|
13
|
+
|
14
|
+
def initialize(description)
|
15
|
+
self.description = description
|
16
|
+
@price = description[:price].to_f
|
17
|
+
end
|
18
|
+
|
19
|
+
def convert_currency(other_currency)
|
20
|
+
self.price *= currency.in(other_currency)
|
21
|
+
self.currency = other_currency
|
22
|
+
@currency_id = other_currency.id
|
23
|
+
end
|
24
|
+
|
25
|
+
def change_category(other_category)
|
26
|
+
self.category = other_category
|
27
|
+
@category_id = other_category.id
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def apply_validation_rules
|
33
|
+
validate :id, proc { |val| !(val.nil? || val.empty?) }
|
34
|
+
validate :category_id, proc { |category_id|
|
35
|
+
category_id && category && (category_id == category.id)
|
36
|
+
}
|
37
|
+
validate :currency_id, proc { |currency_id|
|
38
|
+
currency_id && currency && (currency_id == currency.id)
|
39
|
+
}
|
40
|
+
validate :price, proc { |price| price && price > 0 }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Goods
|
2
|
+
class OffersList
|
3
|
+
extend Container
|
4
|
+
container_for Offer
|
5
|
+
attr_accessor :categories_list, :currencies_list
|
6
|
+
|
7
|
+
def initialize(categories_list = nil, currencies_list = nil, offers = [])
|
8
|
+
self.categories_list = categories_list
|
9
|
+
self.currencies_list = currencies_list
|
10
|
+
offers.each do |offer|
|
11
|
+
add offer
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def prune_categories(level)
|
16
|
+
self.each do |offer|
|
17
|
+
if offer.category.level > level
|
18
|
+
offer.change_category(offer.category.parent_at_level(level))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def convert_currency(other_currency)
|
24
|
+
self.each do |offer|
|
25
|
+
offer.convert_currency(other_currency)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def prepare(object_or_hash)
|
32
|
+
object = super
|
33
|
+
object.category = categories_list.find(object.category_id) if categories_list
|
34
|
+
object.currency = currencies_list.find(object.currency_id) if currencies_list
|
35
|
+
object
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require "xml"
|
2
|
+
|
3
|
+
module Goods
|
4
|
+
class XML
|
5
|
+
class Validator
|
6
|
+
attr_reader :error
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@error = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def valid?(xml)
|
13
|
+
validate(xml)
|
14
|
+
error.nil?
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate(xml)
|
18
|
+
@error = nil
|
19
|
+
document = LibXML::XML::Document.string(xml)
|
20
|
+
|
21
|
+
# Should silence STDERR, because libxml2 spews validation error
|
22
|
+
# to standard error stream
|
23
|
+
silence_stream(STDERR) do
|
24
|
+
# Catch first exception due to bug in libxml - it throws
|
25
|
+
# 'the model is not determenistic' error. The second validation runs
|
26
|
+
# just fine and gives the real error.
|
27
|
+
begin
|
28
|
+
document.validate(dtd)
|
29
|
+
rescue LibXML::XML::Error => e
|
30
|
+
#nothing
|
31
|
+
end
|
32
|
+
|
33
|
+
begin
|
34
|
+
document.validate(dtd)
|
35
|
+
rescue LibXML::XML::Error => e
|
36
|
+
@error = e.to_s
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def dtd_path
|
44
|
+
File.expand_path("../../../support/shops.dtd", __FILE__)
|
45
|
+
end
|
46
|
+
|
47
|
+
def dtd_string
|
48
|
+
File.read(dtd_path)
|
49
|
+
end
|
50
|
+
|
51
|
+
def dtd
|
52
|
+
@dtd ||= LibXML::XML::Dtd.new(dtd_string)
|
53
|
+
end
|
54
|
+
|
55
|
+
def silence_stream(stream)
|
56
|
+
old_stream = stream.dup
|
57
|
+
stream.reopen(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null')
|
58
|
+
stream.sync = true
|
59
|
+
yield
|
60
|
+
ensure
|
61
|
+
stream.reopen(old_stream)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|