├── .formatter.exs ├── .gitignore ├── LICENSE ├── README.md ├── lib ├── elixir_xml_to_map.ex └── xml_to_map │ ├── naive_map.ex │ └── nested_map.ex ├── mix.exs ├── mix.lock └── test ├── elixir_xml_to_map_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | .elixir_ls 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XmlToMap 2 | 3 | [![Module Version](https://img.shields.io/hexpm/v/elixir_xml_to_map.svg)](https://hex.pm/packages/elixir_xml_to_map) 4 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/elixir_xml_to_map/) 5 | [![Total Download](https://img.shields.io/hexpm/dt/elixir_xml_to_map.svg)](https://hex.pm/packages/elixir_xml_to_map) 6 | [![License](https://img.shields.io/hexpm/l/elixir_xml_to_map.svg)](https://github.com/homanchou/elixir_xml_to_map/blob/master/LICENSE) 7 | [![Last Updated](https://img.shields.io/github/last-commit/homanchou/elixir-xml-to-map.svg)](https://github.com/homanchou/elixir-xml-to-map/commits/master) 8 | 9 | This library provides two functions to create an Elixir Map data structure from an XML string. 10 | 11 | ### `XmlToMap.naive_map/2` 12 | 13 | Usage: 14 | 15 | ```elixir 16 | XmlToMap.naive_map("123") 17 | ``` 18 | 19 | Results in: 20 | 21 | ```elixir 22 | %{"foo" => %{"bar" => "123"}} 23 | ``` 24 | 25 | Converts XML string to an Elixir map with strings for keys, not atoms, since atoms are not garbage collected. 26 | 27 | This tool is inspired by Rails `Hash.from_xml()`. 28 | 29 | ---- 30 | 31 | I call the function "naive", because there are known short comings and there is some [controversy](https://stackoverflow.com/questions/40650482/how-to-convert-xml-to-a-map-in-elixir) around using a conversion tool like this since XML and Maps are non-isomorphic and there is no standard way to convert all the information from one format to another. The recommended way to pull specific well structured information from XML is to use something like xpath. But if you understand the risks and still prefer to convert the whole XML string to a map then this tool is for you! 32 | 33 | It is currently not able to parse XML namespace. It also can't determine if a child should be a list unless it sees a repeated child. If and only if nodes are repeated at the same level will they become a list. 34 | 35 | ```elixir 36 | # there are two points inside foo, so the value of "point" becomes a list. Had "foo" only contained one point then there would be no list but instead one nested map 37 | XmlToMap.naive_map("1529") 38 | 39 | # => %{"foo" => %{"point" => [%{"x" => "1", "y" => "5"}, %{"x" => "2", "y" => "9"}]}} 40 | ``` 41 | 42 | Previously this package did not handle XML node attributes. 43 | The current version takes inspiration from a [go goxml2json package](https://github.com/basgys/goxml2json) and exports attributes in the map while prepending "-" so you know they are attributes. 44 | 45 | Whenever we encounter an XML node with BOTH attributes and children, we wrap "#content" around the node's inner value. 46 | 47 | For example this snippet has a `Height` leaf node with attribute `Units` and a value of `0.50`: 48 | 49 | ```xml 50 | 51 | 0.50 52 | 53 | ``` 54 | 55 | This would become this snippet: 56 | 57 | ```json 58 | ... 59 | "ItemDimensions": { 60 | "Height": { 61 | "#content": "0.50", 62 | "-Units": "inches" 63 | } 64 | } 65 | ``` 66 | 67 | Empty tags will have a value of nil. 68 | 69 | ### `XmlToMap.nested_map/2` 70 | 71 | This function produces arguably more verbose and less straightforward result, but it preserves 72 | the order of sequences and might be then instantiated back to _XML_ in an isomorphic way. 73 | 74 | In general, it reflects the tree structure of an input _XML_ per se. 75 | 76 | Usage: 77 | 78 | ```elixir 79 | XmlToMap.nested_map("123") 80 | ``` 81 | 82 | Results in: 83 | 84 | ```elixir 85 | %{ 86 | attributes: [], 87 | name: "foo", 88 | content: %{attributes: [], name: "bar", content: "123"} 89 | } 90 | ``` 91 | 92 | The function does not make any assumptions about the content and recursively builds the result. 93 | 94 | ```xml 95 | 96 | 0.50 97 | 98 | ``` 99 | 100 | This would become this snippet: 101 | 102 | ```elixir 103 | %{ 104 | attributes: [], 105 | name: "ItemDimensions", 106 | content: %{attributes: [{"Units", "inches"}], name: "Height", content: "0.50"} 107 | } 108 | ``` 109 | 110 | Which might then be converted to _JSON_ of the same shape. 111 | 112 | To make it less verbose, pass `purge_empty: true` as the second parameter to `XmlToMap.nested_map/2`: 113 | 114 | ```elixir 115 | XmlToMap.nested_map("123", purge_empty: true) 116 | ``` 117 | 118 | Results in: 119 | 120 | ```elixir 121 | %{ 122 | name: "foo", 123 | content: [ 124 | %{attributes: [{"arg", "yes"}], name: "bar", content: "123"}, 125 | %{name: "baz"} 126 | ] 127 | } 128 | ``` 129 | 130 | ----- 131 | 132 | There is a dependency on Erlsom to parse XML then converts the 'simple_form' structure into a map. 133 | 134 | I prefer Erlsom because it is the best documented erlang XML parser and because it mentions that it does not produce new atoms during the scanning. 135 | 136 | See tests for some example usage. 137 | 138 | ---- 139 | 140 | ## Installation 141 | 142 | The package can be installed as: 143 | 144 | 1. Add `:elixir_xml_to_map` to your list of dependencies in `mix.exs`: 145 | 146 | ```elixir 147 | def deps do 148 | [{:elixir_xml_to_map, "~> 2.0"}] 149 | end 150 | ``` 151 | 152 | 2. Ensure `:elixir_xml_to_map` is started before your application: 153 | 154 | ```elixir 155 | def application do 156 | [extra_applications: [:elixir_xml_to_map]] 157 | end 158 | ``` 159 | 160 | 161 | ## License 162 | 163 | Copyright (c) 2016-present, Homan Chou 164 | 165 | Licensed under the Apache License, Version 2.0 (the "License"); 166 | you may not use this file except in compliance with the License. 167 | You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 168 | 169 | Unless required by applicable law or agreed to in writing, software 170 | distributed under the License is distributed on an "AS IS" BASIS, 171 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 172 | See the License for the specific language governing permissions and 173 | limitations under the License. 174 | -------------------------------------------------------------------------------- /lib/elixir_xml_to_map.ex: -------------------------------------------------------------------------------- 1 | defmodule XmlToMap do 2 | @moduledoc """ 3 | Simple convenience module for getting a map out of an XML string. 4 | """ 5 | 6 | alias XmlToMap.{NaiveMap, NestedMap} 7 | 8 | @doc """ 9 | `naive_map/1` utility is inspired by `Rails Hash.from_xml()` but is 10 | "naive" in that it is convenient (requires no setup) but carries the same 11 | drawbacks. 12 | 13 | For example no validation over what should be a collection. If and only if 14 | nodes are repeated at the same level will they become a list. If a node has 15 | attributes we'll prepend a "-" in front of them and merge them into the map 16 | and take the node value and nest that inside "#content" key. 17 | """ 18 | @defaults %{namespace_match_fn: &__MODULE__.default_namespace_match_fn/0} 19 | 20 | def naive_map(xml, opts \\ []) do 21 | params = Enum.into(opts, @defaults) 22 | tree = get_generic_data_structure(xml, params) 23 | NaiveMap.parse(tree) 24 | end 25 | 26 | def nested_map(xml, opts \\ []) do 27 | params = Enum.into(opts, @defaults) 28 | {purge_empty, params} = Map.pop(params, :purge_empty, false) 29 | tree = get_generic_data_structure(xml, params) 30 | NestedMap.parse(tree, purge_empty) 31 | end 32 | 33 | # Default function to handle namespace in xml 34 | # This fuction invokes an anonymous function that has this parameters: Name, Namespace, Prefix. 35 | # Usually, the namespace appears at the top of xml file, e.g `xmlns:g="http://base.google.com/ns/1.0"`, 36 | # where `g` is the Prefix and `"http://base.google.com/ns/1.0"` the Namespace, 37 | # the Name is the key tag in xml. 38 | # It should return a term. It is called for each tag and attribute name. 39 | # The result will be used in the output. 40 | # When a namespace and prefix is found, it'll return a term like "#{prefix}:#{name}", 41 | # otherwise it will return only the name. 42 | # The default behavior in :erlsom.simple_form/2 is Name if Namespace == undefined, and a string {Namespace}Name otherwise. 43 | def default_namespace_match_fn do 44 | fn name, namespace, prefix -> 45 | cond do 46 | namespace != [] && prefix != [] -> "#{prefix}:#{name}" 47 | true -> name 48 | end 49 | end 50 | end 51 | 52 | defp get_generic_data_structure(xml, %{namespace_match_fn: namespace_match_fn}) do 53 | {:ok, element, _tail} = :erlsom.simple_form(xml, [{:nameFun, namespace_match_fn.()}]) 54 | element 55 | end 56 | 57 | # erlsom simple_form returns a kind of tree: 58 | # Result = {ok, Element, Tail}, 59 | # where Element = {Tag, Attributes, Content}, 60 | # Tag is a string 61 | # Attributes = [{AttributeName, Value}], 62 | # Content is a list of Elements and/or strings. 63 | defp get_generic_data_structure(xml, _opts) do 64 | {:ok, element, _tail} = :erlsom.simple_form(xml) 65 | element 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/xml_to_map/naive_map.ex: -------------------------------------------------------------------------------- 1 | defmodule XmlToMap.NaiveMap do 2 | @moduledoc """ 3 | Module to recursively traverse the output of `erlsom.simple_form/1` 4 | and produce a map. 5 | 6 | `erlsom` uses character lists so this library converts them to 7 | Elixir binary string. 8 | 9 | Attributes, if present, are defined as `tag@attribute_name => attribute_value`. 10 | """ 11 | 12 | # if a single list element of a tuple, ignore the list detail and parse it 13 | def parse([{tag, attributes, content}]) do 14 | parse({tag, attributes, content}) 15 | end 16 | 17 | # for any other singular value, it's probably meant to be a string 18 | def parse([value]) do 19 | to_string(value) |> String.trim() 20 | end 21 | 22 | # a parse with a tuples is probably the entry point we'll hit first 23 | # if this node has no attributes, our work is easier 24 | def parse({tag, [], content}) do 25 | parsed_content = parse(content) 26 | %{to_string(tag) => parsed_content} 27 | end 28 | 29 | # this is probably the entry point we'll hit first 30 | # Element = {Tag, Attributes, Content}, 31 | # Tag is a string 32 | # Attributes = [{AttributeName, Value}], 33 | # Content is a list of Elements and/or strings. 34 | # in this case attributes is not empty 35 | def parse({tag, attributes, content}) do 36 | attributes_map = 37 | Enum.reduce(attributes, %{}, fn {attribute_name, attribute_value}, acc -> 38 | Map.put(acc, "-#{attribute_name}", to_string(attribute_value)) 39 | end) 40 | 41 | parsed_content = parse(content) 42 | joined_content = %{"#content" => parsed_content} |> Map.merge(attributes_map) 43 | 44 | %{to_string(tag) => joined_content} 45 | end 46 | 47 | def parse(list) when is_list(list) do 48 | parsed_list = Enum.map(list, &{to_string(elem(&1, 0)), parse(&1)}) 49 | 50 | content = 51 | Enum.reduce(parsed_list, %{}, fn {k, v}, acc -> 52 | case Map.get(acc, k) do 53 | nil -> 54 | for({key, value} <- v, into: %{}, do: {key, value}) 55 | |> Map.merge(acc) 56 | 57 | [h | t] -> 58 | Map.put(acc, k, [h | t] ++ [v[k]]) 59 | 60 | prev -> 61 | Map.put(acc, k, [prev] ++ [v[k]]) 62 | end 63 | end) 64 | 65 | case content == %{} do 66 | true -> nil 67 | _ -> content 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/xml_to_map/nested_map.ex: -------------------------------------------------------------------------------- 1 | defmodule XmlToMap.NestedMap do 2 | @moduledoc """ 3 | Module to recursively traverse the output of `erlsom.simple_form/1` 4 | and produce a nested map with `name`, `attributes`, and `content` fields. 5 | 6 | `erlsom` uses character lists so this library converts them to 7 | Elixir binary string. 8 | """ 9 | 10 | @moduledoc since: "3.1.0" 11 | 12 | @typedoc "Type of a nested map" 13 | @type element :: %{ 14 | name: String.t(), 15 | attributes: map() | [{charlist() | String.t(), charlist() | String.t()}], 16 | content: [element()] 17 | } 18 | 19 | # if a single list element of a tuple, ignore the list detail and parse it 20 | def parse([{tag, attributes, content}], purge_empty) do 21 | parse({tag, attributes, content}, purge_empty) 22 | end 23 | 24 | # for any other singular value, it's probably meant to be a string 25 | def parse([value], _purge_empty) do 26 | value |> to_string() |> String.trim() 27 | end 28 | 29 | # a parse with a tuples is probably the entry point we'll hit first 30 | # if this node has no attributes, our work is easier 31 | def parse({tag, [], []}, true) do 32 | %{name: to_string(tag)} 33 | end 34 | 35 | def parse({tag, [], content}, true) do 36 | %{name: to_string(tag), content: parse(content, true)} 37 | end 38 | 39 | def parse({tag, attributes, []}, true) do 40 | %{name: to_string(tag), attributes: fix_attributes(attributes)} 41 | end 42 | 43 | def parse({tag, attributes, content}, purge_empty) do 44 | %{ 45 | name: to_string(tag), 46 | attributes: fix_attributes(attributes), 47 | content: parse(content, purge_empty) 48 | } 49 | end 50 | 51 | def parse([], _purge_empty), do: nil 52 | 53 | def parse(list, purge_empty) when is_list(list), do: Enum.map(list, &parse(&1, purge_empty)) 54 | 55 | defp fix_attributes(attributes) do 56 | for {k, v} <- attributes do 57 | k = if is_list(k) and List.ascii_printable?(k), do: to_string(k), else: k 58 | v = if is_list(v) and List.ascii_printable?(v), do: to_string(v), else: v 59 | {k, v} 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule XmlToMap.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/homanchou/elixir-xml-to-map" 5 | 6 | def project do 7 | [ 8 | app: :elixir_xml_to_map, 9 | version: "3.1.0", 10 | elixir: "~> 1.9", 11 | build_embedded: Mix.env() == :prod, 12 | start_permanent: Mix.env() == :prod, 13 | description: "A module for converting an XML string to a map", 14 | package: package(), 15 | deps: deps(), 16 | docs: docs() 17 | ] 18 | end 19 | 20 | def package do 21 | [ 22 | maintainers: ["Homan Chou"], 23 | licenses: ["Apache-2.0"], 24 | links: %{"GitHub" => @source_url} 25 | ] 26 | end 27 | 28 | def application do 29 | [extra_applications: [:logger]] 30 | end 31 | 32 | defp deps do 33 | [ 34 | {:erlsom, "~> 1.4"}, 35 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} 36 | ] 37 | end 38 | 39 | defp docs do 40 | [ 41 | main: "readme", 42 | source_url: @source_url, 43 | extras: ["README.md"] 44 | ] 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, 3 | "erlsom": {:hex, :erlsom, "1.4.1", "53dbacf35adfea6f0714fd0e4a7b0720d495e88c5e24e12c5dc88c7b62bd3e49", [:rebar3], [], "hexpm", "57b777fe2522e342badfa35873b2266c6961e3a9f4d2ac195d761985c40c3247"}, 4 | "ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0db1ee8d1547ab4877c5b5dffc6604ef9454e189928d5ba8967d4a58a801f161"}, 5 | "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"}, 6 | "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, 7 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, 8 | } 9 | -------------------------------------------------------------------------------- /test/elixir_xml_to_map_test.exs: -------------------------------------------------------------------------------- 1 | defmodule XmlToMapTest do 2 | use ExUnit.Case 3 | doctest XmlToMap 4 | 5 | test "make a map (naive)" do 6 | assert XmlToMap.naive_map(sample_xml()) == expectation_naive() 7 | end 8 | 9 | test "combines sibling nodes with the same name into a list (naive)" do 10 | assert XmlToMap.naive_map(amazon_xml()) == amazon_expected_naive() 11 | end 12 | 13 | test "make a map (nested)" do 14 | assert XmlToMap.nested_map(sample_xml()) == expectation_nested() 15 | end 16 | 17 | test "combines sibling nodes with the same name into a list (nested)" do 18 | assert XmlToMap.nested_map(amazon_xml()) == amazon_expected_nested() 19 | end 20 | 21 | test "empty tag => nil" do 22 | xml = """ 23 | 24 | 1 25 | Value 26 | 27 | 28 | 29 | """ 30 | 31 | assert XmlToMap.naive_map(xml) == %{ 32 | "xml" => %{ 33 | "id" => "1", 34 | "name" => "Value", 35 | "empty" => nil, 36 | "emptyWithAttrs" => %{ 37 | "#content" => nil, 38 | "-id" => "123" 39 | } 40 | } 41 | } 42 | 43 | assert XmlToMap.nested_map(xml) == %{ 44 | attributes: [], 45 | content: [ 46 | %{attributes: [], name: "id", content: "1"}, 47 | %{attributes: [], name: "name", content: "Value"}, 48 | %{attributes: [], name: "empty", content: nil}, 49 | %{attributes: [{"id", "123"}], name: "emptyWithAttrs", content: nil} 50 | ], 51 | name: "xml" 52 | } 53 | 54 | assert XmlToMap.nested_map(xml, purge_empty: true) == %{ 55 | content: [ 56 | %{name: "id", content: "1"}, 57 | %{name: "name", content: "Value"}, 58 | %{name: "empty"}, 59 | %{attributes: [{"id", "123"}], name: "emptyWithAttrs"} 60 | ], 61 | name: "xml" 62 | } 63 | end 64 | 65 | test "support for custom namespace_match function" do 66 | assert XmlToMap.naive_map(facebook_xmlns_xml(), 67 | namespace_match_fn: &set_prefix_namespace_fn/0 68 | ) == facebook_custom_function_expected() 69 | end 70 | 71 | test "support for xmlns xml file" do 72 | assert XmlToMap.naive_map(facebook_xmlns_xml()) == facebook_xmlns_xml_expected() 73 | end 74 | 75 | def expectation_naive do 76 | %{ 77 | "orders" => %{ 78 | "order" => [ 79 | %{ 80 | "billing_address" => "My address", 81 | "id" => "123", 82 | "items" => %{ 83 | "item" => [ 84 | %{ 85 | "-currency" => "USD", 86 | "#content" => %{ 87 | "description" => "Hat", 88 | "price" => "5.99", 89 | "quantity" => "1", 90 | "sku" => %{"-lang" => "en", "#content" => "ABC"} 91 | } 92 | }, 93 | %{ 94 | "-currency" => "USD", 95 | "#content" => %{ 96 | "description" => "Bat", 97 | "price" => "9.99", 98 | "quantity" => "2", 99 | "sku" => "ABC" 100 | } 101 | } 102 | ] 103 | } 104 | }, 105 | %{ 106 | "billing_address" => "Uncle's House", 107 | "id" => "124", 108 | "items" => %{ 109 | "item" => %{ 110 | "-currency" => "USD", 111 | "#content" => %{ 112 | "description" => "Hat", 113 | "price" => "5.99", 114 | "quantity" => "2", 115 | "sku" => "ABC" 116 | } 117 | } 118 | } 119 | } 120 | ] 121 | } 122 | } 123 | end 124 | 125 | def expectation_nested do 126 | %{ 127 | name: "orders", 128 | attributes: [], 129 | content: [ 130 | %{ 131 | name: "order", 132 | attributes: [], 133 | content: [ 134 | %{attributes: [], name: "id", content: "123"}, 135 | %{attributes: [], name: "billing_address", content: "My address"}, 136 | %{ 137 | attributes: [], 138 | name: "items", 139 | content: [ 140 | %{ 141 | attributes: [{"currency", "USD"}], 142 | name: "item", 143 | content: [ 144 | %{attributes: [{"lang", "en"}], name: "sku", content: "ABC"}, 145 | %{attributes: [], name: "description", content: "Hat"}, 146 | %{attributes: [], name: "price", content: "5.99"}, 147 | %{attributes: [], name: "quantity", content: "1"} 148 | ] 149 | }, 150 | %{ 151 | attributes: [{"currency", "USD"}], 152 | name: "item", 153 | content: [ 154 | %{attributes: [], name: "sku", content: "ABC"}, 155 | %{attributes: [], name: "description", content: "Bat"}, 156 | %{attributes: [], name: "price", content: "9.99"}, 157 | %{attributes: [], name: "quantity", content: "2"} 158 | ] 159 | } 160 | ] 161 | } 162 | ] 163 | }, 164 | %{ 165 | attributes: [], 166 | name: "order", 167 | content: [ 168 | %{attributes: [], name: "id", content: "124"}, 169 | %{attributes: [], name: "billing_address", content: "Uncle's House"}, 170 | %{ 171 | attributes: [], 172 | name: "items", 173 | content: %{ 174 | attributes: [{"currency", "USD"}], 175 | name: "item", 176 | content: [ 177 | %{attributes: [], name: "sku", content: "ABC"}, 178 | %{attributes: [], name: "description", content: "Hat"}, 179 | %{attributes: [], name: "price", content: "5.99"}, 180 | %{attributes: [], name: "quantity", content: "2"} 181 | ] 182 | } 183 | } 184 | ] 185 | } 186 | ] 187 | } 188 | end 189 | 190 | def sample_xml do 191 | """ 192 | 193 | 194 | 123 195 | My address 196 | 197 | 198 | ABC 199 | Hat 200 | 5.99 201 | 202 | 1 203 | 204 | 205 | 206 | ABC 207 | Bat 208 | 9.99 209 | 210 | 2 211 | 212 | 213 | 214 | 215 | 216 | 124 217 | Uncle's House 218 | 219 | 220 | ABC 221 | Hat 222 | 5.99 223 | 2 224 | 225 | 226 | 227 | 228 | """ 229 | end 230 | 231 | def amazon_expected_naive do 232 | %{ 233 | "GetReportRequestListResponse" => %{ 234 | "GetReportRequestListResult" => %{ 235 | "HasNext" => "true", 236 | "NextToken" => 237 | "bnKUjUwrpfD2jpZedg0wbVuY6vtoszFEs90MCUIyGQ/PkNXwVrATLSf6YzH8PQiWICyhlLgHd4gqVtOYt5i3YX/y5ZICxITwrMWltwHPross7S2LHmNKmcpVErfopfm7ZgI5YM+bbLFRPCnQrq7eGPqiUs2SoKaRPxuuVZAjoAG5Hd34Twm1igafEPREmauvQPEfQK/OReJ9wNJ/XIY3rAvjRfjTJJa5YKoSylcR8gttj983g7esDr0wZ3V0GwaZstMPcqxOnL//uIo+owquzirF36SWlaJ9J5zSS6le1iIsxqkIMXCWKNSOyeZZ1ics+UXSqjS0c15jmJnjJN2V5uMEDoXRsC9PFEVVZ6joTY2uGFVSjAf2NsFIcEAdr4xQz2Y051TPxxk=", 238 | "ReportRequestInfo" => [ 239 | %{ 240 | "CompletedDate" => "2016-11-18T20:53:14+00:00", 241 | "EndDate" => "2016-11-18T20:53:00+00:00", 242 | "GeneratedReportId" => "3412841972017123", 243 | "ReportProcessingStatus" => "_DONE_", 244 | "ReportRequestId" => "50920017123", 245 | "ReportType" => "_GET_FBA_MYI_UNSUPPRESSED_INVENTORY_DATA_", 246 | "Scheduled" => "false", 247 | "StartDate" => "2016-11-18T20:53:00+00:00", 248 | "StartedProcessingDate" => "2016-11-18T20:53:07+00:00", 249 | "SubmittedDate" => "2016-11-18T20:53:00+00:00" 250 | }, 251 | %{ 252 | "CompletedDate" => "2016-11-18T20:51:57+00:00", 253 | "EndDate" => "2016-11-18T20:51:44+00:00", 254 | "GeneratedReportId" => "3414908503017123", 255 | "ReportProcessingStatus" => "_DONE_", 256 | "ReportRequestId" => "50919017123", 257 | "ReportType" => "_GET_AFN_INVENTORY_DATA_BY_COUNTRY_", 258 | "Scheduled" => "false", 259 | "StartDate" => "2016-11-18T20:51:44+00:00", 260 | "StartedProcessingDate" => "2016-11-18T20:51:49+00:00", 261 | "SubmittedDate" => "2016-11-18T20:51:44+00:00" 262 | }, 263 | %{ 264 | "CompletedDate" => "2016-11-18T14:54:24+00:00", 265 | "EndDate" => "2016-11-18T14:54:10+00:00", 266 | "GeneratedReportId" => "3410642176017123", 267 | "ReportProcessingStatus" => "_DONE_", 268 | "ReportRequestId" => "50918017123", 269 | "ReportType" => "_GET_FBA_MYI_UNSUPPRESSED_INVENTORY_DATA_", 270 | "Scheduled" => "false", 271 | "StartDate" => "2016-11-18T14:54:10+00:00", 272 | "StartedProcessingDate" => "2016-11-18T14:54:17+00:00", 273 | "SubmittedDate" => "2016-11-18T14:54:10+00:00" 274 | }, 275 | %{ 276 | "CompletedDate" => "2016-11-18T14:52:37+00:00", 277 | "EndDate" => "2016-11-18T14:52:26+00:00", 278 | "GeneratedReportId" => "3417419172017123", 279 | "ReportProcessingStatus" => "_DONE_", 280 | "ReportRequestId" => "50917017123", 281 | "ReportType" => "_GET_AFN_INVENTORY_DATA_BY_COUNTRY_", 282 | "Scheduled" => "false", 283 | "StartDate" => "2016-11-18T14:52:26+00:00", 284 | "StartedProcessingDate" => "2016-11-18T14:52:32+00:00", 285 | "SubmittedDate" => "2016-11-18T14:52:26+00:00" 286 | }, 287 | %{ 288 | "CompletedDate" => "2016-11-18T08:54:01+00:00", 289 | "EndDate" => "2016-11-18T08:53:49+00:00", 290 | "GeneratedReportId" => "3408643280017123", 291 | "ReportProcessingStatus" => "_DONE_", 292 | "ReportRequestId" => "50916017123", 293 | "ReportType" => "_GET_FBA_MYI_UNSUPPRESSED_INVENTORY_DATA_", 294 | "Scheduled" => "false", 295 | "StartDate" => "2016-11-18T08:53:49+00:00", 296 | "StartedProcessingDate" => "2016-11-18T08:53:54+00:00", 297 | "SubmittedDate" => "2016-11-18T08:53:49+00:00" 298 | }, 299 | %{ 300 | "CompletedDate" => "2016-11-18T08:51:55+00:00", 301 | "EndDate" => "2016-11-18T08:51:43+00:00", 302 | "GeneratedReportId" => "3410105984017123", 303 | "ReportProcessingStatus" => "_DONE_", 304 | "ReportRequestId" => "50915017123", 305 | "ReportType" => "_GET_AFN_INVENTORY_DATA_BY_COUNTRY_", 306 | "Scheduled" => "false", 307 | "StartDate" => "2016-11-18T08:51:43+00:00", 308 | "StartedProcessingDate" => "2016-11-18T08:51:49+00:00", 309 | "SubmittedDate" => "2016-11-18T08:51:43+00:00" 310 | }, 311 | %{ 312 | "CompletedDate" => "2016-11-18T02:57:46+00:00", 313 | "EndDate" => "2016-11-18T02:57:34+00:00", 314 | "GeneratedReportId" => "3408556063017123", 315 | "ReportProcessingStatus" => "_DONE_", 316 | "ReportRequestId" => "50914017123", 317 | "ReportType" => "_GET_FBA_MYI_UNSUPPRESSED_INVENTORY_DATA_", 318 | "Scheduled" => "false", 319 | "StartDate" => "2016-11-18T02:57:34+00:00", 320 | "StartedProcessingDate" => "2016-11-18T02:57:39+00:00", 321 | "SubmittedDate" => "2016-11-18T02:57:34+00:00" 322 | }, 323 | %{ 324 | "CompletedDate" => "2016-11-18T02:56:12+00:00", 325 | "EndDate" => "2016-11-18T02:55:59+00:00", 326 | "GeneratedReportId" => "3402759511017123", 327 | "ReportProcessingStatus" => "_DONE_", 328 | "ReportRequestId" => "50913017123", 329 | "ReportType" => "_GET_AFN_INVENTORY_DATA_BY_COUNTRY_", 330 | "Scheduled" => "false", 331 | "StartDate" => "2016-11-18T02:55:59+00:00", 332 | "StartedProcessingDate" => "2016-11-18T02:56:05+00:00", 333 | "SubmittedDate" => "2016-11-18T02:55:59+00:00" 334 | }, 335 | %{ 336 | "CompletedDate" => "2016-11-17T20:56:04+00:00", 337 | "EndDate" => "2016-11-17T20:55:49+00:00", 338 | "GeneratedReportId" => "3399094295017122", 339 | "ReportProcessingStatus" => "_DONE_", 340 | "ReportRequestId" => "50912017122", 341 | "ReportType" => "_GET_FBA_MYI_UNSUPPRESSED_INVENTORY_DATA_", 342 | "Scheduled" => "false", 343 | "StartDate" => "2016-11-17T20:55:49+00:00", 344 | "StartedProcessingDate" => "2016-11-17T20:55:54+00:00", 345 | "SubmittedDate" => "2016-11-17T20:55:49+00:00" 346 | }, 347 | %{ 348 | "CompletedDate" => "2016-11-17T20:54:45+00:00", 349 | "EndDate" => "2016-11-17T20:54:33+00:00", 350 | "GeneratedReportId" => "3405850523017122", 351 | "ReportProcessingStatus" => "_DONE_", 352 | "ReportRequestId" => "50911017122", 353 | "ReportType" => "_GET_AFN_INVENTORY_DATA_BY_COUNTRY_", 354 | "Scheduled" => "false", 355 | "StartDate" => "2016-11-17T20:54:33+00:00", 356 | "StartedProcessingDate" => "2016-11-17T20:54:39+00:00", 357 | "SubmittedDate" => "2016-11-17T20:54:33+00:00" 358 | } 359 | ] 360 | }, 361 | "ResponseMetadata" => %{"RequestId" => "7509cdb2-0b69-4ca0-89dc-c77f8a747834"} 362 | } 363 | } 364 | end 365 | 366 | def amazon_expected_nested do 367 | %{ 368 | attributes: [], 369 | content: [ 370 | %{ 371 | attributes: [], 372 | name: "GetReportRequestListResult", 373 | content: [ 374 | %{attributes: [], name: "HasNext", content: "true"}, 375 | %{ 376 | attributes: [], 377 | name: "ReportRequestInfo", 378 | content: [ 379 | %{ 380 | attributes: [], 381 | name: "ReportType", 382 | content: "_GET_FBA_MYI_UNSUPPRESSED_INVENTORY_DATA_" 383 | }, 384 | %{attributes: [], name: "ReportProcessingStatus", content: "_DONE_"}, 385 | %{attributes: [], name: "EndDate", content: "2016-11-18T20:53:00+00:00"}, 386 | %{attributes: [], name: "Scheduled", content: "false"}, 387 | %{attributes: [], name: "ReportRequestId", content: "50920017123"}, 388 | %{ 389 | attributes: [], 390 | name: "StartedProcessingDate", 391 | content: "2016-11-18T20:53:07+00:00" 392 | }, 393 | %{attributes: [], name: "SubmittedDate", content: "2016-11-18T20:53:00+00:00"}, 394 | %{attributes: [], name: "StartDate", content: "2016-11-18T20:53:00+00:00"}, 395 | %{attributes: [], name: "CompletedDate", content: "2016-11-18T20:53:14+00:00"}, 396 | %{attributes: [], name: "GeneratedReportId", content: "3412841972017123"} 397 | ] 398 | }, 399 | %{ 400 | attributes: [], 401 | name: "ReportRequestInfo", 402 | content: [ 403 | %{ 404 | attributes: [], 405 | name: "ReportType", 406 | content: "_GET_AFN_INVENTORY_DATA_BY_COUNTRY_" 407 | }, 408 | %{attributes: [], name: "ReportProcessingStatus", content: "_DONE_"}, 409 | %{attributes: [], name: "EndDate", content: "2016-11-18T20:51:44+00:00"}, 410 | %{attributes: [], name: "Scheduled", content: "false"}, 411 | %{attributes: [], name: "ReportRequestId", content: "50919017123"}, 412 | %{ 413 | attributes: [], 414 | name: "StartedProcessingDate", 415 | content: "2016-11-18T20:51:49+00:00" 416 | }, 417 | %{attributes: [], name: "SubmittedDate", content: "2016-11-18T20:51:44+00:00"}, 418 | %{attributes: [], name: "StartDate", content: "2016-11-18T20:51:44+00:00"}, 419 | %{attributes: [], name: "CompletedDate", content: "2016-11-18T20:51:57+00:00"}, 420 | %{attributes: [], name: "GeneratedReportId", content: "3414908503017123"} 421 | ] 422 | }, 423 | %{ 424 | attributes: [], 425 | name: "ReportRequestInfo", 426 | content: [ 427 | %{ 428 | attributes: [], 429 | name: "ReportType", 430 | content: "_GET_FBA_MYI_UNSUPPRESSED_INVENTORY_DATA_" 431 | }, 432 | %{attributes: [], name: "ReportProcessingStatus", content: "_DONE_"}, 433 | %{attributes: [], name: "EndDate", content: "2016-11-18T14:54:10+00:00"}, 434 | %{attributes: [], name: "Scheduled", content: "false"}, 435 | %{attributes: [], name: "ReportRequestId", content: "50918017123"}, 436 | %{ 437 | attributes: [], 438 | name: "StartedProcessingDate", 439 | content: "2016-11-18T14:54:17+00:00" 440 | }, 441 | %{attributes: [], name: "SubmittedDate", content: "2016-11-18T14:54:10+00:00"}, 442 | %{attributes: [], name: "StartDate", content: "2016-11-18T14:54:10+00:00"}, 443 | %{attributes: [], name: "CompletedDate", content: "2016-11-18T14:54:24+00:00"}, 444 | %{attributes: [], name: "GeneratedReportId", content: "3410642176017123"} 445 | ] 446 | }, 447 | %{ 448 | attributes: [], 449 | name: "ReportRequestInfo", 450 | content: [ 451 | %{ 452 | attributes: [], 453 | name: "ReportType", 454 | content: "_GET_AFN_INVENTORY_DATA_BY_COUNTRY_" 455 | }, 456 | %{attributes: [], name: "ReportProcessingStatus", content: "_DONE_"}, 457 | %{attributes: [], name: "EndDate", content: "2016-11-18T14:52:26+00:00"}, 458 | %{attributes: [], name: "Scheduled", content: "false"}, 459 | %{attributes: [], name: "ReportRequestId", content: "50917017123"}, 460 | %{ 461 | attributes: [], 462 | name: "StartedProcessingDate", 463 | content: "2016-11-18T14:52:32+00:00" 464 | }, 465 | %{attributes: [], name: "SubmittedDate", content: "2016-11-18T14:52:26+00:00"}, 466 | %{attributes: [], name: "StartDate", content: "2016-11-18T14:52:26+00:00"}, 467 | %{attributes: [], name: "CompletedDate", content: "2016-11-18T14:52:37+00:00"}, 468 | %{attributes: [], name: "GeneratedReportId", content: "3417419172017123"} 469 | ] 470 | }, 471 | %{ 472 | attributes: [], 473 | name: "ReportRequestInfo", 474 | content: [ 475 | %{ 476 | attributes: [], 477 | name: "ReportType", 478 | content: "_GET_FBA_MYI_UNSUPPRESSED_INVENTORY_DATA_" 479 | }, 480 | %{attributes: [], name: "ReportProcessingStatus", content: "_DONE_"}, 481 | %{attributes: [], name: "EndDate", content: "2016-11-18T08:53:49+00:00"}, 482 | %{attributes: [], name: "Scheduled", content: "false"}, 483 | %{attributes: [], name: "ReportRequestId", content: "50916017123"}, 484 | %{ 485 | attributes: [], 486 | name: "StartedProcessingDate", 487 | content: "2016-11-18T08:53:54+00:00" 488 | }, 489 | %{attributes: [], name: "SubmittedDate", content: "2016-11-18T08:53:49+00:00"}, 490 | %{attributes: [], name: "StartDate", content: "2016-11-18T08:53:49+00:00"}, 491 | %{attributes: [], name: "CompletedDate", content: "2016-11-18T08:54:01+00:00"}, 492 | %{attributes: [], name: "GeneratedReportId", content: "3408643280017123"} 493 | ] 494 | }, 495 | %{ 496 | attributes: [], 497 | name: "ReportRequestInfo", 498 | content: [ 499 | %{ 500 | attributes: [], 501 | name: "ReportType", 502 | content: "_GET_AFN_INVENTORY_DATA_BY_COUNTRY_" 503 | }, 504 | %{attributes: [], name: "ReportProcessingStatus", content: "_DONE_"}, 505 | %{attributes: [], name: "EndDate", content: "2016-11-18T08:51:43+00:00"}, 506 | %{attributes: [], name: "Scheduled", content: "false"}, 507 | %{attributes: [], name: "ReportRequestId", content: "50915017123"}, 508 | %{ 509 | attributes: [], 510 | name: "StartedProcessingDate", 511 | content: "2016-11-18T08:51:49+00:00" 512 | }, 513 | %{attributes: [], name: "SubmittedDate", content: "2016-11-18T08:51:43+00:00"}, 514 | %{attributes: [], name: "StartDate", content: "2016-11-18T08:51:43+00:00"}, 515 | %{attributes: [], name: "CompletedDate", content: "2016-11-18T08:51:55+00:00"}, 516 | %{attributes: [], name: "GeneratedReportId", content: "3410105984017123"} 517 | ] 518 | }, 519 | %{ 520 | attributes: [], 521 | name: "ReportRequestInfo", 522 | content: [ 523 | %{ 524 | attributes: [], 525 | name: "ReportType", 526 | content: "_GET_FBA_MYI_UNSUPPRESSED_INVENTORY_DATA_" 527 | }, 528 | %{attributes: [], name: "ReportProcessingStatus", content: "_DONE_"}, 529 | %{attributes: [], name: "EndDate", content: "2016-11-18T02:57:34+00:00"}, 530 | %{attributes: [], name: "Scheduled", content: "false"}, 531 | %{attributes: [], name: "ReportRequestId", content: "50914017123"}, 532 | %{ 533 | attributes: [], 534 | name: "StartedProcessingDate", 535 | content: "2016-11-18T02:57:39+00:00" 536 | }, 537 | %{attributes: [], name: "SubmittedDate", content: "2016-11-18T02:57:34+00:00"}, 538 | %{attributes: [], name: "StartDate", content: "2016-11-18T02:57:34+00:00"}, 539 | %{attributes: [], name: "CompletedDate", content: "2016-11-18T02:57:46+00:00"}, 540 | %{attributes: [], name: "GeneratedReportId", content: "3408556063017123"} 541 | ] 542 | }, 543 | %{ 544 | attributes: [], 545 | name: "ReportRequestInfo", 546 | content: [ 547 | %{ 548 | attributes: [], 549 | name: "ReportType", 550 | content: "_GET_AFN_INVENTORY_DATA_BY_COUNTRY_" 551 | }, 552 | %{attributes: [], name: "ReportProcessingStatus", content: "_DONE_"}, 553 | %{attributes: [], name: "EndDate", content: "2016-11-18T02:55:59+00:00"}, 554 | %{attributes: [], name: "Scheduled", content: "false"}, 555 | %{attributes: [], name: "ReportRequestId", content: "50913017123"}, 556 | %{ 557 | attributes: [], 558 | name: "StartedProcessingDate", 559 | content: "2016-11-18T02:56:05+00:00" 560 | }, 561 | %{attributes: [], name: "SubmittedDate", content: "2016-11-18T02:55:59+00:00"}, 562 | %{attributes: [], name: "StartDate", content: "2016-11-18T02:55:59+00:00"}, 563 | %{attributes: [], name: "CompletedDate", content: "2016-11-18T02:56:12+00:00"}, 564 | %{attributes: [], name: "GeneratedReportId", content: "3402759511017123"} 565 | ] 566 | }, 567 | %{ 568 | attributes: [], 569 | name: "ReportRequestInfo", 570 | content: [ 571 | %{ 572 | attributes: [], 573 | name: "ReportType", 574 | content: "_GET_FBA_MYI_UNSUPPRESSED_INVENTORY_DATA_" 575 | }, 576 | %{attributes: [], name: "ReportProcessingStatus", content: "_DONE_"}, 577 | %{attributes: [], name: "EndDate", content: "2016-11-17T20:55:49+00:00"}, 578 | %{attributes: [], name: "Scheduled", content: "false"}, 579 | %{attributes: [], name: "ReportRequestId", content: "50912017122"}, 580 | %{ 581 | attributes: [], 582 | name: "StartedProcessingDate", 583 | content: "2016-11-17T20:55:54+00:00" 584 | }, 585 | %{attributes: [], name: "SubmittedDate", content: "2016-11-17T20:55:49+00:00"}, 586 | %{attributes: [], name: "StartDate", content: "2016-11-17T20:55:49+00:00"}, 587 | %{attributes: [], name: "CompletedDate", content: "2016-11-17T20:56:04+00:00"}, 588 | %{attributes: [], name: "GeneratedReportId", content: "3399094295017122"} 589 | ] 590 | }, 591 | %{ 592 | attributes: [], 593 | name: "ReportRequestInfo", 594 | content: [ 595 | %{ 596 | attributes: [], 597 | name: "ReportType", 598 | content: "_GET_AFN_INVENTORY_DATA_BY_COUNTRY_" 599 | }, 600 | %{attributes: [], name: "ReportProcessingStatus", content: "_DONE_"}, 601 | %{attributes: [], name: "EndDate", content: "2016-11-17T20:54:33+00:00"}, 602 | %{attributes: [], name: "Scheduled", content: "false"}, 603 | %{attributes: [], name: "ReportRequestId", content: "50911017122"}, 604 | %{ 605 | attributes: [], 606 | name: "StartedProcessingDate", 607 | content: "2016-11-17T20:54:39+00:00" 608 | }, 609 | %{attributes: [], name: "SubmittedDate", content: "2016-11-17T20:54:33+00:00"}, 610 | %{attributes: [], name: "StartDate", content: "2016-11-17T20:54:33+00:00"}, 611 | %{attributes: [], name: "CompletedDate", content: "2016-11-17T20:54:45+00:00"}, 612 | %{attributes: [], name: "GeneratedReportId", content: "3405850523017122"} 613 | ] 614 | }, 615 | %{ 616 | attributes: [], 617 | name: "NextToken", 618 | content: 619 | "bnKUjUwrpfD2jpZedg0wbVuY6vtoszFEs90MCUIyGQ/PkNXwVrATLSf6YzH8PQiWICyhlLgHd4gqVtOYt5i3YX/y5ZICxITwrMWltwHPross7S2LHmNKmcpVErfopfm7ZgI5YM+bbLFRPCnQrq7eGPqiUs2SoKaRPxuuVZAjoAG5Hd34Twm1igafEPREmauvQPEfQK/OReJ9wNJ/XIY3rAvjRfjTJJa5YKoSylcR8gttj983g7esDr0wZ3V0GwaZstMPcqxOnL//uIo+owquzirF36SWlaJ9J5zSS6le1iIsxqkIMXCWKNSOyeZZ1ics+UXSqjS0c15jmJnjJN2V5uMEDoXRsC9PFEVVZ6joTY2uGFVSjAf2NsFIcEAdr4xQz2Y051TPxxk=" 620 | } 621 | ] 622 | }, 623 | %{ 624 | attributes: [], 625 | name: "ResponseMetadata", 626 | content: %{ 627 | attributes: [], 628 | name: "RequestId", 629 | content: "7509cdb2-0b69-4ca0-89dc-c77f8a747834" 630 | } 631 | } 632 | ], 633 | name: "GetReportRequestListResponse" 634 | } 635 | end 636 | 637 | def set_prefix_namespace_fn do 638 | fn name, namespace, prefix -> 639 | cond do 640 | namespace != [] && prefix != [] -> "#{prefix}_namespace:#{name}" 641 | true -> name 642 | end 643 | end 644 | end 645 | 646 | def facebook_custom_function_expected do 647 | %{ 648 | "rss" => %{ 649 | "#content" => %{ 650 | "channel" => %{ 651 | "title" => "Test Store", 652 | "link" => "http://www.example.com", 653 | "description" => "An example item from the feed", 654 | "item" => %{ 655 | "g_namespace:id" => "DB_1", 656 | "g_namespace:title" => "Dog Bowl In Blue", 657 | "g_namespace:description" => "Solid plastic Dog Bowl in marine blue color", 658 | "g_namespace:link" => "http://www.example.com/bowls/db-1.html", 659 | "g_namespace:image_link" => "http://images.example.com/DB_1.png", 660 | "g_namespace:brand" => "Example", 661 | "g_namespace:condition" => "new", 662 | "g_namespace:availability" => "in stock", 663 | "g_namespace:price" => "9.99 GBP", 664 | "g_namespace:shipping" => %{ 665 | "g_namespace:country" => "UK", 666 | "g_namespace:service" => "Standard", 667 | "g_namespace:price" => "4.95 GBP" 668 | }, 669 | "g_namespace:google_product_category" => "Animals > Pet Supplies", 670 | "g_namespace:custom_label_0" => "Made in Waterford, IE" 671 | } 672 | } 673 | }, 674 | "-version" => "2.0" 675 | } 676 | } 677 | end 678 | 679 | def facebook_xmlns_xml_expected do 680 | %{ 681 | "rss" => %{ 682 | "#content" => %{ 683 | "channel" => %{ 684 | "title" => "Test Store", 685 | "link" => "http://www.example.com", 686 | "description" => "An example item from the feed", 687 | "item" => %{ 688 | "g:id" => "DB_1", 689 | "g:title" => "Dog Bowl In Blue", 690 | "g:description" => "Solid plastic Dog Bowl in marine blue color", 691 | "g:link" => "http://www.example.com/bowls/db-1.html", 692 | "g:image_link" => "http://images.example.com/DB_1.png", 693 | "g:brand" => "Example", 694 | "g:condition" => "new", 695 | "g:availability" => "in stock", 696 | "g:price" => "9.99 GBP", 697 | "g:shipping" => %{ 698 | "g:country" => "UK", 699 | "g:service" => "Standard", 700 | "g:price" => "4.95 GBP" 701 | }, 702 | "g:google_product_category" => "Animals > Pet Supplies", 703 | "g:custom_label_0" => "Made in Waterford, IE" 704 | } 705 | } 706 | }, 707 | "-version" => "2.0" 708 | } 709 | } 710 | end 711 | 712 | def facebook_xmlns_xml do 713 | """ 714 | 715 | 716 | 717 | Test Store 718 | http://www.example.com 719 | An example item from the feed 720 | 721 | DB_1 722 | Dog Bowl In Blue 723 | Solid plastic Dog Bowl in marine blue color 724 | http://www.example.com/bowls/db-1.html 725 | http://images.example.com/DB_1.png 726 | Example 727 | new 728 | in stock 729 | 9.99 GBP 730 | 731 | UK 732 | Standard 733 | 4.95 GBP 734 | 735 | Animals > Pet Supplies 736 | Made in Waterford, IE 737 | 738 | 739 | 740 | """ 741 | end 742 | 743 | def amazon_xml do 744 | """ 745 | 746 | 747 | 748 | true 749 | 750 | _GET_FBA_MYI_UNSUPPRESSED_INVENTORY_DATA_ 751 | _DONE_ 752 | 2016-11-18T20:53:00+00:00 753 | false 754 | 50920017123 755 | 2016-11-18T20:53:07+00:00 756 | 2016-11-18T20:53:00+00:00 757 | 2016-11-18T20:53:00+00:00 758 | 2016-11-18T20:53:14+00:00 759 | 3412841972017123 760 | 761 | 762 | _GET_AFN_INVENTORY_DATA_BY_COUNTRY_ 763 | _DONE_ 764 | 2016-11-18T20:51:44+00:00 765 | false 766 | 50919017123 767 | 2016-11-18T20:51:49+00:00 768 | 2016-11-18T20:51:44+00:00 769 | 2016-11-18T20:51:44+00:00 770 | 2016-11-18T20:51:57+00:00 771 | 3414908503017123 772 | 773 | 774 | _GET_FBA_MYI_UNSUPPRESSED_INVENTORY_DATA_ 775 | _DONE_ 776 | 2016-11-18T14:54:10+00:00 777 | false 778 | 50918017123 779 | 2016-11-18T14:54:17+00:00 780 | 2016-11-18T14:54:10+00:00 781 | 2016-11-18T14:54:10+00:00 782 | 2016-11-18T14:54:24+00:00 783 | 3410642176017123 784 | 785 | 786 | _GET_AFN_INVENTORY_DATA_BY_COUNTRY_ 787 | _DONE_ 788 | 2016-11-18T14:52:26+00:00 789 | false 790 | 50917017123 791 | 2016-11-18T14:52:32+00:00 792 | 2016-11-18T14:52:26+00:00 793 | 2016-11-18T14:52:26+00:00 794 | 2016-11-18T14:52:37+00:00 795 | 3417419172017123 796 | 797 | 798 | _GET_FBA_MYI_UNSUPPRESSED_INVENTORY_DATA_ 799 | _DONE_ 800 | 2016-11-18T08:53:49+00:00 801 | false 802 | 50916017123 803 | 2016-11-18T08:53:54+00:00 804 | 2016-11-18T08:53:49+00:00 805 | 2016-11-18T08:53:49+00:00 806 | 2016-11-18T08:54:01+00:00 807 | 3408643280017123 808 | 809 | 810 | _GET_AFN_INVENTORY_DATA_BY_COUNTRY_ 811 | _DONE_ 812 | 2016-11-18T08:51:43+00:00 813 | false 814 | 50915017123 815 | 2016-11-18T08:51:49+00:00 816 | 2016-11-18T08:51:43+00:00 817 | 2016-11-18T08:51:43+00:00 818 | 2016-11-18T08:51:55+00:00 819 | 3410105984017123 820 | 821 | 822 | _GET_FBA_MYI_UNSUPPRESSED_INVENTORY_DATA_ 823 | _DONE_ 824 | 2016-11-18T02:57:34+00:00 825 | false 826 | 50914017123 827 | 2016-11-18T02:57:39+00:00 828 | 2016-11-18T02:57:34+00:00 829 | 2016-11-18T02:57:34+00:00 830 | 2016-11-18T02:57:46+00:00 831 | 3408556063017123 832 | 833 | 834 | _GET_AFN_INVENTORY_DATA_BY_COUNTRY_ 835 | _DONE_ 836 | 2016-11-18T02:55:59+00:00 837 | false 838 | 50913017123 839 | 2016-11-18T02:56:05+00:00 840 | 2016-11-18T02:55:59+00:00 841 | 2016-11-18T02:55:59+00:00 842 | 2016-11-18T02:56:12+00:00 843 | 3402759511017123 844 | 845 | 846 | _GET_FBA_MYI_UNSUPPRESSED_INVENTORY_DATA_ 847 | _DONE_ 848 | 2016-11-17T20:55:49+00:00 849 | false 850 | 50912017122 851 | 2016-11-17T20:55:54+00:00 852 | 2016-11-17T20:55:49+00:00 853 | 2016-11-17T20:55:49+00:00 854 | 2016-11-17T20:56:04+00:00 855 | 3399094295017122 856 | 857 | 858 | _GET_AFN_INVENTORY_DATA_BY_COUNTRY_ 859 | _DONE_ 860 | 2016-11-17T20:54:33+00:00 861 | false 862 | 50911017122 863 | 2016-11-17T20:54:39+00:00 864 | 2016-11-17T20:54:33+00:00 865 | 2016-11-17T20:54:33+00:00 866 | 2016-11-17T20:54:45+00:00 867 | 3405850523017122 868 | 869 | bnKUjUwrpfD2jpZedg0wbVuY6vtoszFEs90MCUIyGQ/PkNXwVrATLSf6YzH8PQiWICyhlLgHd4gqVtOYt5i3YX/y5ZICxITwrMWltwHPross7S2LHmNKmcpVErfopfm7ZgI5YM+bbLFRPCnQrq7eGPqiUs2SoKaRPxuuVZAjoAG5Hd34Twm1igafEPREmauvQPEfQK/OReJ9wNJ/XIY3rAvjRfjTJJa5YKoSylcR8gttj983g7esDr0wZ3V0GwaZstMPcqxOnL//uIo+owquzirF36SWlaJ9J5zSS6le1iIsxqkIMXCWKNSOyeZZ1ics+UXSqjS0c15jmJnjJN2V5uMEDoXRsC9PFEVVZ6joTY2uGFVSjAf2NsFIcEAdr4xQz2Y051TPxxk= 870 | 871 | 872 | 7509cdb2-0b69-4ca0-89dc-c77f8a747834 873 | 874 | 875 | """ 876 | end 877 | end 878 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------