flexparser 1.0.2

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.
@@ -0,0 +1,23 @@
1
+ module Flexparser
2
+ #
3
+ # Class to keep config-options for the whole module.
4
+ #
5
+ class Configuration
6
+ AVAILABLE_OPTIONS =
7
+ %i[retry_without_namespaces explicit_property_naming].freeze
8
+ attr_accessor(*AVAILABLE_OPTIONS)
9
+
10
+ def initialize
11
+ set_defaults
12
+ end
13
+
14
+ def to_h
15
+ Hash[AVAILABLE_OPTIONS.map { |o| [o, public_send(o)] }]
16
+ end
17
+
18
+ def set_defaults
19
+ @retry_without_namespaces = true
20
+ @explicit_property_naming = true
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module Flexparser
2
+ #
3
+ # Used as a safeguard when passing nil or an empty string
4
+ # to the FragmentBuilder.
5
+ #
6
+ class EmptyFragment < Fragment
7
+ def xpath(_path)
8
+ self.class.new(nil)
9
+ end
10
+
11
+ def empty?
12
+ true
13
+ end
14
+
15
+ def text
16
+ nil
17
+ end
18
+
19
+ def namespaces
20
+ {}
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ module Flexparser
2
+ class Error < StandardError; end
3
+
4
+ #
5
+ # An Error that will be thrown if a value is required but
6
+ # missing.
7
+ #
8
+ class RequiredMissingError < Error
9
+ attr_reader :parser
10
+
11
+ def initialize(parser)
12
+ @parser = parser
13
+ end
14
+ end
15
+
16
+ #
17
+ # An Error that is thrown when there is no clear name
18
+ # for a property.
19
+ #
20
+ class AmbiguousNamingError < ArgumentError; end
21
+ end
@@ -0,0 +1,44 @@
1
+ module Flexparser
2
+ #
3
+ # Wraps Nokogiri::Xml::Document to automatically hand down namespaces
4
+ # to fragments generated by #xpath
5
+ #
6
+ class Fragment
7
+ extend Forwardable
8
+
9
+ attr_reader :doc
10
+ attr_writer :namespaces
11
+
12
+ def_delegators(:@doc, :text, :empty?, :map,
13
+ :each, :namespaces, :collect_namespaces)
14
+
15
+ def initialize(str, namespaces: {})
16
+ @doc = str.is_a?(String) ? Nokogiri::XML(str) : str
17
+ @propagated_namespaces = namespaces
18
+ end
19
+
20
+ def to_s
21
+ "#{self.class}: \n\tNamespaces: #{_namespaces}
22
+ \tRaw:\n\t\t#{@raw_doc}\tNokogiri:\n#{doc}"
23
+ end
24
+
25
+ #
26
+ # The same as Nokogiri::Xml::Document#xpath but
27
+ # namespaces are passed down to resulting fragments.
28
+ #
29
+ def xpath(path)
30
+ FragmentBuilder
31
+ .build(doc.xpath(path, propagating_namespaces))
32
+ end
33
+
34
+ def propagating_namespaces
35
+ return @propagated_namespaces unless doc.respond_to?(:namespaces)
36
+ if doc.respond_to?(:collect_namespaces)
37
+ return @propagated_namespaces.merge doc.collect_namespaces
38
+ end
39
+ @propagated_namespaces.merge doc.namespaces
40
+ end
41
+
42
+ alias pns propagating_namespaces
43
+ end
44
+ end
@@ -0,0 +1,15 @@
1
+ module Flexparser
2
+ #
3
+ # Class to build handle turn a given object into
4
+ # a Fragment. Used mostly as a Safeguard.
5
+ #
6
+ class FragmentBuilder
7
+ class << self
8
+ def build(str, namespaces: {})
9
+ return str if str.is_a?(Fragment)
10
+ return EmptyFragment.new(str) if str.nil?
11
+ Fragment.new(str, namespaces: namespaces)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexparser
4
+ #
5
+ # A parser for a single tag.
6
+ #
7
+ class TagParser
8
+ attr_accessor :xpaths, :options
9
+
10
+ #
11
+ # @param name [Symbol] the name used for the accessor
12
+ # defined on the parent.
13
+ # @param xpath [String] an xpath string used to access a Nokogiri-Fragment
14
+ #
15
+ def initialize(tags, **opts)
16
+ @xpaths = XPaths.new(tags)
17
+ @options = opts
18
+ end
19
+
20
+ #
21
+ # @param doc [Nokogiri::Node] a node that can be accessed through xpath
22
+ # @return a String if no type was specified, otherwise the type
23
+ # will try to parse the string using ::parse
24
+ #
25
+ def parse(doc)
26
+ result = content(doc) || return
27
+ return options[:sub_parser].parse(result) if sub_parser
28
+ type ? transform(result.text) : result.text
29
+ end
30
+
31
+ def name
32
+ options[:name] || xpaths.method_name
33
+ end
34
+
35
+ protected
36
+
37
+ def sub_parser
38
+ options[:sub_parser]
39
+ end
40
+
41
+ def type
42
+ options[:type] || options[:transform]
43
+ end
44
+
45
+ def transform(string)
46
+ return options[:type].parse string if options[:type]
47
+ return string.public_send(options[:transform]) if options[:transform]
48
+ string
49
+ end
50
+
51
+ def content(doc)
52
+ xpaths.valid_paths(doc).each do |path|
53
+ set = doc.xpath("(#{path})[1]")
54
+ return set unless set.empty?
55
+ end
56
+ return unless options[:required]
57
+ raise_required_error(doc)
58
+ end
59
+
60
+ def raise_required_error(doc)
61
+ raise(RequiredMissingError.new(self),
62
+ "Required field #{name} not found in #{doc}")
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,3 @@
1
+ module Flexparser
2
+ VERSION = '1.0.2'.freeze
3
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexparser
4
+ #
5
+ # A class to represent a collection of paths.
6
+ # This class is supposed to be more convenient for
7
+ # handeling a collection of xpaths.
8
+ #
9
+ class XPaths
10
+ attr_accessor :tags
11
+
12
+ #
13
+ # @param tags [Array<String>] a list of names for xpaths
14
+ # that the xpaths instance will handle.
15
+ #
16
+ def initialize(tags)
17
+ @tags = Array(tags).map(&:to_sym).flatten
18
+ raise ArgumentError, arg_err_msg if @tags.empty?
19
+ end
20
+
21
+ #
22
+ # The name of an accesor, based on the collection of tags.
23
+ #
24
+ def method_name
25
+ tags.first.to_s.sub(/^@/, '').gsub(/([[:punct:]]|-)+/, '_')
26
+ end
27
+
28
+ #
29
+ # Builds xpaths from the given tags
30
+ #
31
+ def xpaths
32
+ @xpaths ||= begin
33
+ paths = tags.map do |t|
34
+ XPath.current.descendant(t)
35
+ end
36
+ if Flexparser.configuration.retry_without_namespaces
37
+ paths += ns_ignortant_xpaths
38
+ end
39
+ paths
40
+ end
41
+ end
42
+
43
+ #
44
+ # Returns the valid paths from this collection, based on the given doc.
45
+ # @param doc [Flexparser::Fragment] the fragment
46
+ # that carries the namespaces.
47
+ # @return [Array<String>] the xpaths that can be applied to this fragment
48
+ #
49
+ def valid_paths(doc)
50
+ xpaths.reject do |path|
51
+ namespaced?(path) && !namespace_available?(path, doc)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ #
58
+ # Returns xpaths that ignore namespaces.
59
+ #
60
+ def ns_ignortant_xpaths
61
+ tags.reject(&method(:namespaced?)).flat_map do |tag|
62
+ XPath.current.descendant[XPath.name.equals(tag.to_s)]
63
+ end
64
+ end
65
+
66
+ #
67
+ # Checks whether a tag has a specified namespace.
68
+ # @param tag [String] The tag to be looked at
69
+ #
70
+ def namespaced?(tag)
71
+ !(tag.to_s =~ /\w+:\w+/).nil?
72
+ end
73
+
74
+ #
75
+ # Checks whether or not a path is can be applied to a given fragment.
76
+ # @param path [String] the path that is to be checked
77
+ # @param doc [Flexparser::Fragment] the fragment that carries the
78
+ # namespaces.
79
+ # @return [Boolean] true if the path can be applied, false otherwise.
80
+ #
81
+ def namespace_available?(path, doc)
82
+ nms = doc.propagating_namespaces.keys.map { |ns| ns.gsub('xmlns:', '') }
83
+ path.to_s.match Regexp.union(nms)
84
+ end
85
+
86
+ # no-doc
87
+ def arg_err_msg
88
+ 'There needs to be at least one path for a path-collection to be valid'
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,11 @@
1
+ <bookstore>
2
+ <book>James</book>
3
+ <book>John</book>
4
+ <book>Jeff</book>
5
+ <book>Jerry</book>
6
+ <mag>Ed</mag>
7
+ <mag>Eddy</mag>
8
+ <tomb>
9
+ <paper>John</paper>
10
+ </tomb>
11
+ </bookstore>
metadata ADDED
@@ -0,0 +1,181 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flexparser
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Paul Martensen
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-08-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: xpath
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: guard
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.14'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.14'
55
+ - !ruby/object:Gem::Dependency
56
+ name: guard-minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.4'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry-byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.4'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.4'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.13'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.13'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '10.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '10.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: minitest
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '5.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '5.0'
125
+ description: A flexible and robust parser-dsl.
126
+ email:
127
+ - paul.martensen@gmx.de
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".gitignore"
133
+ - ".rubocop.yml"
134
+ - ".rubocop_todo.yml"
135
+ - ".travis.yml"
136
+ - Gemfile
137
+ - Guardfile
138
+ - LICENSE
139
+ - README.md
140
+ - Rakefile
141
+ - bin/console
142
+ - bin/setup
143
+ - flexparser.gemspec
144
+ - lib/flexparser.rb
145
+ - lib/flexparser/anonymous_parser.rb
146
+ - lib/flexparser/class_methods.rb
147
+ - lib/flexparser/collection_parser.rb
148
+ - lib/flexparser/configuration.rb
149
+ - lib/flexparser/empty_fragment.rb
150
+ - lib/flexparser/errors.rb
151
+ - lib/flexparser/fragment.rb
152
+ - lib/flexparser/fragment_builder.rb
153
+ - lib/flexparser/tag_parser.rb
154
+ - lib/flexparser/version.rb
155
+ - lib/flexparser/xpaths.rb
156
+ - test.xml
157
+ homepage: https://github.com/lokalportal/flexparser
158
+ licenses:
159
+ - GPL-3.0
160
+ metadata: {}
161
+ post_install_message:
162
+ rdoc_options: []
163
+ require_paths:
164
+ - lib
165
+ required_ruby_version: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ required_rubygems_version: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: '0'
175
+ requirements: []
176
+ rubyforge_project:
177
+ rubygems_version: 2.6.12
178
+ signing_key:
179
+ specification_version: 4
180
+ summary: A xml-parser dsl
181
+ test_files: []