1defmodule Hex.Version do 2 @moduledoc false 3 4 defmodule Requirement do 5 @moduledoc false 6 defstruct [:source, :req] 7 end 8 9 defmodule InvalidRequirementError do 10 @moduledoc false 11 defexception [:requirement] 12 13 def exception(requirement) when is_binary(requirement) do 14 %__MODULE__{requirement: requirement} 15 end 16 17 def message(%{requirement: requirement}) do 18 "invalid requirement: #{inspect(requirement)}" 19 end 20 end 21 22 defmodule InvalidVersionError do 23 @moduledoc false 24 defexception [:version] 25 26 def exception(version) when is_binary(version) do 27 %__MODULE__{version: version} 28 end 29 30 def message(%{version: version}) do 31 "invalid version: #{inspect(version)}" 32 end 33 end 34 35 @ets :hex_version 36 37 def start do 38 :ets.new(@ets, [:named_table, :public]) 39 {:ok, []} 40 end 41 42 def stable?(%Version{pre: []}), do: true 43 def stable?(%Version{}), do: false 44 def stable?(other), do: stable?(parse!(other)) 45 46 def match?(version, requirement, opts \\ []) do 47 allow_pre = Keyword.get(opts, :allow_pre, false) 48 req_source = requirement_source(requirement) 49 50 cache({:match?, version, req_source, allow_pre}, fn -> 51 version = parse!(version) 52 requirement = parse_requirement!(req_source, allow_pre: allow_pre) 53 54 cond do 55 allow_pre_available?() -> 56 Version.match?(version, requirement, allow_pre: allow_pre) 57 58 allow_pre -> 59 Version.match?(version, requirement) 60 61 true -> 62 custom_match?(version, requirement) 63 end 64 end) 65 end 66 67 def compare(version1, version2) do 68 cache({:compare, version1, version2}, fn -> 69 version1 = parse!(version1) 70 version2 = parse!(version2) 71 Version.compare(version1, version2) 72 end) 73 end 74 75 def parse(%Version{} = version) do 76 {:ok, version} 77 end 78 79 def parse(version) do 80 cache({:version, version}, fn -> 81 Version.parse(version) 82 end) 83 end 84 85 def parse!(version) do 86 case parse(version) do 87 {:ok, version} -> 88 version 89 90 :error -> 91 raise InvalidVersionError, version 92 end 93 end 94 95 def parse_requirement(req, opts \\ []) 96 97 def parse_requirement(%Requirement{} = req, _opts) do 98 {:ok, req} 99 end 100 101 def parse_requirement(%Version.Requirement{} = req, _opts) do 102 {:ok, req} 103 end 104 105 def parse_requirement(requirement, opts) do 106 allow_pre = Keyword.get(opts, :allow_pre, false) 107 108 cache({:req, requirement, allow_pre}, fn -> 109 if allow_pre or allow_pre_available?() do 110 case Version.parse_requirement(requirement) do 111 {:ok, req} -> {:ok, compile_requirement(req)} 112 :error -> :error 113 end 114 else 115 custom_requirement(requirement) 116 end 117 end) 118 end 119 120 defp compile_requirement(req) do 121 if allow_pre_available?() do 122 Version.compile_requirement(req) 123 else 124 req 125 end 126 end 127 128 def parse_requirement!(requirement, opts \\ []) do 129 case parse_requirement(requirement, opts) do 130 {:ok, requirement} -> 131 requirement 132 133 :error -> 134 raise InvalidRequirementError, requirement 135 end 136 end 137 138 defp cache(key, fun) do 139 case :ets.lookup(@ets, key) do 140 [{_, value}] -> 141 value 142 143 [] -> 144 value = fun.() 145 :ets.insert(@ets, {key, value}) 146 value 147 end 148 end 149 150 defp requirement_source(%Requirement{source: source}), do: source 151 defp requirement_source(%Version.Requirement{source: source}), do: source 152 defp requirement_source(source), do: source 153 154 defp custom_match?(version, %Requirement{req: req}) do 155 custom_match?(version, req) 156 end 157 158 defp custom_match?(version, {"and", x, y}) do 159 custom_match?(version, x) and custom_match?(version, y) 160 end 161 162 defp custom_match?(version, {"or", x, y}) do 163 custom_match?(version, x) or custom_match?(version, y) 164 end 165 166 defp custom_match?(version, {%Version.Requirement{} = req, true}) do 167 Version.match?(version, req) 168 end 169 170 defp custom_match?(%Version{pre: []} = version, {%Version.Requirement{} = req, false}) do 171 Version.match?(version, req) 172 end 173 174 defp custom_match?(_version, _req) do 175 false 176 end 177 178 defp custom_requirement(requirement) do 179 try do 180 req = 181 requirement 182 |> String.split(" ", trim: true) 183 |> split_ops() 184 |> custom_parse() 185 186 {:ok, %Requirement{source: requirement, req: req}} 187 catch 188 :error -> 189 :error 190 end 191 end 192 193 @version_ops ~w(~> == != <= >= < >) 194 @bool_ops ~w(and or) 195 196 defp custom_parse([op, version]) when op in @version_ops do 197 pre? = String.contains?(version, "-") 198 199 case Version.parse_requirement(op <> " " <> version) do 200 {:ok, req} -> 201 {req, pre?} 202 203 :error -> 204 throw(:error) 205 end 206 end 207 208 defp custom_parse([op1, version, op2 | rest]) when op2 in @bool_ops do 209 {op2, custom_parse([op1, version]), custom_parse(rest)} 210 end 211 212 defp custom_parse([version]) do 213 custom_parse(["==", version]) 214 end 215 216 defp custom_parse(_) do 217 throw(:error) 218 end 219 220 def split_ops([op | rest]) when op in @version_ops do 221 [op | split_ops(rest)] 222 end 223 224 def split_ops([<<op::binary-2, version::binary>> | rest]) when op in @version_ops do 225 [op, version | split_ops(rest)] 226 end 227 228 def split_ops([<<op::binary-1, version::binary>> | rest]) when op in @version_ops do 229 [op, version | split_ops(rest)] 230 end 231 232 def split_ops([version | rest]) do 233 [version | split_ops(rest)] 234 end 235 236 def split_ops([]) do 237 [] 238 end 239 240 defp allow_pre_available? do 241 Code.ensure_loaded?(Version) and function_exported?(Version, :match?, 3) 242 end 243 244 def major_version_change?(%Version{} = version1, %Version{} = version2) do 245 version1.major != version2.major 246 end 247 248 def breaking_minor_version_change?(%Version{} = version1, %Version{} = version2) do 249 version1.major == 0 and version2.major == 0 and version1.minor != version2.minor 250 end 251end 252