├── .tool-versions ├── config ├── dev.exs ├── test.exs └── config.exs ├── spec ├── support │ └── json │ │ └── valid.json ├── spec_helper.exs └── lib │ └── test_that_json │ ├── loading_spec.exs │ ├── normalization_spec.exs │ ├── configuration_spec.exs │ ├── parsing_spec.exs │ ├── generation_spec.exs │ ├── helpers │ ├── has_json_size_spec.exs │ ├── has_json_keys_spec.exs │ ├── has_only_json_keys_spec.exs │ ├── has_json_properties_spec.exs │ ├── has_only_json_properties_spec.exs │ ├── is_json_equal_spec.exs │ ├── has_json_path_spec.exs │ ├── has_json_values_spec.exs │ ├── has_json_type_spec.exs │ └── has_only_json_values_spec.exs │ ├── exclusion_spec.exs │ ├── pathing_spec.exs │ └── helpers_spec.exs ├── test ├── test_helper.exs └── helpers_test.exs ├── lib ├── test_that_json.ex └── test_that_json │ ├── normalization.ex │ ├── configuration.ex │ ├── exclusion.ex │ ├── loading.ex │ ├── parsing.ex │ ├── generation.ex │ ├── pathing.ex │ ├── exceptions.ex │ ├── helpers.ex │ └── json.ex ├── .travis.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── mix.lock ├── mix.exs ├── CODE_OF_CONDUCT.md └── README.md /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.6.5 2 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /spec/support/json/valid.json: -------------------------------------------------------------------------------- 1 | [1,2,3,4] 2 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /lib/test_that_json.ex: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson do 2 | end 3 | -------------------------------------------------------------------------------- /spec/spec_helper.exs: -------------------------------------------------------------------------------- 1 | ESpec.configure fn(config) -> 2 | config.before fn -> 3 | {:shared, hello: :world} 4 | end 5 | 6 | config.finally fn(_shared) -> 7 | :ok 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/test_that_json/normalization.ex: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Normalization do 2 | def prettify(json) do 3 | JSX.prettify(json) 4 | end 5 | 6 | def prettify!(json) do 7 | JSX.prettify!(json) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/test_that_json/configuration.ex: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Configuration do 2 | def excluded_keys do 3 | Application.get_env(:test_that_json, :excluded_keys, default_excluded_keys()) 4 | end 5 | 6 | def default_excluded_keys, do: ~w(id inserted_at updated_at) 7 | end 8 | 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 1.4.5 4 | - 1.5.3 5 | - 1.6.5 6 | otp_release: 7 | - 18.3 8 | - 19.3 9 | - 20.3 10 | sudo: false # to use faster container based build environment 11 | script: 12 | - mix deps.get 13 | - mix espec 14 | - mix test 15 | after_script: 16 | - MIX_ENV=docs mix deps.get 17 | - MIX_ENV=docs mix inch.report 18 | -------------------------------------------------------------------------------- /spec/lib/test_that_json/loading_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.LoadingSpec do 2 | use ESpec 3 | 4 | alias TestThatJson.Loading 5 | 6 | let :path, do: "spec/support/json/valid.json" 7 | 8 | describe "load" do 9 | subject do: Loading.load(path) 10 | 11 | it do: should eq({:ok, "[1,2,3,4]"}) 12 | end 13 | 14 | describe "load!" do 15 | subject do: Loading.load!(path) 16 | 17 | it do: should eq("[1,2,3,4]") 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/test_that_json/exclusion.ex: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Exclusion do 2 | alias TestThatJson.Configuration 3 | 4 | def exclude_keys(list) when is_list(list) do 5 | for value <- list, do: exclude_keys(value) 6 | end 7 | def exclude_keys(map) when is_map(map) do 8 | Map.drop(map, excluded_keys()) 9 | end 10 | def exclude_keys(other), do: other 11 | 12 | def excluded_keys do 13 | MapSet.new(Configuration.excluded_keys()) 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /lib/test_that_json/loading.ex: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Loading do 2 | alias TestThatJson.Generation 3 | alias TestThatJson.Parsing 4 | 5 | def load(path) do 6 | with {:ok, json} <- File.read(path), 7 | {:ok, value} <- Parsing.parse(json), 8 | do: Generation.generate(value) 9 | end 10 | 11 | def load!(path) do 12 | json = File.read!(path) 13 | value = Parsing.parse!(json) 14 | Generation.generate!(value) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /spec/lib/test_that_json/normalization_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.NormalizationSpec do 2 | use ESpec 3 | 4 | alias TestThatJson.Normalization 5 | 6 | let :json, do: "[1,2,3,4]" 7 | 8 | describe "prettify" do 9 | subject do: Normalization.prettify(json) 10 | 11 | it do: should eq({:ok, "[\n 1,\n 2,\n 3,\n 4\n]"}) 12 | end 13 | 14 | describe "prettify!" do 15 | subject do: Normalization.prettify!(json) 16 | 17 | it do: should eq("[\n 1,\n 2,\n 3,\n 4\n]") 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.6.0 4 | 5 | ### Backwards-Incompatible Changes 6 | 7 | * Support elixir >= 1.4 8 | * Update exjsx to 4.0.0 9 | 10 | 11 | ## v0.5.0 12 | 13 | ### Backwards-Incompatible Changes 14 | 15 | * Move all functions from `TestThatJson.Assertions` into `TestThatJson.Helpers` and remove `TestThatJson.Assertions`. To upgrade, replace all references to `TestThatJson.Assertions` with `TestThatJson.Helpers`. 16 | 17 | 18 | ## v0.2.0 19 | 20 | * Implement `has_json_type` and `has_json_size` 21 | -------------------------------------------------------------------------------- /lib/test_that_json/parsing.ex: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Parsing do 2 | def parse!(json) when is_binary(json) do 3 | JSX.decode!(json) 4 | rescue 5 | ArgumentError -> raise TestThatJson.InvalidJsonError 6 | end 7 | def parse!(value), do: value 8 | 9 | def parse(json) when is_binary(json) do 10 | case JSX.decode(json) do 11 | {:error, _} -> {:error, {TestThatJson.InvalidJsonError, [json], "Invalid JSON"}} 12 | result -> result 13 | end 14 | end 15 | def parse(value), do: {:ok, value} 16 | end 17 | -------------------------------------------------------------------------------- /spec/lib/test_that_json/configuration_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.ConfigurationSpec do 2 | use ESpec 3 | 4 | alias TestThatJson.Configuration 5 | 6 | describe "excluded_keys" do 7 | subject do: Configuration.excluded_keys() 8 | 9 | context "with no keys in the environment" do 10 | # Assumes no env-configured keys 11 | it do: should eq(Configuration.default_excluded_keys()) 12 | end 13 | 14 | context "with keys in the environment" do 15 | let :excluded_keys, do: ~w(some other keys) 16 | 17 | before do 18 | allow Application |> to(accept(:get_env, fn(_, _, _) -> excluded_keys end)) 19 | end 20 | 21 | it do: should eq(excluded_keys) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/test_that_json/generation.ex: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Generation do 2 | alias TestThatJson.Normalization 3 | 4 | def generate(value) do 5 | case JSX.encode(value) do 6 | {:error, _} -> {:error, {TestThatJson.JsonEncodingError, [value]}} 7 | value -> value 8 | end 9 | end 10 | 11 | def generate!(value) do 12 | JSX.encode!(value) 13 | rescue 14 | ArgumentError -> raise TestThatJson.JsonEncodingError 15 | end 16 | 17 | def generate_prettified(value) do 18 | case generate(value) do 19 | {:ok, json} -> Normalization.prettify(json) 20 | error -> error 21 | end 22 | end 23 | 24 | def generate_prettified!(value) do 25 | json = generate!(value) 26 | Normalization.prettify!(json) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/lib/test_that_json/parsing_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.ParsingSpec do 2 | use ESpec 3 | 4 | alias TestThatJson.Parsing 5 | 6 | describe "parse!" do 7 | let :json, do: "[1,2,3]" 8 | 9 | before do 10 | allow JSX |> to(accept(:decode!, fn(_) -> nil end)) 11 | Parsing.parse!(json) 12 | end 13 | 14 | it "calls the correct command" do 15 | expect JSX |> to(accepted(:decode!, [json])) 16 | end 17 | end 18 | 19 | describe "parse" do 20 | let :json, do: "[1,2,3]" 21 | 22 | before do 23 | allow JSX |> to(accept(:decode, fn(_) -> nil end)) 24 | Parsing.parse(json) 25 | end 26 | 27 | it "calls the correct command" do 28 | expect JSX |> to(accepted(:decode, [json])) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/lib/test_that_json/generation_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.GenerationSpec do 2 | use ESpec 3 | 4 | alias TestThatJson.Generation 5 | 6 | describe "generate" do 7 | subject do: Generation.generate(value) 8 | 9 | let :value, do: "i'm a string" 10 | 11 | it do: should eq({:ok, "\"i'm a string\""}) 12 | end 13 | 14 | describe "generate!" do 15 | subject do: Generation.generate!(value) 16 | 17 | let :value, do: "i'm a string" 18 | 19 | it do: should eq("\"i'm a string\"") 20 | end 21 | 22 | describe "generate_prettified" do 23 | subject do: Generation.generate_prettified(value) 24 | 25 | let :value, do: [1,2,3,4] 26 | 27 | it do: should eq({:ok, "[\n 1,\n 2,\n 3,\n 4\n]"}) 28 | end 29 | 30 | describe "generate_prettified!" do 31 | subject do: Generation.generate_prettified!(value) 32 | 33 | let :value, do: [1,2,3,4] 34 | 35 | it do: should eq("[\n 1,\n 2,\n 3,\n 4\n]") 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/test_that_json/pathing.ex: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Pathing do 2 | def value_at_path(value, path) when is_binary(path) do 3 | path_parts = String.split(path, "/") 4 | Enum.reduce(path_parts, value, &do_value_at_path/2) 5 | end 6 | def value_at_path(value, nil), do: value 7 | def value_at_path(_value, _path), do: raise TestThatJson.InvalidPathError 8 | 9 | # PRIVATE ################################################## 10 | 11 | defp do_value_at_path(path_part, map_value) when is_map(map_value) do 12 | case Map.get(map_value, path_part, :error) do 13 | :error -> raise TestThatJson.PathNotFoundError 14 | value -> value 15 | end 16 | end 17 | defp do_value_at_path(path_part, list_value) when is_list(list_value) do 18 | {index, _} = Integer.parse(path_part) 19 | case Enum.at(list_value, index, :error) do 20 | :error -> raise TestThatJson.PathNotFoundError 21 | value -> value 22 | end 23 | end 24 | defp do_value_at_path(_path_part, _value), do: raise TestThatJson.InvalidPathError 25 | end 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Joshua Rieken 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/test_that_json/exceptions.ex: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.InvalidPathError do 2 | defexception [:message] 3 | 4 | def exception(attrs) do 5 | value = attrs[:value] 6 | message = attrs[:message] || "improperly formatted path: #{inspect value}" 7 | %TestThatJson.InvalidPathError{message: message} 8 | end 9 | end 10 | 11 | defmodule TestThatJson.PathNotFoundError do 12 | defexception [:message] 13 | 14 | def exception(attrs) do 15 | value = attrs[:value] 16 | message = attrs[:message] || "path not found: #{inspect value}" 17 | %TestThatJson.PathNotFoundError{message: message} 18 | end 19 | end 20 | 21 | defmodule TestThatJson.InvalidJsonError do 22 | defexception [:message] 23 | 24 | def exception(attrs) do 25 | value = attrs[:value] 26 | message = attrs[:message] || "unparseable JSON: #{inspect value}" 27 | %TestThatJson.InvalidJsonError{message: message} 28 | end 29 | end 30 | 31 | defmodule TestThatJson.JsonEncodingError do 32 | defexception [:message] 33 | 34 | def exception(attrs) do 35 | value = attrs[:value] 36 | message = attrs[:message] || "unable to JSON encode value: #{inspect value}" 37 | %TestThatJson.JsonEncodingError{message: message} 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :test_that_json, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:test_that_json, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"}, 3 | "espec": {:hex, :espec, "1.5.1", "46c603c4adb4244b152ea53c4d5f4545ab1eb1de1556588ec908b8e4ba570188", [:mix], [{:meck, "0.8.9", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "ex_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "inch_ex": {:hex, :inch_ex, "0.5.6", "418357418a553baa6d04eccd1b44171936817db61f4c0840112b420b8e378e67", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 7 | "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, 8 | "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"}, 9 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, 10 | } 11 | -------------------------------------------------------------------------------- /spec/lib/test_that_json/helpers/has_json_size_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Helpers.HasJsonSizeSpec do 2 | use ESpec 3 | 4 | import TestThatJson.Helpers, only: [has_json_size: 2] 5 | 6 | context "when the subject is a JSON object" do 7 | let :json do 8 | """ 9 | { 10 | "some": "property", 11 | "another": "property with value" 12 | } 13 | """ 14 | end 15 | 16 | it do: expect has_json_size(json, 2) |> to(be_true) 17 | it do: expect has_json_size(json, 5) |> to(be_false) 18 | it do: expect has_json_size(json, 0) |> to(be_false) 19 | end 20 | 21 | context "when the subject is a JSON array" do 22 | let :json, do: "[1,2]" 23 | 24 | it do: expect has_json_size(json, 2) |> to(be_true) 25 | it do: expect has_json_size(json, 1) |> to(be_false) 26 | end 27 | 28 | context "when the subject is a JSON string" do 29 | let :json, do: "\"string\"" 30 | 31 | it "raises an error" do 32 | raiser = fn -> 33 | expect has_json_size(json, 2) |> to(be_true) 34 | end 35 | 36 | expect raiser |> to(raise_exception(ArgumentError)) 37 | end 38 | end 39 | 40 | context "when the size is not an integer" do 41 | let :json, do: "[1,2,3]" 42 | 43 | it "raises an error" do 44 | raiser = fn -> 45 | expect has_json_size(json, "0") |> to(be_true) 46 | end 47 | 48 | expect raiser |> to(raise_exception(ArgumentError)) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/lib/test_that_json/helpers/has_json_keys_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Helpers.HasJsonKeysSpec do 2 | use ESpec 3 | 4 | import TestThatJson.Helpers, only: [has_json_keys: 2] 5 | 6 | context "when the subject is a JSON object" do 7 | let :json, do: "{\"some\": \"object\", \"another\": \"object with value\"}" 8 | 9 | context "when the value is a list" do 10 | it do: expect has_json_keys(json, ["some", "another"]) |> to(be_true) 11 | it do: expect has_json_keys(json, ["some", "another", "yet_another"]) |> to(be_false) 12 | end 13 | 14 | context "when the value is a string" do 15 | it do: expect has_json_keys(json, "some") |> to(be_true) 16 | it do: expect has_json_keys(json, "other") |> to(be_false) 17 | end 18 | 19 | context "when the value is neither a list or a string" do 20 | let :json, do: "[{\"some\": \"object\"}]" 21 | 22 | it "raises an error" do 23 | raiser = fn -> 24 | expect has_json_keys(json, %{"a" => "map"}) |> to(be_true) 25 | end 26 | 27 | expect raiser |> to(raise_exception(ArgumentError)) 28 | end 29 | end 30 | end 31 | 32 | context "when the subject is not a JSON object" do 33 | let :json, do: "[{\"some\": \"object\"}]" 34 | 35 | it "raises an error" do 36 | raiser = fn -> 37 | expect has_json_keys(json, ["some"]) |> to(be_true) 38 | end 39 | 40 | expect raiser |> to(raise_exception(ArgumentError)) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/lib/test_that_json/helpers/has_only_json_keys_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Helpers.HasOnlyJsonKeysSpec do 2 | use ESpec 3 | 4 | import TestThatJson.Helpers, only: [has_only_json_keys: 2] 5 | 6 | context "when the subject is a JSON object" do 7 | let :json, do: "{\"some\": \"object\", \"another\": \"object with value\"}" 8 | 9 | context "when the argument is a list" do 10 | it do: expect has_only_json_keys(json, ["some", "another"]) |> to(be_true) 11 | it do: expect has_only_json_keys(json, ["some"]) |> to(be_false) 12 | it do: expect has_only_json_keys(json, ["some", "another", "yet_another"]) |> to(be_false) 13 | end 14 | 15 | context "when the argument is not a list" do 16 | let :json, do: "{\"some\": \"object\"}" 17 | 18 | it do: expect has_only_json_keys(json, "some") |> to(be_true) 19 | it do: expect has_only_json_keys(json, "other") |> to(be_false) 20 | end 21 | 22 | context "when the value is neither a list or a string" do 23 | let :json, do: "[{\"some\": \"object\"}]" 24 | 25 | it "raises an error" do 26 | raiser = fn -> 27 | expect has_only_json_keys(json, %{"a" => "map"}) |> to(be_true) 28 | end 29 | 30 | expect raiser |> to(raise_exception(ArgumentError)) 31 | end 32 | end 33 | end 34 | 35 | context "when the subject is not a JSON object" do 36 | let :json, do: "[{\"some\": \"object\"}]" 37 | 38 | it "raises an error" do 39 | raiser = fn -> 40 | expect has_only_json_keys(json, ["some"]) |> to(be_true) 41 | end 42 | 43 | expect raiser |> to(raise_exception(ArgumentError)) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/lib/test_that_json/exclusion_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.ExclusionSpec do 2 | use ESpec 3 | 4 | alias TestThatJson.Configuration 5 | alias TestThatJson.Exclusion 6 | 7 | describe "exclude_keys" do 8 | subject do: exclusion_result 9 | 10 | let :exclusion_result, do: Exclusion.exclude_keys(value) 11 | 12 | let :map_value do 13 | %{ 14 | "id" => "1234", 15 | "inserted_at" => "5678", 16 | "user" => "Leeroy", 17 | } 18 | end 19 | 20 | context "when the value is a list" do 21 | subject do: Enum.at(exclusion_result, 0) 22 | 23 | let :value do 24 | [ 25 | Map.merge(map_value, %{ 26 | "email" => "leeroy@example.com", 27 | }) 28 | ] 29 | end 30 | 31 | it do: should have_key("user") 32 | it do: should_not have_key("id") 33 | it do: should_not have_key("inserted_at") 34 | end 35 | 36 | context "when the value is a map" do 37 | let :value, do: map_value 38 | 39 | it do: should have_key("user") 40 | it do: should_not have_key("id") 41 | it do: should_not have_key("inserted_at") 42 | end 43 | 44 | context "when the value is a non-list, non-map" do 45 | let :value, do: "value" 46 | 47 | it do: should eq(value) 48 | end 49 | end 50 | 51 | describe "excluded_keys" do 52 | subject do: Exclusion.excluded_keys() 53 | 54 | before do 55 | allow Configuration |> to(accept(:excluded_keys, fn -> [] end)) 56 | subject 57 | end 58 | 59 | it "calls the correct function" do 60 | expect Configuration |> to(accepted(:excluded_keys)) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :test_that_json, 7 | version: "0.6.0", 8 | elixir: "~> 1.4", 9 | build_embedded: Mix.env == :prod, 10 | start_permanent: Mix.env == :prod, 11 | deps: deps(), 12 | description: description(), 13 | package: package(), 14 | docs: [extras: ["README.md", "CHANGELOG.md", "LICENSE.md"]], 15 | preferred_cli_env: [ 16 | espec: :test, 17 | ], 18 | ] 19 | end 20 | 21 | # Configuration for the OTP application 22 | # 23 | # Type "mix help compile.app" for more information 24 | def application do 25 | [ 26 | applications: [ 27 | :logger, 28 | ], 29 | ] 30 | end 31 | 32 | defp package do 33 | [ 34 | maintainers: ["Joshua Rieken"], 35 | licenses: ["MIT"], 36 | links: %{"GitHub" => "https://github.com/facto/test_that_json"}, 37 | files: ~w(mix.exs README.md CHANGELOG.md LICENSE.md lib), 38 | ] 39 | end 40 | 41 | defp description do 42 | """ 43 | JSON-related helpers for your Elixir testing needs 44 | """ 45 | end 46 | 47 | # Dependencies can be Hex packages: 48 | # 49 | # {:mydep, "~> 0.3.0"} 50 | # 51 | # Or git/path repositories: 52 | # 53 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 54 | # 55 | # Type "mix help deps" for more examples and options 56 | defp deps do 57 | [ 58 | {:exjsx, "~> 4.0"}, 59 | {:ex_doc, "~> 0.18", only: :dev}, 60 | {:espec, "~> 1.0", only: :test}, 61 | {:inch_ex, only: :docs}, 62 | ] 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/lib/test_that_json/helpers/has_json_properties_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Helpers.HasJsonPropertiesSpec do 2 | use ESpec 3 | 4 | import TestThatJson.Helpers, only: [has_json_properties: 2] 5 | 6 | context "when the subject is a JSON object" do 7 | let :json do 8 | """ 9 | { 10 | "some": "property", 11 | "another": "property with value" 12 | } 13 | """ 14 | end 15 | 16 | context "when the argument is a map" do 17 | it do: expect has_json_properties(json, %{"some" => "property"}) |> to(be_true) 18 | it do: expect has_json_properties(json, %{"some" => "property", "another" => "property with value"}) |> to(be_true) 19 | it do: expect has_json_properties(json, %{"some_other" => "property"}) |> to(be_false) 20 | it do: expect has_json_properties(json, %{"some" => "property", "some_other" => "property"}) |> to(be_false) 21 | end 22 | 23 | context "when the argument is not a map" do 24 | let :json, do: "[{\"some\": \"object\"}]" 25 | 26 | it "raises an error" do 27 | raiser = fn -> 28 | expect json |> to(has_json_properties(json, ["some"])) 29 | end 30 | 31 | expect raiser |> to(raise_exception(ArgumentError)) 32 | end 33 | end 34 | end 35 | 36 | context "when the subject is not a JSON object" do 37 | let :json do 38 | """ 39 | { 40 | "some": "property", 41 | "another": "property with value" 42 | } 43 | """ 44 | end 45 | 46 | it "raises an error" do 47 | raiser = fn -> 48 | expect has_json_properties(json, ["some"]) |> to(be_true) 49 | end 50 | 51 | expect raiser |> to(raise_exception(ArgumentError)) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/lib/test_that_json/helpers/has_only_json_properties_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Helpers.HaveOnlyJsonPropertiesSpec do 2 | use ESpec 3 | 4 | import TestThatJson.Helpers, only: [has_only_json_properties: 2] 5 | 6 | context "when the subject is a JSON object" do 7 | let :json do 8 | """ 9 | { 10 | "some": "property", 11 | "another": "property with value" 12 | } 13 | """ 14 | end 15 | 16 | context "when the argument is a map" do 17 | it do: expect has_only_json_properties(json, %{"some" => "property", "another" => "property with value"}) |> to(be_true) 18 | it do: expect has_only_json_properties(json, %{"some" => "property"}) |> to(be_false) 19 | it do: expect has_only_json_properties(json, %{"some" => "property", "another" => "property with value", "one" => "more"}) |> to(be_false) 20 | it do: expect has_only_json_properties(json, %{"some_other" => "property"}) |> to(be_false) 21 | it do: expect has_only_json_properties(json, %{"some" => "property", "some_other" => "property"}) |> to(be_false) 22 | end 23 | 24 | context "when the argument is not a map" do 25 | let :json, do: "[{\"some\": \"object\"}]" 26 | 27 | it "raises an error" do 28 | raiser = fn -> 29 | expect has_only_json_properties(json, ["some"]) |> to(be_true) 30 | end 31 | 32 | expect raiser |> to(raise_exception(ArgumentError)) 33 | end 34 | end 35 | end 36 | 37 | context "when the subject is not a JSON object" do 38 | let :json do 39 | """ 40 | { 41 | "some": "property", 42 | "another": "property with value" 43 | } 44 | """ 45 | end 46 | 47 | it "raises an error" do 48 | raiser = fn -> 49 | expect has_only_json_properties(json, ["some"]) |> to(be_true) 50 | end 51 | 52 | expect raiser |> to(raise_exception(ArgumentError)) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/lib/test_that_json/helpers/is_json_equal_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Helpers.IsJsonEqualSpec do 2 | use ESpec 3 | 4 | import TestThatJson.Helpers, only: [is_json_equal: 2] 5 | 6 | context "when the subject is a JSON object" do 7 | let :json do 8 | """ 9 | { 10 | "some": "object", 11 | "another": "key with value", 12 | "key_with_object": { 13 | "nested": "object y'all" 14 | } 15 | } 16 | """ 17 | end 18 | 19 | let :slightly_different_json do 20 | """ 21 | { 22 | "another": "key with value", 23 | "some": "object", 24 | "key_with_object": { 25 | "nested": "object y'all" 26 | } 27 | } 28 | """ 29 | end 30 | 31 | let :different_json do 32 | """ 33 | { 34 | "some": "object", 35 | "key_with_object": { 36 | "nested": "object y'all" 37 | } 38 | } 39 | """ 40 | end 41 | 42 | it do: expect is_json_equal(json, json) |> to(be_true) 43 | it do: expect is_json_equal(json, %{ 44 | "some" => "object", 45 | "another" => "key with value", 46 | "key_with_object" => %{"nested" => "object y'all"}}) 47 | |> to(be_true) 48 | it do: expect is_json_equal(json, slightly_different_json) |> to(be_true) 49 | it do: expect is_json_equal(json, different_json) |> to(be_false) 50 | end 51 | 52 | context "when the subject is a JSON array" do 53 | let :json, do: "[1,2,3,4]" 54 | 55 | it do: expect is_json_equal(json, json) |> to(be_true) 56 | it do: expect is_json_equal(json, [1,2,3,4]) |> to(be_true) 57 | it do: expect is_json_equal(json, "[1,3,2,4]") |> to(be_false) 58 | it do: expect is_json_equal(json, "[1,2,3]") |> to(be_false) 59 | it do: expect is_json_equal(json, "{\"different\": \"json\"}") |> to(be_false) 60 | end 61 | 62 | context "when the subject is a JSON string" do 63 | let :json, do: "\"json string\"" 64 | 65 | it do: expect is_json_equal(json, json) |> to(be_true) 66 | it do: expect is_json_equal(json, "json string") |> to(be_true) 67 | it do: expect is_json_equal(json, "\"josn string\"") |> to(be_false) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/helpers_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.HelpersTest do 2 | use ExUnit.Case, async: true 3 | 4 | import TestThatJson.Helpers 5 | 6 | test "has_json_keys" do 7 | json = """ 8 | { 9 | "key1": "value", 10 | "key2": "data" 11 | } 12 | """ 13 | 14 | assert has_json_keys(json, ["key1", "key2"]) 15 | end 16 | 17 | test "has_only_json_keys" do 18 | json = """ 19 | { 20 | "key1": "value", 21 | "key2": "data" 22 | } 23 | """ 24 | 25 | assert has_only_json_keys(json, ["key1", "key2"]) 26 | end 27 | 28 | test "has_json_values" do 29 | json = """ 30 | { 31 | "key1": "value", 32 | "key2": "data" 33 | } 34 | """ 35 | 36 | assert has_json_values(json, ["value", "data"]) 37 | end 38 | 39 | test "has_only_json_values" do 40 | json = """ 41 | { 42 | "key1": "value", 43 | "key2": "data" 44 | } 45 | """ 46 | 47 | assert has_only_json_values(json, ["value", "data"]) 48 | end 49 | 50 | test "has_json_properties" do 51 | json = """ 52 | { 53 | "key1": "value", 54 | "key2": "data" 55 | } 56 | """ 57 | 58 | assert has_json_properties(json, %{"key1" => "value", "key2" => "data"}) 59 | end 60 | 61 | test "has_only_json_properties" do 62 | json = """ 63 | { 64 | "key1": "value", 65 | "key2": "data" 66 | } 67 | """ 68 | 69 | assert has_only_json_properties(json, %{"key1" => "value", "key2" => "data"}) 70 | end 71 | 72 | test "has_json_path" do 73 | json = """ 74 | { 75 | "key1": "value", 76 | "key2": "data" 77 | } 78 | """ 79 | 80 | assert has_json_path(json, "key1") 81 | end 82 | 83 | test "has_json_size" do 84 | json = """ 85 | { 86 | "key1": "value", 87 | "key2": "data" 88 | } 89 | """ 90 | 91 | assert has_json_size(json, 2) 92 | end 93 | 94 | test "has_json_type" do 95 | json = """ 96 | { 97 | "key1": "value", 98 | "key2": "data" 99 | } 100 | """ 101 | 102 | assert has_json_type(json, :object) 103 | end 104 | 105 | test "is_json_equal" do 106 | json = "[1,2,3]" 107 | 108 | assert is_json_equal(json, json) 109 | end 110 | end 111 | 112 | -------------------------------------------------------------------------------- /spec/lib/test_that_json/helpers/has_json_path_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Helpers.HasJsonPathSpec do 2 | use ESpec 3 | 4 | import TestThatJson.Helpers, only: [has_json_path: 2] 5 | 6 | context "when the subject is a JSON object" do 7 | context "when that object is shallow" do 8 | let :json do 9 | """ 10 | { 11 | "some": "object", 12 | "another": "key with value", 13 | "key_with_object": { 14 | "nested": "object y'all" 15 | } 16 | } 17 | """ 18 | end 19 | 20 | it do: expect has_json_path(json, "some") |> to(be_true) 21 | it do: expect has_json_path(json, "key_with_object") |> to(be_true) 22 | it do: expect has_json_path(json, "does_not_exist") |> to(be_false) 23 | end 24 | 25 | context "when that object has nesting" do 26 | let :json do 27 | """ 28 | { 29 | "nested": { 30 | "inner": { 31 | "another": { 32 | "array": [1,2,3], 33 | "innermost": "property" 34 | } 35 | } 36 | } 37 | } 38 | """ 39 | end 40 | 41 | it do: expect has_json_path(json, "nested/inner/another/array/2") |> to(be_true) 42 | it do: expect has_json_path(json, "nested/inner/another/innermost") |> to(be_true) 43 | it do: expect has_json_path(json, "nested/inner/another/array/8") |> to(be_false) 44 | it do: expect has_json_path(json, "nested/inner/another/inner") |> to(be_false) 45 | end 46 | end 47 | 48 | context "when the subject is a JSON array" do 49 | let :json do 50 | """ 51 | [ 52 | 1,2,3 53 | ] 54 | """ 55 | end 56 | 57 | it do: expect has_json_path(json, "1") |> to(be_true) 58 | it do: expect has_json_path(json, "-1") |> to(be_true) 59 | it do: expect has_json_path(json, "-4") |> to(be_false) 60 | it do: expect has_json_path(json, "3") |> to(be_false) 61 | it do: expect has_json_path(json, "6") |> to(be_false) 62 | end 63 | 64 | context "when the subject is not a JSON object or array" do 65 | let :json, do: "\"json string\"" 66 | 67 | it "raises an error" do 68 | raiser = fn -> 69 | expect has_json_path(json, "some/path") |> to(be_true) 70 | end 71 | 72 | expect raiser |> to(raise_exception(TestThatJson.InvalidPathError)) 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/lib/test_that_json/pathing_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.PathingSpec do 2 | use ESpec 3 | 4 | alias TestThatJson.Pathing 5 | 6 | describe "value_at_path" do 7 | subject do: Pathing.value_at_path(value, path) 8 | 9 | context "when the path is nil" do 10 | let :value, do: %{"some" => "map"} 11 | let :path, do: nil 12 | 13 | it do: should eq(value) 14 | end 15 | 16 | context "when the value is a map" do 17 | let :value do 18 | %{ 19 | "id" => "1234", 20 | "inserted_at" => "5678", 21 | "user" => "Leeroy", 22 | } 23 | end 24 | 25 | let :path, do: "id" 26 | 27 | it do: should eq("1234") 28 | 29 | context "when that map has a list as a value" do 30 | let :value do 31 | %{ 32 | "id" => ["1234"], 33 | "inserted_at" => "5678", 34 | "user" => "Leeroy", 35 | } 36 | end 37 | 38 | let :path, do: "id/0" 39 | 40 | it do: should eq("1234") 41 | 42 | context "when the key does not exist" do 43 | let :path, do: "does_not_exist" 44 | 45 | it "raises an error" do 46 | raiser = fn -> 47 | expect subject |> to(eq("")) 48 | end 49 | 50 | expect raiser |> to(raise_exception(TestThatJson.PathNotFoundError)) 51 | end 52 | end 53 | end 54 | end 55 | 56 | context "when the value is a list" do 57 | let :value do 58 | [ 59 | "5678", 60 | "9876", 61 | ] 62 | end 63 | 64 | context "when the index is valid" do 65 | let :path, do: "1" 66 | 67 | it do: should eq("9876") 68 | end 69 | 70 | context "when the index is not valid" do 71 | let :path, do: "10" 72 | 73 | it "raises an error" do 74 | raiser = fn -> 75 | expect subject |> to(eq("")) 76 | end 77 | 78 | expect raiser |> to(raise_exception(TestThatJson.PathNotFoundError)) 79 | end 80 | end 81 | end 82 | 83 | context "when the value is a non-list, non-map" do 84 | let :value do 85 | "hi" 86 | end 87 | 88 | let :path, do: "1" 89 | 90 | it "raises an error" do 91 | raiser = fn -> 92 | expect subject |> to(eq("")) 93 | end 94 | 95 | expect raiser |> to(raise_exception(TestThatJson.InvalidPathError)) 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/test_that_json/helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Helpers do 2 | alias TestThatJson.Generation 3 | alias TestThatJson.Json 4 | alias TestThatJson.Loading 5 | alias TestThatJson.Normalization 6 | alias TestThatJson.Parsing 7 | 8 | def has_json_keys(subject, value) do 9 | handle_result!(Json.has_keys?(subject, value)) 10 | end 11 | 12 | def has_only_json_keys(subject, value) do 13 | handle_result!(Json.has_only_keys?(subject, value)) 14 | end 15 | 16 | def has_json_values(subject, value) do 17 | handle_result!(Json.has_values?(subject, value)) 18 | end 19 | 20 | def has_only_json_values(subject, value) do 21 | handle_result!(Json.has_only_values?(subject, value)) 22 | end 23 | 24 | def has_json_properties(subject, value) do 25 | handle_result!(Json.has_properties?(subject, value)) 26 | end 27 | 28 | def has_only_json_properties(subject, value) do 29 | handle_result!(Json.has_only_properties?(subject, value)) 30 | end 31 | 32 | def has_json_path(subject, value) do 33 | handle_result!(Json.has_path?(subject, value)) 34 | end 35 | 36 | def has_json_size(subject, size) do 37 | handle_result!(Json.has_size?(subject, size)) 38 | end 39 | 40 | def has_json_type(subject, type) do 41 | handle_result!(Json.has_type?(subject, type)) 42 | end 43 | 44 | def is_json_equal(subject, value) do 45 | handle_result!(Json.equals?(subject, value)) 46 | end 47 | 48 | def load_json(path) do 49 | Loading.load(path) 50 | end 51 | 52 | def load_json!(path) do 53 | Loading.load!(path) 54 | end 55 | 56 | def parse_json(json) do 57 | Parsing.parse(json) 58 | end 59 | 60 | def parse_json!(json) do 61 | Parsing.parse!(json) 62 | end 63 | 64 | def prettify_json(json) do 65 | Normalization.prettify(json) 66 | end 67 | 68 | def prettify_json!(json) do 69 | Normalization.prettify!(json) 70 | end 71 | 72 | def to_json(value) do 73 | Generation.generate(value) 74 | end 75 | 76 | def to_json!(value) do 77 | Generation.generate!(value) 78 | end 79 | 80 | def to_prettified_json(value) do 81 | Generation.generate_prettified(value) 82 | end 83 | 84 | def to_prettified_json!(value) do 85 | Generation.generate_prettified!(value) 86 | end 87 | 88 | 89 | 90 | # PRIVATE ################################################## 91 | 92 | defp handle_result!(expr) do 93 | case expr do 94 | {:error, {module, _values, message}} -> raise module, message: message 95 | {:error, {module, values}} -> raise module, value: List.first(values) 96 | result -> result 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/lib/test_that_json/helpers/has_json_values_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Helpers.HaveJsonValuesSpec do 2 | use ESpec 3 | 4 | import TestThatJson.Helpers, only: [has_json_values: 2] 5 | 6 | context "when the subject is a JSON object" do 7 | let :json do 8 | """ 9 | { 10 | "some": "object", 11 | "another": "key with value", 12 | "key_with_object": { 13 | "nested": "object y'all" 14 | } 15 | } 16 | """ 17 | end 18 | 19 | context "when the value is a map" do 20 | it do: expect has_json_values(json, %{"nested" => "object y'all"}) |> to(be_true) 21 | it do: expect has_json_values(json, %{"missing" => "object"}) |> to(be_false) 22 | end 23 | 24 | context "when the value is a list" do 25 | it do: expect has_json_values(json, ["object", "key with value"]) |> to(be_true) 26 | it do: expect has_json_values(json, ["object", "key with value", "yet_another"]) |> to(be_false) 27 | end 28 | 29 | context "when the value is a string" do 30 | it do: expect has_json_values(json, "object") |> to(be_true) 31 | it do: expect has_json_values(json, "some") |> to(be_false) 32 | it do: expect has_json_values(json, "{\"nested\": \"object y'all\"}") |> to(be_true) 33 | end 34 | end 35 | 36 | context "when the subject is a JSON array" do 37 | let :json do 38 | """ 39 | [ 40 | { 41 | "some": "object", 42 | "another": "key with value" 43 | }, 44 | { 45 | "another": "object" 46 | }, 47 | ["a list with a string"], 48 | "some string" 49 | ] 50 | """ 51 | end 52 | 53 | context "when the value is a list" do 54 | it do: expect has_json_values(json, [["a list with a string"]]) |> to(be_true) 55 | end 56 | 57 | context "when the value is a map" do 58 | it do: expect has_json_values(json, %{"some" => "object", "another" => "key with value"}) |> to(be_true) 59 | it do: expect has_json_values(json, %{"some_other" => "object"}) |> to(be_false) 60 | end 61 | 62 | context "when the value is a string" do 63 | context "when the string is valid JSON" do 64 | it do: expect has_json_values(json, "[\"a list with a string\"]") |> to(be_true) 65 | it do: expect has_json_values(json, "\"a list with a string\"") |> to(be_false) 66 | end 67 | 68 | context "when the string is not JSON" do 69 | it do: expect has_json_values(json, "some string") |> to(be_true) 70 | it do: expect has_json_values(json, "some other string") |> to(be_false) 71 | end 72 | end 73 | end 74 | 75 | context "when the subject is a JSON string" do 76 | let :json, do: "\"string\"" 77 | 78 | context "when the value is a JSON string" do 79 | it do: expect has_json_values(json, "\"string\"") |> to(be_true) 80 | it do: expect has_json_values(json, "\"another string\"") |> to(be_false) 81 | end 82 | 83 | context "when the value is a string but not JSON" do 84 | it do: expect has_json_values(json, "string") |> to(be_true) 85 | it do: expect has_json_values(json, "another string") |> to(be_false) 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /spec/lib/test_that_json/helpers/has_json_type_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Helpers.HasJsonTypeSpec do 2 | use ESpec 3 | 4 | import TestThatJson.Helpers, only: [has_json_type: 2] 5 | 6 | context "when the subject is a JSON object" do 7 | let :json do 8 | """ 9 | { 10 | "some": "property", 11 | "another": "property with value" 12 | } 13 | """ 14 | end 15 | 16 | it do: expect has_json_type(json, :object) |> to(be_true) 17 | it do: expect has_json_type(json, :array) |> to(be_false) 18 | end 19 | 20 | context "when the subject is a JSON array" do 21 | let :json do 22 | """ 23 | [ 24 | { 25 | "some": "property", 26 | "another": "property with value" 27 | }, 28 | { 29 | "yet_another" :"property" 30 | } 31 | ] 32 | """ 33 | end 34 | 35 | it do: expect has_json_type(json, :array) |> to(be_true) 36 | it do: expect has_json_type(json, :object) |> to(be_false) 37 | end 38 | 39 | context "when the subject is a JSON string" do 40 | let :json, do: "\"string\"" 41 | 42 | it do: expect has_json_type(json, :string) |> to(be_true) 43 | it do: expect has_json_type(json, :array) |> to(be_false) 44 | it do: expect has_json_type(json, :object) |> to(be_false) 45 | end 46 | 47 | context "when the subject is a JSON integer" do 48 | let :json, do: "0" 49 | 50 | it do: expect has_json_type(json, :number) |> to(be_true) 51 | it do: expect has_json_type(json, :integer) |> to(be_true) 52 | it do: expect has_json_type(json, :float) |> to(be_false) 53 | it do: expect has_json_type(json, :string) |> to(be_false) 54 | end 55 | 56 | context "when the subject is a JSON float" do 57 | let :json, do: "1.1" 58 | 59 | it do: expect has_json_type(json, :number) |> to(be_true) 60 | it do: expect has_json_type(json, :float) |> to(be_true) 61 | it do: expect has_json_type(json, :integer) |> to(be_false) 62 | it do: expect has_json_type(json, :string) |> to(be_false) 63 | end 64 | 65 | context "when the subject is JSON null" do 66 | let :json, do: "null" 67 | 68 | it do: expect has_json_type(json, :null) |> to(be_true) 69 | it do: expect has_json_type(json, :string) |> to(be_false) 70 | end 71 | 72 | context "when the subject is JSON boolean" do 73 | let :json, do: "true" 74 | 75 | it do: expect has_json_type(json, :boolean) |> to(be_true) 76 | it do: expect has_json_type(json, :null) |> to(be_false) 77 | it do: expect has_json_type(json, :string) |> to(be_false) 78 | end 79 | 80 | context "when the type is not an atom" do 81 | let :json, do: "true" 82 | 83 | it "raises an error" do 84 | raiser = fn -> 85 | expect has_json_type(json, "some") |> to(be_true) 86 | end 87 | 88 | expect raiser |> to(raise_exception(ArgumentError)) 89 | end 90 | end 91 | 92 | context "when the type is not supported" do 93 | let :json, do: "true" 94 | 95 | it "raises an error" do 96 | raiser = fn -> 97 | expect has_json_type(json, :bloop) |> to(be_true) 98 | end 99 | 100 | expect raiser |> to(raise_exception(ArgumentError)) 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at joshua@joshuarieken.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /spec/lib/test_that_json/helpers_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.HelpersSpec do 2 | use ESpec 3 | 4 | alias TestThatJson.Helpers 5 | 6 | let :json, do: "[1,2,3,4]" 7 | 8 | describe "parse_json" do 9 | before do 10 | allow TestThatJson.Parsing |> to(accept(:parse, fn _ -> nil end)) 11 | Helpers.parse_json(json) 12 | end 13 | 14 | it "calls the correct function" do 15 | expect TestThatJson.Parsing |> to(accepted(:parse, [json])) 16 | end 17 | end 18 | 19 | describe "parse_json!" do 20 | before do 21 | allow TestThatJson.Parsing |> to(accept(:parse!, fn _ -> nil end)) 22 | Helpers.parse_json!(json) 23 | end 24 | 25 | it "calls the correct function" do 26 | expect TestThatJson.Parsing |> to(accepted(:parse!, [json])) 27 | end 28 | end 29 | 30 | describe "to_json" do 31 | before do 32 | allow TestThatJson.Generation |> to(accept(:generate, fn _ -> nil end)) 33 | Helpers.to_json(json) 34 | end 35 | 36 | it "calls the correct function" do 37 | expect TestThatJson.Generation |> to(accepted(:generate, [json])) 38 | end 39 | end 40 | 41 | describe "to_json!" do 42 | before do 43 | allow TestThatJson.Generation |> to(accept(:generate!, fn _ -> nil end)) 44 | Helpers.to_json!(json) 45 | end 46 | 47 | it "calls the correct function" do 48 | expect TestThatJson.Generation |> to(accepted(:generate!, [json])) 49 | end 50 | end 51 | 52 | describe "prettify_json" do 53 | before do 54 | allow TestThatJson.Normalization |> to(accept(:prettify, fn _ -> nil end)) 55 | Helpers.prettify_json(json) 56 | end 57 | 58 | it "calls the correct function" do 59 | expect TestThatJson.Normalization |> to(accepted(:prettify, [json])) 60 | end 61 | end 62 | 63 | describe "prettify_json!" do 64 | before do 65 | allow TestThatJson.Normalization |> to(accept(:prettify!, fn _ -> nil end)) 66 | Helpers.prettify_json!(json) 67 | end 68 | 69 | it "calls the correct function" do 70 | expect TestThatJson.Normalization |> to(accepted(:prettify!, [json])) 71 | end 72 | end 73 | 74 | describe "to_prettified_json" do 75 | before do 76 | allow TestThatJson.Generation |> to(accept(:generate_prettified, fn _ -> nil end)) 77 | Helpers.to_prettified_json(json) 78 | end 79 | 80 | it "calls the correct function" do 81 | expect TestThatJson.Generation |> to(accepted(:generate_prettified, [json])) 82 | end 83 | end 84 | 85 | describe "to_prettified_json!" do 86 | before do 87 | allow TestThatJson.Generation |> to(accept(:generate_prettified, fn _ -> nil end)) 88 | Helpers.to_prettified_json(json) 89 | end 90 | 91 | it "calls the correct function" do 92 | expect TestThatJson.Generation |> to(accepted(:generate_prettified, [json])) 93 | end 94 | end 95 | 96 | describe "load_json" do 97 | before do 98 | allow TestThatJson.Loading |> to(accept(:load, fn _ -> nil end)) 99 | Helpers.load_json(json) 100 | end 101 | 102 | it "calls the correct function" do 103 | expect TestThatJson.Loading |> to(accepted(:load, [json])) 104 | end 105 | end 106 | 107 | describe "load_json!" do 108 | before do 109 | allow TestThatJson.Loading |> to(accept(:load!, fn _ -> nil end)) 110 | Helpers.load_json!(json) 111 | end 112 | 113 | it "calls the correct function" do 114 | expect TestThatJson.Loading |> to(accepted(:load!, [json])) 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Test That JSON! 2 | 3 | Helpers for a better JSON testing experience in Elixir. 4 | 5 | [![Build Status](https://travis-ci.org/facto/test_that_json.svg?branch=master)](https://travis-ci.org/facto/test_that_json) 6 | [![Inline docs](http://inch-ci.org/github/facto/test_that_json.svg)](http://inch-ci.org/github/facto/test_that_json) 7 | 8 | Using [ESpec](https://github.com/antonmi/espec)? Check out [test_that_json_espec](https://github.com/facto/test_that_json_espec). 9 | 10 | 11 | ## Docs 12 | 13 | [All Docs](https://hexdocs.pm/test_that_json/api-reference.html) 14 | 15 | For now, see the [`Json`](https://hexdocs.pm/test_that_json/TestThatJson.Json.html) module for docs for much of the API. 16 | 17 | This project has an extensive test suite, so see that for detailed usage. 18 | 19 | 20 | ## Helpers 21 | 22 | - [X] `has_json_keys` 23 | - [X] `has_only_json_keys` 24 | - [X] `has_json_values` 25 | - [X] `has_only_json_values` 26 | - [X] `has_json_properties` 27 | - [X] `has_only_json_properties` 28 | - [X] `has_json_path` 29 | - [X] `has_json_size` 30 | - [X] `has_json_type` 31 | - [X] `is_json_equal` 32 | - [X] `load_json` 33 | - [X] `load_json!` 34 | - [X] `parse_json` 35 | - [X] `parse_json!` 36 | - [X] `prettify_json` 37 | - [X] `prettify_json!` 38 | - [X] `to_json` 39 | - [X] `to_json!` 40 | - [X] `to_prettified_json` 41 | - [X] `to_prettified_json!` 42 | 43 | 44 | ## Additional Functionality 45 | 46 | - [ ] Helpers that return a boolean can optionally take a path 47 | - [ ] Helpers can be composed together w/ the pipe |> operator 48 | 49 | 50 | ## Example 51 | 52 | ```elixir 53 | defmodule MyProject.ExampleTest 54 | use ExUnit.Case 55 | 56 | import TestThatJson.Helpers 57 | 58 | test "verifying JSON key presence" do 59 | json = load_json("test/support/json/valid.json") # example helper use 60 | 61 | assert has_json_keys(["key1", "key2"]) 62 | end 63 | end 64 | ``` 65 | 66 | Test That JSON! has extensive tests, but they're mostly written as [ESpec](https://github.com/antonmi/espec) specs because I like that style for complex testing. See the `test` directory for some basic happy-path tests, and the `spec` directory for detailed use cases. 67 | 68 | 69 | ## Installation 70 | 71 | 1. Add `test_that_json` as a test-only dependency in `mix.exs`: 72 | 73 | ```elixir 74 | def deps do 75 | [ 76 | {:test_that_json, "~> 0.6.0", only: :test}, 77 | ] 78 | end 79 | ``` 80 | 81 | 82 | ## Configuration 83 | 84 | ### Key Exclusion 85 | 86 | By default, to avoid needing to know the values of these ahead of time, the following keys are ignored for all JSON objects: `id`, `inserted_at`, and `updated_at`. 87 | 88 | This can be reconfigured as follows: 89 | 90 | ``` elixir 91 | config :test_that_json, 92 | excluded_keys: ~w(id inserted_at updated_at some other keys) 93 | ``` 94 | 95 | 96 | ## Paths 97 | 98 | These are simple strings of "/"-separated object keys and array indexes passed to `has_json_path`. For instance, with the following JSON: 99 | 100 | ``` json 101 | { 102 | "first_name": "Jon", 103 | "last_name": "Snow", 104 | "friends": [ 105 | { 106 | "first_name": "Know", 107 | "last_name": "Nothing" 108 | } 109 | ] 110 | } 111 | ``` 112 | 113 | We could access the first friend's first name with the path "friends/0/first_name". 114 | 115 | 116 | ## Project Chores 117 | 118 | - [X] Tests 119 | - [ ] Docs for entire helper API 120 | 121 | 122 | ## Related Projects 123 | 124 | - [DockYard/json_api_assert](https://github.com/DockYard/json_api_assert) 125 | - [jonasschmidt/ex_json_schema](https://github.com/jonasschmidt/ex_json_schema) 126 | 127 | 128 | ## Thanks 129 | 130 | Thanks to the creators and maintainers of the [Ruby json_spec](https://github.com/collectiveidea/json_spec) project for heavy inspiration. 131 | 132 | 133 | ## Contributing 134 | 135 | 1. Before opening a pull request, please open an issue first. 136 | 2. Do the usual fork/add/fix/run tests dance, or whatever tickles your fancy. Tests are highly encouraged. 137 | 3. Open a PR. 138 | 4. Treat yourself. You deserve it. 139 | 140 | 141 | ## License 142 | 143 | See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT). 144 | -------------------------------------------------------------------------------- /spec/lib/test_that_json/helpers/has_only_json_values_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Helpers.HaveOnlyJsonValuesSpec do 2 | use ESpec 3 | 4 | import TestThatJson.Helpers, only: [has_only_json_values: 2] 5 | 6 | context "when the subject is a JSON object" do 7 | context "when the value is a map" do 8 | let :json do 9 | """ 10 | { 11 | "key_with_object": { 12 | "nested": "object y'all" 13 | } 14 | } 15 | """ 16 | end 17 | 18 | it do: expect has_only_json_values(json, %{"nested" => "object y'all"}) |> to(be_true) 19 | it do: expect has_only_json_values(json, %{"missing" => "object"}) |> to(be_false) 20 | end 21 | 22 | context "when the value is a list" do 23 | let :json do 24 | """ 25 | { 26 | "some": "object", 27 | "another": "key with value", 28 | "key_with_object": { 29 | "nested": "object y'all" 30 | } 31 | } 32 | """ 33 | end 34 | 35 | context "when all provided values are present and exhaustive" do 36 | it do: expect has_only_json_values(json, ["object", "key with value", %{"nested" => "object y'all"}]) |> to(be_true) 37 | end 38 | 39 | context "when more than the provided values are present" do 40 | it do: expect has_only_json_values(json, ["object"]) |> to(be_false) 41 | end 42 | 43 | context "when fewer than the provided values are present" do 44 | it do: expect has_only_json_values(json, ["object", "key with value", "yet_another", %{"nested" => "object y'all"}]) |> to(be_false) 45 | end 46 | end 47 | 48 | context "when the value is a string" do 49 | context "when there is exactly one string value" do 50 | let :json do 51 | """ 52 | { 53 | "some": "object" 54 | } 55 | """ 56 | end 57 | 58 | it do: expect has_only_json_values(json, "object") |> to(be_true) 59 | end 60 | 61 | context "when there is more than one string value" do 62 | let :json do 63 | """ 64 | { 65 | "some": "property", 66 | "this is": "another property" 67 | } 68 | """ 69 | end 70 | 71 | it do: expect has_only_json_values(json, "property") |> to(be_false) 72 | it do: expect has_only_json_values(json, "another property") |> to(be_false) 73 | end 74 | end 75 | end 76 | 77 | context "when the subject is a JSON array" do 78 | context "when the value is a list" do 79 | let :json do 80 | """ 81 | [ 82 | { 83 | "some": "object", 84 | "another": "key with value" 85 | }, 86 | { 87 | "another": "object" 88 | }, 89 | ["a list with a string"], 90 | "some string" 91 | ] 92 | """ 93 | end 94 | 95 | it do: expect has_only_json_values(json, [ 96 | %{"some" => "object", "another" => "key with value"}, 97 | %{"another" => "object"}, 98 | ["a list with a string"], 99 | "some string", 100 | ]) |> to(be_true) 101 | 102 | it do: expect has_only_json_values(json, [ 103 | %{"another" => "object"}, 104 | %{"some" => "object", "another" => "key with value"}, 105 | ["a list with a string"], 106 | "some string", 107 | ]) |> to(be_false) 108 | 109 | it do: expect has_only_json_values(json, [ 110 | %{"another" => "object"}, 111 | ["a list with a string"], 112 | "some string", 113 | ]) |> to(be_false) 114 | 115 | it do: expect has_only_json_values(json, [ 116 | %{"some" => "object", "another" => "key with value"}, 117 | %{"another" => "object"}, 118 | ["a list with a string"], 119 | "some string", 120 | "another string", 121 | ]) |> to(be_false) 122 | end 123 | 124 | context "when the value is a map" do 125 | let :json do 126 | """ 127 | [ 128 | { 129 | "some": "object", 130 | "another": "key with value" 131 | } 132 | ] 133 | """ 134 | end 135 | 136 | it do: expect has_only_json_values(json, %{"some" => "object", "another" => "key with value"}) |> to(be_true) 137 | it do: expect has_only_json_values(json, %{"some_other" => "object"}) |> to(be_false) 138 | end 139 | 140 | context "when the value is a string" do 141 | context "when the string is valid JSON" do 142 | let :json do 143 | """ 144 | [ 145 | ["a list with a string"] 146 | ] 147 | """ 148 | end 149 | 150 | it do: expect has_only_json_values(json, "[\"a list with a string\"]") |> to(be_true) 151 | it do: expect has_only_json_values(json, "\"a list with a string\"") |> to(be_false) 152 | end 153 | 154 | context "when the string is not JSON" do 155 | let :json do 156 | """ 157 | [ 158 | "a list with a string" 159 | ] 160 | """ 161 | end 162 | 163 | it do: expect has_only_json_values(json, "a list with a string") |> to(be_true) 164 | it do: expect has_only_json_values(json, "some other string") |> to(be_false) 165 | end 166 | end 167 | end 168 | 169 | context "when the subject is a JSON string" do 170 | let :json, do: "\"string\"" 171 | 172 | context "when the value is a JSON string" do 173 | it do: expect has_only_json_values(json, "\"string\"") |> to(be_true) 174 | it do: expect has_only_json_values(json, "\"another string\"") |> to(be_false) 175 | end 176 | 177 | context "when the value is a string but not JSON" do 178 | it do: expect has_only_json_values(json, "string") |> to(be_true) 179 | it do: expect has_only_json_values(json, "another string") |> to(be_false) 180 | end 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /lib/test_that_json/json.ex: -------------------------------------------------------------------------------- 1 | defmodule TestThatJson.Json do 2 | alias TestThatJson.Exclusion 3 | alias TestThatJson.Parsing 4 | alias TestThatJson.Pathing 5 | 6 | @type json_object :: String.t | map 7 | @type json_array :: String.t | list 8 | @type json_string :: String.t 9 | @type json_number :: String.t | number 10 | @type json_value :: json_object | json_array | json_string | json_number 11 | 12 | @doc """ 13 | Receives a subject and a value and returns whether they are equal. 14 | """ 15 | @spec equals?(json_value, json_value) :: boolean 16 | def equals?(subject, value) do 17 | {processed_subject, processed_value} = process(subject, value) 18 | do_equals?(processed_subject, processed_value) 19 | end 20 | 21 | @doc """ 22 | Receives a subject and a value and returns whether the subject has all keys 23 | specified by the value. 24 | """ 25 | @spec has_keys?(json_object, json_array | json_string) :: boolean 26 | def has_keys?(subject, value) do 27 | {processed_subject, processed_value} = process(subject, value) 28 | do_has_keys?(processed_subject, processed_value) 29 | end 30 | 31 | @doc """ 32 | Receives a subject and a value and returns whether the subject contains 33 | only the keys specified by the value. 34 | """ 35 | @spec has_only_keys?(json_object, json_array | json_string) :: boolean 36 | def has_only_keys?(subject, value) do 37 | {processed_subject, processed_value} = process(subject, value) 38 | do_has_only_keys?(processed_subject, processed_value) 39 | end 40 | 41 | @doc """ 42 | Receives a subject and a value and returns whether the subject contains the 43 | values specified by the value. 44 | 45 | If the value is a list, returns whether all values in the list are in the subject. 46 | 47 | If the value is a JSON array as a `String.t`, it is not treated the same as 48 | a list. Instead, returns whether that list itself is one of the values in the subject. 49 | """ 50 | @spec has_values?(json_value, json_value) :: boolean 51 | def has_values?(subject, value) do 52 | {processed_subject, processed_value} = process_for_values(subject, value) 53 | do_has_values?(processed_subject, processed_value) 54 | end 55 | 56 | @doc """ 57 | Receives a subject and a value and returns whether the subject contains only 58 | the values specified by the value. 59 | """ 60 | @spec has_only_values?(json_value, json_value) :: boolean 61 | def has_only_values?(subject, value) do 62 | {processed_subject, processed_value} = process_for_values(subject, value) 63 | do_has_only_values?(processed_subject, processed_value) 64 | end 65 | 66 | @doc """ 67 | Receives a subject and a value and returns whether the subject contains the 68 | properties (or key value pairs) specified by the value. 69 | """ 70 | @spec has_properties?(json_object, json_object) :: boolean 71 | def has_properties?(subject, value) do 72 | {processed_subject, processed_value} = process(subject, value) 73 | do_has_properties?(processed_subject, processed_value) 74 | end 75 | 76 | @doc """ 77 | Receives a subject and a value and returns whether the subject contains only 78 | the properties (or key value pairs) specified by the value. 79 | """ 80 | @spec has_only_properties?(json_object, json_object) :: boolean 81 | def has_only_properties?(subject, value) do 82 | {processed_subject, processed_value} = process(subject, value) 83 | do_has_only_properties?(processed_subject, processed_value) 84 | end 85 | 86 | @doc """ 87 | Receives a subject and a path and returns whether the path exists within the subject. 88 | """ 89 | @spec has_path?(json_object | json_array, String.t) :: boolean 90 | def has_path?(subject, path) do 91 | processed_subject = process(subject) 92 | do_has_path?(processed_subject, path) 93 | end 94 | 95 | @doc """ 96 | Receives a subject and a JSON type as an atom and returns whether the subject is that type. 97 | 98 | Valid types: `:object`, `:array`, `:string`, `:number`, `:integer`, `:float`, `:boolean`, `:null`. 99 | """ 100 | @spec has_type?(json_value, atom) :: boolean 101 | def has_type?(subject, type) do 102 | processed_subject = process(subject) 103 | do_has_type?(processed_subject, type) 104 | end 105 | 106 | @doc """ 107 | Receives a subject and an integer and returns with the subject has a size equal to the integer. 108 | """ 109 | @spec has_size?(json_value, integer) :: boolean 110 | def has_size?(subject, size) do 111 | processed_subject = process(subject) 112 | do_has_size?(processed_subject, size) 113 | end 114 | 115 | 116 | 117 | 118 | # PRIVATE ################################################## 119 | 120 | defp do_equals?(subject, value) do 121 | subject == value 122 | end 123 | 124 | defp do_has_keys?(subject_map, list_value) when is_map(subject_map) and is_list(list_value) do 125 | keys = Map.keys(subject_map) 126 | Enum.all?(list_value, &Enum.member?(keys, &1)) 127 | end 128 | defp do_has_keys?(subject_map, string_value) when is_map(subject_map) and is_binary(string_value) do 129 | keys = Map.keys(subject_map) 130 | Enum.member?(keys, string_value) 131 | end 132 | defp do_has_keys?(subject_map, invalid_value) when is_map(subject_map) do 133 | {:error, {ArgumentError, [invalid_value], "Value must be a list or a String.t"}} 134 | end 135 | defp do_has_keys?(invalid_subject, value) when is_list(value) or is_binary(value) do 136 | {:error, {ArgumentError, [invalid_subject], "Subject must be a map"}} 137 | end 138 | defp do_has_keys?(invalid_subject, invalid_value) do 139 | {:error, {ArgumentError, [invalid_subject, invalid_value], "Subject must be a map and value must be a list or a String.t"}} 140 | end 141 | 142 | defp do_has_only_keys?(subject_map, list_value) when is_map(subject_map) and is_list(list_value) do 143 | keys = Map.keys(subject_map) 144 | sorted_keys = Enum.sort(keys) 145 | sorted_list_value = Enum.sort(list_value) 146 | sorted_keys == sorted_list_value 147 | end 148 | defp do_has_only_keys?(subject_map, value) when is_map(subject_map) do 149 | keys = Map.keys(subject_map) 150 | length(keys) == 1 && List.first(keys) == value 151 | end 152 | defp do_has_only_keys?(subject_map, invalid_value) when is_map(subject_map) do 153 | {:error, {ArgumentError, [invalid_value], "Value must be a list or a String.t"}} 154 | end 155 | defp do_has_only_keys?(invalid_subject, value) when is_list(value) or is_binary(value) do 156 | {:error, {ArgumentError, [invalid_subject], "Subject must be a map"}} 157 | end 158 | defp do_has_only_keys?(invalid_subject, invalid_value) do 159 | {:error, {ArgumentError, [invalid_subject, invalid_value], "Subject must be a map and value must be a list or a String.t"}} 160 | end 161 | 162 | defp do_has_values?(map_subject, list_value) when is_map(map_subject) and is_list(list_value) do 163 | values = Map.values(map_subject) 164 | Enum.all?(list_value, &Enum.member?(values, &1)) 165 | end 166 | defp do_has_values?(map_subject, value) when is_map(map_subject) do 167 | values = Map.values(map_subject) 168 | Enum.member?(values, value) 169 | end 170 | defp do_has_values?(list_subject, list_value) when is_list(list_subject) and is_list(list_value) and list_subject == list_value, do: true 171 | defp do_has_values?(list_subject, list_value) when is_list(list_subject) and is_list(list_value) do 172 | Enum.all?(list_value, &Enum.member?(list_subject, &1)) 173 | end 174 | defp do_has_values?(list_subject, value) when is_list(list_subject) do 175 | Enum.member?(list_subject, value) 176 | end 177 | defp do_has_values?(subject, value) do 178 | subject == value 179 | end 180 | 181 | defp do_has_only_values?(map_subject, list_value) when is_map(map_subject) and is_list(list_value) do 182 | values = Map.values(map_subject) 183 | sorted_values = Enum.sort(values) 184 | sorted_list_value = Enum.sort(list_value) 185 | sorted_values == sorted_list_value 186 | end 187 | defp do_has_only_values?(map_subject, value) when is_map(map_subject) do 188 | subject_values = Map.values(map_subject) 189 | length(subject_values) == 1 && List.first(subject_values) == value 190 | end 191 | defp do_has_only_values?(list_subject, list_value) when is_list(list_subject) and is_list(list_value) do 192 | list_subject == list_value 193 | end 194 | defp do_has_only_values?(list_subject, value) when is_list(list_subject) do 195 | length(list_subject) == 1 && List.first(list_subject) == value 196 | end 197 | defp do_has_only_values?(subject, value) do 198 | subject == value 199 | end 200 | 201 | defp do_has_properties?(subject_map, other_map) when is_map(subject_map) and is_map(other_map) do 202 | Enum.all?(Map.keys(other_map), fn(key) -> Map.get(subject_map, key) == Map.get(other_map, key) end) 203 | end 204 | defp do_has_properties?(invalid_subject, invalid_value) do 205 | {:error, {ArgumentError, [invalid_subject, invalid_value], "Arguments must be maps"}} 206 | end 207 | 208 | defp do_has_only_properties?(subject_map, other_map) when is_map(subject_map) and is_map(other_map) do 209 | map_keys = Map.keys(subject_map) 210 | other_map_keys = Map.keys(other_map) 211 | 212 | case Enum.sort(map_keys) == Enum.sort(other_map_keys) do 213 | true -> do_has_properties?(subject_map, other_map) 214 | false -> false 215 | end 216 | end 217 | defp do_has_only_properties?(invalid_subject, invalid_value) do 218 | {:error, {ArgumentError, [invalid_subject, invalid_value], "Arguments must be maps"}} 219 | end 220 | 221 | defp do_has_path?(subject, path) when is_binary(path) do 222 | Pathing.value_at_path(subject, path) 223 | true 224 | rescue 225 | TestThatJson.PathNotFoundError -> false 226 | end 227 | 228 | defp do_has_type?(subject, :object), do: is_map(subject) 229 | defp do_has_type?(subject, :array), do: is_list(subject) 230 | defp do_has_type?(subject, :string), do: is_binary(subject) 231 | defp do_has_type?(subject, :number), do: is_number(subject) 232 | defp do_has_type?(subject, :integer), do: is_integer(subject) 233 | defp do_has_type?(subject, :float), do: is_float(subject) 234 | defp do_has_type?(subject, :boolean), do: is_boolean(subject) 235 | defp do_has_type?(subject, :null), do: is_nil(subject) 236 | defp do_has_type?(_subject, unsupported_type) when is_atom(unsupported_type) do 237 | {:error, {ArgumentError, [unsupported_type], "Unsupported type"}} 238 | end 239 | defp do_has_type?(_subject, invalid_type) do 240 | {:error, {ArgumentError, [invalid_type], "Type must be an atom"}} 241 | end 242 | 243 | defp do_has_size?(subject, size) when is_map(subject) or is_list(subject) and is_integer(size) do 244 | Enum.count(subject) == size 245 | end 246 | defp do_has_size?(subject, invalid_size) when is_map(subject) or is_list(subject) do 247 | {:error, {ArgumentError, [invalid_size], "Size must be an integer"}} 248 | end 249 | defp do_has_size?(invalid_subject, _size) do 250 | {:error, {ArgumentError, [invalid_subject], "Subject must be a map or a list"}} 251 | end 252 | 253 | 254 | 255 | 256 | 257 | defp process(subject) do 258 | parsed_subject = parse(subject) 259 | scrub(parsed_subject) 260 | end 261 | defp process(subject, value) do 262 | processed_subject = process(subject) 263 | processed_value = process(value) 264 | {processed_subject, processed_value} 265 | end 266 | 267 | defp process_for_values(subject, value) do 268 | parsed_subject = parse(subject) 269 | parsed_value = parse_for_values(value) 270 | 271 | scrubbed_subject = scrub(parsed_subject) 272 | scrubbed_value = scrub(parsed_value) 273 | 274 | {scrubbed_subject, scrubbed_value} 275 | end 276 | 277 | defp parse(value) do 278 | case Parsing.parse(value) do 279 | {:ok, parsed_value} -> parsed_value 280 | {:error, _} -> value 281 | end 282 | end 283 | 284 | defp parse_for_values(value) when is_binary(value) do 285 | case Parsing.parse(value) do 286 | {:ok, parsed_value} -> 287 | case parsed_value do 288 | parsed_value when is_list(parsed_value) -> [parsed_value] 289 | parsed_value -> parsed_value 290 | end 291 | {:error, _} -> value 292 | end 293 | end 294 | defp parse_for_values(value), do: parse(value) 295 | 296 | defp scrub(value), do: Exclusion.exclude_keys(value) 297 | end 298 | --------------------------------------------------------------------------------