transformator 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,123 @@
1
+ require "transformator"
2
+
3
+ module Transformator::Examples
4
+ class PrimoSearchResponseTransformation
5
+ def self.apply(*args)
6
+ transformation.apply(*args)
7
+ end
8
+
9
+ def self.transformation
10
+ Transformator::Transformation.new do
11
+ def search_brief_return_transformation
12
+ @search_brief_return_transformation ||= Transformator::Transformation.new do
13
+ #
14
+ # setup target skeleton
15
+ #
16
+ process :document do |source, target|
17
+ target_skeleton = {
18
+ took: nil,
19
+ hits: {
20
+ hits: []
21
+ },
22
+ facets: {}
23
+ }
24
+
25
+ elements_from_hash(target_skeleton).each do |element|
26
+ target << element
27
+ end
28
+ end
29
+
30
+ #
31
+ # facets
32
+ #
33
+ process "SEGMENTS/JAGROOT/RESULT/FACETLIST/FACET" do |source_facet, target|
34
+ # syntactic mapping
35
+ find(target, "facets") << element(source_facet["NAME"]) do |target_facet|
36
+ source_facet_values = find_all(source_facet, "FACET_VALUES")
37
+
38
+ target_facet << element("_type", text: "terms")
39
+ target_facet << element("total", text: source_facet_values.length, type: "integer")
40
+ target_facet << (terms_array = array("terms"))
41
+
42
+ source_facet_values.each do |source_facet_value|
43
+ array(terms_array) do |term|
44
+ term << element("term", text: source_facet_value[:KEY])
45
+ term << element("count", text: source_facet_value[:VALUE], type: "integer")
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ #
52
+ # records
53
+ #
54
+ process "SEGMENTS/JAGROOT/RESULT/DOCSET/DOC" do |record, target|
55
+ array(find(target, "hits/hits")) do |hit|
56
+ # syntactic mapping
57
+ hit << element("_type", text: "record")
58
+ hit << _source = element("_source") do |element|
59
+ record.locate("PrimoNMBib/record/?").each do |section|
60
+ element << section
61
+ end
62
+ end
63
+
64
+ # semantic mapping
65
+ {
66
+ "display/creationdate" => "created",
67
+ "display/description" => "description",
68
+ "display/edition" => "edition",
69
+ "display/format" => "format",
70
+ "display/language" => "language",
71
+ "display/title" => "title",
72
+ "display/subject" => "subject",
73
+ "display/publisher" => "publisher", # may there be more than one?
74
+ "control/recordid" => "id"
75
+ }
76
+ .each_pair do |from, to|
77
+ find(_source, "_source/#{from}") do |element|
78
+ hit << element(to, text: element.text)
79
+ end
80
+ end
81
+
82
+ find_all(_source, "_source/display/creator").each do |creator|
83
+ hit << element("creator", text: creator.text)
84
+ end
85
+
86
+ #
87
+ # identifier
88
+ #
89
+ hit << array("identifier") do |identifier|
90
+ # ilsApiId
91
+ identifier << element(find(_source, "control/ilsapiid").text, text: "ilsApiId")
92
+
93
+ # isbns
94
+ find_all(_source, "search/isbn").each do |isbn|
95
+ identifier << element(isbn.text, text: "isbn")
96
+ end
97
+
98
+ # recordId
99
+ identifier << element(find(_source, "control/recordid").text, text: "recordId")
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ process :document do |source, target|
107
+ # parse the "string encoded" inner search brief return
108
+ search_brief_return = Transformator.document_from_xml(
109
+ find(source, "Envelope/Body/searchBriefResponse/searchBriefReturn").text,
110
+ remove_namespaces: true,
111
+ remove_whitespace_only_text_nodes: false
112
+ )
113
+
114
+ # apply the literal transformation and merge the result's nodes with target
115
+ search_brief_return_transformation.apply(
116
+ to: search_brief_return,
117
+ output: :ox_document
118
+ ).nodes.each { |node| target << node }
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,89 @@
1
+ require "transformator"
2
+
3
+ module Transformator::Examples
4
+ class SearchRequestTransformation
5
+ def self.apply(*args)
6
+ transformation.apply(*args)
7
+ end
8
+
9
+ def self.transformation
10
+ Transformator::Transformation.new do
11
+ #
12
+ # setup outer target skeleton
13
+ #
14
+ process :target do |target|
15
+ target << element_from_xml(
16
+ <<-xml.strip_heredoc
17
+ <env:Envelope
18
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
19
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
20
+ xmlns:impl="http://primo.kobv.de/PrimoWebServices/services/searcher"
21
+ xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
22
+ xmlns:ins0="http://xml.apache.org/xml-soap">
23
+ <env:Body>
24
+ <impl:searchBrief></impl:searchBrief>
25
+ </env:Body>
26
+ </env:Envelope>
27
+ xml
28
+ )
29
+ end
30
+
31
+ #
32
+ # setup inner search request that will be wrapped in a cdata element at the end
33
+ #
34
+ process :none do
35
+ # we setup this skeleton instead of dynamic element creation because order matters with primo
36
+ @search_request = element_from_xml(
37
+ <<-xml.strip_heredoc
38
+ <searchRequest xmlns="http://www.exlibris.com/primo/xsd/wsRequest" xmlns:uic="http://www.exlibris.com/primo/xsd/primoview/uicomponents">
39
+ <PrimoSearchRequest xmlns="http://www.exlibris.com/primo/xsd/search/request">
40
+ <QueryTerms>
41
+ <BoolOpeator>AND</BoolOpeator>
42
+ </QueryTerms>
43
+ <StartIndex></StartIndex>
44
+ <BulkSize></BulkSize>
45
+ <DidUMeanEnabled>false</DidUMeanEnabled>
46
+ <HighlightingEnabled>false</HighlightingEnabled>
47
+ <Languages></Languages>
48
+ <SortByList></SortByList>
49
+ <Locations></Locations>
50
+ </PrimoSearchRequest>
51
+ <onCampus>false</onCampus>
52
+ </searchRequest>
53
+ xml
54
+ )
55
+ end
56
+
57
+ #
58
+ # transform source into target
59
+ #
60
+ process "/from" do |element|
61
+ find(@search_request, "PrimoSearchRequest/StartIndex") << element.text
62
+ end
63
+
64
+ process "/size" do |element|
65
+ find(@search_request, "PrimoSearchRequest/BulkSize") << element.text
66
+ end
67
+
68
+ process "//query_string" do |query_string|
69
+ find(@search_request, "PrimoSearchRequest/QueryTerms") << element_from_xml(
70
+ <<-xml.strip_heredoc
71
+ <QueryTerm>
72
+ <IndexField></IndexField>
73
+ <PrecisionOperator>contains</PrecisionOperator>
74
+ <Value>#{find(query_string, "query").text}</Value>
75
+ </QueryTerm>
76
+ xml
77
+ )
78
+ end
79
+
80
+ #
81
+ # finally, wrap the search request into a cdata element
82
+ #
83
+ process :target do |target|
84
+ find(target, "//impl:searchBrief") << cdata(xml_from_element(@search_request))
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,93 @@
1
+ require "ox"
2
+
3
+ module Transformator::Dsl
4
+ def array(name_or_node, &block)
5
+ name_or_node = name_or_node.to_s if name_or_node.is_a?(Symbol) # eliminate "symbol"-case
6
+
7
+ name = name_or_node.is_a?(String) ? name_or_node : name_or_node.value
8
+ node = name_or_node.is_a?(Ox::Element) ? name_or_node : element(name, type: "array")
9
+
10
+ if block
11
+ append_accumulator = Struct.new(:elements) do
12
+ def <<(element)
13
+ self.elements.push(element)
14
+ end
15
+ end.new([])
16
+
17
+ yield(append_accumulator)
18
+
19
+ node << element(name) do |array_element|
20
+ append_accumulator.elements.each do |element|
21
+ array_element << element
22
+ end
23
+ end
24
+ end
25
+
26
+ node
27
+ end
28
+
29
+ def cdata(content, &block)
30
+ new_cdata = Ox::CData.new(content)
31
+ block ? yield(new_cdata) : new_cdata
32
+ end
33
+
34
+ def element(name, options = {}, &block)
35
+ new_element = Ox::Element.new(name)
36
+
37
+ if (attributes = options[:attributes]).is_a?(Hash)
38
+ attributes.each_pair do |key, value|
39
+ new_element[key.to_s] = value.to_s
40
+ end
41
+ end
42
+
43
+ if nodes = options[:nodes]
44
+ (nodes.is_a?(Array) ? nodes : [nodes]).each do |node|
45
+ new_element << node
46
+ end
47
+ end
48
+
49
+ if text = options[:text]
50
+ new_element << text.to_s
51
+ end
52
+
53
+ if type = options[:type]
54
+ new_element["type"] = type.to_s
55
+ end
56
+
57
+ yield(new_element) if block
58
+ new_element
59
+ end
60
+
61
+ def elements_from_hash(hash)
62
+ Transformator.document_from_hash(hash).root.nodes
63
+ end
64
+
65
+ def element_from_xml(xml, options = {})
66
+ elements_from_xml(xml, options).first
67
+ end
68
+
69
+ def elements_from_xml(xml, options = {})
70
+ Transformator.document_from_xml(xml, options).nodes
71
+ end
72
+
73
+ def find(node, path, &block)
74
+ find_result = find_all(node, path).first
75
+
76
+ if block && find_result
77
+ yield(find_result)
78
+ else
79
+ find_result
80
+ end
81
+ end
82
+
83
+ def find_all(node, path, &block)
84
+ find_all_result = node.locate(Transformator.oxify_path(path))
85
+
86
+ yield(find_all_result) if block && !find_all_result.empty?
87
+ find_all_result
88
+ end
89
+
90
+ def xml_from_element(element)
91
+ Transformator.xml_from_document(element, with_xml: false)
92
+ end
93
+ end
@@ -0,0 +1,13 @@
1
+ require "active_support/core_ext/hash/conversions"
2
+ require "libxml"
3
+ require "transformator/format_converter/document_from_xml"
4
+
5
+ module Transformator::FormatConverter::DocumentFromHash
6
+ include Transformator::FormatConverter::DocumentFromXml
7
+
8
+ def document_from_hash(hash)
9
+ ActiveSupport::XmlMini.backend = "LibXML"
10
+ xml = hash.to_xml(dasherize: false, indent: 0, root: :hash, skip_types: false)
11
+ document_from_xml(xml, remove_whitespace_only_text_nodes: false)
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ module Transformator::FormatConverter::DocumentFromObject
2
+ def document_from_object(obj, options = {})
3
+ case Transformator.determine_format(obj)
4
+ when :hash
5
+ Transformator.document_from_hash(obj)
6
+ when :json
7
+ Transformator.document_from_json(obj)
8
+ when :ox_document
9
+ obj
10
+ when :xml
11
+ Transformator.document_from_xml(
12
+ obj,
13
+ remove_whitespace_only_text_nodes: false,
14
+ remove_namespaces: true
15
+ )
16
+ when nil
17
+ Ox::Document.new(version: "1.0", encoding: "UTF-8")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ module Transformator::FormatConverter::DocumentFromXml
2
+ def document_from_xml(xml, options = {})
3
+ xml =
4
+ if options[:remove_whitespace_only_text_nodes] || options[:remove_namespaces]
5
+ xml.dup
6
+ else
7
+ xml
8
+ end
9
+
10
+ unless options[:remove_whitespace_only_text_nodes] == false
11
+ Transformator::FormatConverter.remove_whitespace_only_text_nodes!(xml)
12
+ end
13
+
14
+ if options[:remove_namespaces] == true
15
+ Transformator::FormatConverter.remove_namespaces!(xml)
16
+ end
17
+
18
+ if xml[/\A\s*<\?xml/]
19
+ Ox.parse(xml)
20
+ else
21
+ Ox::Document.new(version: "1.0", encoding: "UTF-8").tap do |new_document|
22
+ Ox.parse("<root>" << xml << "</root>").nodes.each do |node|
23
+ new_document << node
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,61 @@
1
+ require "ox"
2
+
3
+ module Transformator::FormatConverter::HashFromDocument
4
+ def hash_from_document(document)
5
+ document =
6
+ if document.root.value == "hash"
7
+ document
8
+ else
9
+ hash_container_document = Ox::Document.new
10
+ hash_container_document << (hash_root_element = Ox::Element.new("hash"))
11
+ document.nodes.each do |node|
12
+ hash_root_element << node
13
+ end
14
+
15
+ hash_container_document
16
+ end
17
+
18
+ hash = Transformator::FormatConverter::HashFromDocument.process_node(document.root, {})
19
+ hash["hash"].nil? ? hash : hash["hash"]
20
+ end
21
+
22
+ def self.process_node(node, hash)
23
+ value =
24
+ if (child_nodes = node.nodes).all? { |child_node| child_node.is_a?(String) }
25
+ case node[:type]
26
+ when "integer" then child_nodes.join.to_i
27
+ when "float" then child_nodes.join.to_f
28
+ when "boolean" then child_nodes.join.downcase == "true"
29
+ else node[:nil] == "true" ? nil : child_nodes.join
30
+ end
31
+ else
32
+ if node[:type] == "array"
33
+ node.locate(node.value)
34
+ .map do |child_node|
35
+ if (arr_element = process_node(child_node, {})).is_a?(Hash) && arr_element.keys == [node.value]
36
+ arr_element.values.first
37
+ else
38
+ arr_element
39
+ end
40
+ end
41
+ else
42
+ {}.tap do |child_nodes_hash|
43
+ child_nodes.each do |child_node|
44
+ process_node(child_node, child_nodes_hash)
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ hash[node.value] =
51
+ if hash[node.value].nil?
52
+ value
53
+ elsif hash[node.value].is_a?(Array)
54
+ hash[node.value] << value
55
+ else
56
+ [hash[node.value], value]
57
+ end
58
+
59
+ hash
60
+ end
61
+ end
@@ -0,0 +1,7 @@
1
+ require "ox"
2
+
3
+ module Transformator::FormatConverter::XmlFromDocument
4
+ def xml_from_document(document, options = {})
5
+ Ox.dump(document, { }.merge(options))
6
+ end
7
+ end
@@ -0,0 +1,27 @@
1
+ module Transformator::FormatConverter
2
+ require_relative "./format_converter/document_from_hash"
3
+ require_relative "./format_converter/document_from_object"
4
+ require_relative "./format_converter/document_from_xml"
5
+ require_relative "./format_converter/hash_from_document"
6
+ require_relative "./format_converter/xml_from_document"
7
+
8
+ include DocumentFromHash
9
+ include DocumentFromObject
10
+ include DocumentFromXml
11
+ include HashFromDocument
12
+ include XmlFromDocument
13
+
14
+ def self.remove_namespaces!(xml)
15
+ xml.gsub!(/<(\/?)\w+:(\w+)/, "<\\1\\2")
16
+ end
17
+
18
+ def self.remove_whitespace_only_text_nodes(xml)
19
+ remove_whitespace_only_text_nodes!(xml.dup)
20
+ end
21
+
22
+ def self.remove_whitespace_only_text_nodes!(xml)
23
+ # remove whitespace only text nodes
24
+ xml.gsub!(/>(\s|\n|\r)+</, "><")
25
+ xml
26
+ end
27
+ end
@@ -1,41 +1,67 @@
1
1
  require "ox"
2
+ require "transformator/dsl"
2
3
 
3
4
  class Transformator::Transformation
4
5
  def initialize(options = {}, &block)
5
- @context = options[:context]
6
6
  @rules = []
7
7
  self.instance_eval(&block) if block
8
8
  end
9
9
 
10
- def apply_to(source, options = {})
11
- source = Ox.parse(source) if source.is_a?(String)
12
- result = Ox::Document.new(version: "1.0", encoding: "UTF-8").tap do |target|
13
- @rules.each do |rule|
14
- apply_rule(rule, source, target, options)
10
+ #
11
+ public
12
+ #
13
+ def apply(options = {})
14
+ source = Transformator.document_from_object(options[:source] ||= options[:to])
15
+ target = Transformator.document_from_object(options[:target])
16
+
17
+ @rules.each do |rule|
18
+ block = rule[:callable]
19
+ path = rule[:path]
20
+ type = rule[:type]
21
+
22
+ # allow user do process source => source, target => target or target => source
23
+ _source = options[:source] == :target ? target : source
24
+ _target = options[:target] == :source ? source : target
25
+
26
+ if type == :process
27
+ if path.is_a?(Array)
28
+ block.call(
29
+ path.map do |_path| { _path => find_all(_source, _path) } end,
30
+ _target
31
+ )
32
+ elsif path.is_a?(String)
33
+ find_all(_source, path).each do |element_to_process|
34
+ block.call(element_to_process, _target)
35
+ end
36
+ elsif path.is_a?(Symbol)
37
+ if path == :document
38
+ block.call(_source, _target)
39
+ elsif path == :none
40
+ block.call
41
+ elsif path == :source
42
+ block.call(source, _target)
43
+ elsif path == :target
44
+ block.call(target, _target)
45
+ end
46
+ end
15
47
  end
16
48
  end
17
49
 
18
- Ox.dump(result, with_xml: true)
50
+ case options[:output_format] || Transformator.determine_format(options[:source])
51
+ when :hash then Transformator.hash_from_document(target)
52
+ when :ox_document then target
53
+ when :xml then Ox.dump(target, with_xml: true)
54
+ else raise "Unknown output format!"
55
+ end
19
56
  end
20
57
 
58
+ #
21
59
  private
22
-
60
+ #
61
+ include Transformator::Dsl
62
+
23
63
  def process(path, options = {}, &block)
24
- @rules.push({ callable: block, path: path }.merge(options))
64
+ @rules.push({ callable: block, path: path, type: :process}.merge(options))
25
65
  self
26
66
  end
27
-
28
- def apply_rule(rule, source, target, options = {})
29
- value =
30
- if rule[:path].is_a?(Array)
31
- rule[:path].inject({}) do |hash, path|
32
- hash[path] = source.locate(path)
33
- memo
34
- end
35
- else
36
- source.locate(rule[:path])
37
- end
38
-
39
- (options[:context] || @context || self).instance_exec(value, target, &rule[:callable])
40
- end
41
67
  end
@@ -1,3 +1,3 @@
1
1
  module Transformator
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/transformator.rb CHANGED
@@ -1,5 +1,33 @@
1
+ require "ox"
1
2
  require "transformator/version"
2
3
 
3
4
  module Transformator
5
+ require_relative "./transformator/dsl"
6
+ require_relative "./transformator/format_converter"
4
7
  require_relative "./transformator/transformation"
8
+
9
+ extend Transformator::FormatConverter
10
+
11
+ def self.determine_format(obj)
12
+ if obj.is_a?(Hash)
13
+ :hash
14
+ elsif obj.is_a?(Ox::Document)
15
+ :ox_document
16
+ elsif obj.is_a?(String) && obj[/\A\s*{/]
17
+ :json
18
+ elsif obj.is_a?(String) && obj[/\A\s*</]
19
+ :xml
20
+ elsif obj.nil?
21
+ nil
22
+ else
23
+ raise "Unkown format!"
24
+ end
25
+ end
26
+
27
+ def self.oxify_path(path)
28
+ path
29
+ .gsub(/\A\/\/(\S+)/, "*/\\1")
30
+ .gsub(/(\S*)\/\/(\S*)/, "\\1/*/\\2")
31
+ .gsub(/\A\/(\w+)(\S*)/, "?/\\1\\2") # replace "/foo" with "?/foo"
32
+ end
5
33
  end
@@ -0,0 +1,19 @@
1
+ require_relative "../../examples/primo_search_response_transformation"
2
+
3
+ describe "Examples" do
4
+ let(:primo_search_response) do
5
+ File.read(
6
+ File.expand_path(
7
+ File.join(
8
+ File.dirname(__FILE__), "../../assets/primo_search_response_1.xml"
9
+ )
10
+ )
11
+ )
12
+ end
13
+
14
+ it "transforms primo search response" do
15
+ Transformator::Examples::PrimoSearchResponseTransformation.apply(to: primo_search_response, output_format: :hash).tap do |w|
16
+ # binding.pry
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,48 @@
1
+ require_relative "../../examples/search_request_transformation"
2
+
3
+ describe "Examples" do
4
+ let(:search_request_hash) do
5
+ {
6
+ from: 0,
7
+ size: 20,
8
+ query: {
9
+ bool: {
10
+ must: [
11
+ {
12
+ query_string: {
13
+ query: "linux",
14
+ default_operator: "AND",
15
+ fields: [
16
+ "title^2",
17
+ "_all"
18
+ ]
19
+ }
20
+ },
21
+ {
22
+ query_string: {
23
+ query: "kofler",
24
+ default_operator: "AND",
25
+ fields: [
26
+ "creator"
27
+ ]
28
+ }
29
+ },
30
+ {
31
+ range: {
32
+ lsr09: {
33
+ gte: 20140101
34
+ }
35
+ }
36
+ }
37
+ ]
38
+ }
39
+ },
40
+ }
41
+ end
42
+
43
+ it "transforms search request to primo soap body" do
44
+ Transformator::Examples::SearchRequestTransformation.apply(to: search_request_hash, output_format: :xml).tap do |w|
45
+ # binding.pry
46
+ end
47
+ end
48
+ end