# Drop-in Elixir client library for the Friendship Tracker HTTP API. # # Save this file under your project as `lib/friendship_client.ex` and use # the FriendshipClient module: # # client = FriendshipClient.new("pat_...") # {:ok, rows} = FriendshipClient.account_list(client, sort: "-created_at") # {:ok, fresh} = FriendshipClient.account_create(client, %{"name" => "Example GmbH"}) # # Every endpoint exposed by the HTTP API is wrapped as a typed # `_/N` function on FriendshipClient. List functions take an # optional keyword list; get/update/delete take the row id as the # second argument (after the client). # # Provided as-is, with no warranty. Vendor freely; modify as needed. # Targets Elixir 1.18+ + OTP 26+; uses only stdlib (`:httpc`, `:inets`, # `:ssl`, `:rand`, `JSON`). # # DO NOT EDIT THIS FILE MANUALLY - re-download from the docs site. # Local edits will be overwritten by the once-per-day version check. defmodule FriendshipClient do @moduledoc """ Drop-in HTTP client for the Friendship Tracker API. """ import Bitwise @app_slug "friendship" @app_name "Friendship Tracker" @module_name "friendship_client" @client_version "0.3.13" @language "elixir" @default_base "https://friendship-tracker.com" # Per-type metadata baked at generation time. Decoded lazily by # `types/0`; useful at runtime when calling code needs to know the # legal filters / sort columns / max_limit for a model without a # second round-trip. @types_json ~S""" {"activity":{"ops":["list","read","create","update","delete"],"create_fields":["parent_id","kind","summary","description","occurred_at","location"],"update_fields":["kind","summary","description","occurred_at","location"],"allowed_filters":["data__parent_id","data__kind","status","is_archived","owned_by"],"allowed_sorts":["created_at","data__occurred_at"],"default_sort":"data__occurred_at","max_limit":200,"fields":[{"name":"kind","type":"enum","values":["meeting","call","email","message","event","other"]},{"name":"summary","type":"string","max_len":400},{"name":"location","type":"string","max_len":200},{"name":"parent_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}},{"name":"description","type":"string","max_len":4000},{"name":"occurred_at","type":"string","max_len":32}]},"contact":{"ops":["list","read","create","update","delete"],"create_fields":["name","nickname","pronouns","email","phone","secondary_email","secondary_phone","company","job_title","address_line","city","country","website","linkedin","twitter","birthday","anniversary","gender","how_we_met","food_prefs","allergies","last_contacted_at","stay_in_touch_frequency","stay_in_touch_topic","notes","tags","favorite","avatar_blob_id","color"],"update_fields":["name","nickname","pronouns","email","phone","secondary_email","secondary_phone","company","job_title","address_line","city","country","website","linkedin","twitter","birthday","anniversary","gender","how_we_met","food_prefs","allergies","last_contacted_at","stay_in_touch_frequency","stay_in_touch_topic","notes","tags","favorite","avatar_blob_id","color"],"allowed_filters":["data__name","data__email","data__company","data__city","data__country","data__favorite","data__tags","data__gender","data__stay_in_touch_frequency","status","is_archived","owned_by"],"allowed_sorts":["created_at","updated_at","data__name","data__company","data__last_contacted_at","data__birthday","data__stay_in_touch_frequency"],"default_sort":"data__name","max_limit":200,"fields":[{"name":"city","type":"string","max_len":120},{"name":"name","type":"string","max_len":200},{"name":"tags","type":"tags"},{"name":"color","type":"string","max_len":24},{"name":"email","type":"string","max_len":320},{"name":"notes","type":"string","max_len":8000},{"name":"phone","type":"string","max_len":64},{"name":"gender","type":"enum","values":["male","female","other","unspecified"]},{"name":"company","type":"string","max_len":200},{"name":"country","type":"string","max_len":120},{"name":"twitter","type":"url"},{"name":"website","type":"url"},{"name":"birthday","type":"string","max_len":32},{"name":"favorite","type":"bool"},{"name":"linkedin","type":"url"},{"name":"nickname","type":"string","max_len":120},{"name":"pronouns","type":"string","max_len":32},{"name":"allergies","type":"string","max_len":600},{"name":"job_title","type":"string","max_len":200},{"name":"food_prefs","type":"string","max_len":600},{"name":"how_we_met","type":"string","max_len":1000},{"name":"anniversary","type":"string","max_len":32},{"name":"address_line","type":"string","max_len":200},{"name":"avatar_blob_id","type":"string","max_len":64},{"name":"secondary_email","type":"string","max_len":320},{"name":"secondary_phone","type":"string","max_len":64},{"name":"last_contacted_at","type":"string","max_len":32},{"name":"stay_in_touch_topic","type":"string","max_len":400},{"name":"stay_in_touch_frequency","type":"enum","values":["never","weekly","biweekly","monthly","quarterly","yearly"]}]},"conversation":{"ops":["list","read","create","update","delete"],"create_fields":["parent_id","channel","summary","content","occurred_at","sentiment","duration_minutes"],"update_fields":["channel","summary","content","occurred_at","sentiment","duration_minutes"],"allowed_filters":["data__parent_id","data__channel","data__sentiment","status","is_archived","owned_by"],"allowed_sorts":["created_at","data__occurred_at"],"default_sort":"data__occurred_at","max_limit":200,"fields":[{"name":"channel","type":"enum","values":["call","sms","whatsapp","email","in_person","video","voice","letter","other"]},{"name":"content","type":"string","max_len":8000},{"name":"summary","type":"string","max_len":400},{"name":"parent_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}},{"name":"sentiment","type":"enum","values":["positive","neutral","negative"]},{"name":"occurred_at","type":"string","max_len":32},{"name":"duration_minutes","type":"number"}]},"custom_field":{"ops":["list","read","create","update","delete"],"create_fields":["parent_id","label","value","kind","icon"],"update_fields":["label","value","kind","icon"],"allowed_filters":["data__parent_id","data__kind","status","is_archived","owned_by"],"allowed_sorts":["created_at","data__label"],"default_sort":"created_at","max_limit":200,"fields":[{"name":"icon","type":"string","max_len":32},{"name":"kind","type":"enum","values":["text","number","date","url","bool"]},{"name":"label","type":"string","max_len":80},{"name":"value","type":"string","max_len":2000},{"name":"parent_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}}]},"gift":{"ops":["list","read","create","update","delete"],"create_fields":["parent_id","title","occasion","status","occurred_at","price","currency","url","notes"],"update_fields":["title","occasion","status","occurred_at","price","currency","url","notes"],"allowed_filters":["data__parent_id","data__status","data__occasion","status","is_archived","owned_by"],"allowed_sorts":["created_at","data__occurred_at","data__title"],"default_sort":"data__occurred_at","max_limit":200,"fields":[{"name":"url","type":"url"},{"name":"notes","type":"string","max_len":4000},{"name":"price","type":"number"},{"name":"title","type":"string","max_len":200},{"name":"status","type":"enum","values":["idea","given","received"]},{"name":"currency","type":"string","max_len":8},{"name":"occasion","type":"string","max_len":120},{"name":"parent_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}},{"name":"occurred_at","type":"string","max_len":32}]},"journal_entry":{"ops":["list","read","create","update","delete"],"create_fields":["title","body","mood","occurred_at","tags"],"update_fields":["title","body","mood","occurred_at","tags"],"allowed_filters":["data__mood","data__tags","status","is_archived","owned_by"],"allowed_sorts":["data__occurred_at","created_at","updated_at"],"default_sort":"data__occurred_at","max_limit":200,"fields":[{"name":"body","type":"string","max_len":16000},{"name":"mood","type":"enum","values":["great","good","ok","down","awful"]},{"name":"tags","type":"tags"},{"name":"title","type":"string","max_len":200},{"name":"occurred_at","type":"string","max_len":32}]},"life_event":{"ops":["list","read","create","update","delete"],"create_fields":["parent_id","kind","title","occurred_at","description","location","recurring"],"update_fields":["kind","title","occurred_at","description","location","recurring"],"allowed_filters":["data__parent_id","data__kind","data__recurring","status","is_archived","owned_by"],"allowed_sorts":["data__occurred_at","created_at"],"default_sort":"data__occurred_at","max_limit":200,"fields":[{"name":"kind","type":"enum","values":["birthday","anniversary","met","graduation","job_start","job_end","move","marriage","birth","loss","milestone","custom"]},{"name":"title","type":"string","max_len":200},{"name":"location","type":"string","max_len":200},{"name":"parent_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}},{"name":"recurring","type":"bool"},{"name":"description","type":"string","max_len":4000},{"name":"occurred_at","type":"string","max_len":32}]},"note":{"ops":["list","read","create","update","delete"],"create_fields":["parent_id","body","pinned"],"update_fields":["body","pinned"],"allowed_filters":["data__parent_id","data__pinned","status","is_archived","owned_by"],"allowed_sorts":["created_at","updated_at"],"default_sort":"created_at","max_limit":200,"fields":[{"name":"body","type":"string","max_len":8000},{"name":"pinned","type":"bool"},{"name":"parent_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}}]},"pet":{"ops":["list","read","create","update","delete"],"create_fields":["parent_id","name","species","species_other","breed","born_at","color","notes"],"update_fields":["name","species","species_other","breed","born_at","color","notes"],"allowed_filters":["data__parent_id","data__species","status","is_archived","owned_by"],"allowed_sorts":["created_at","data__name","data__born_at"],"default_sort":"data__name","max_limit":200,"fields":[{"name":"name","type":"string","max_len":80},{"name":"breed","type":"string","max_len":120},{"name":"color","type":"string","max_len":80},{"name":"notes","type":"string","max_len":2000},{"name":"born_at","type":"string","max_len":32},{"name":"species","type":"enum","values":["dog","cat","bird","fish","rabbit","hamster","guinea_pig","reptile","horse","other"]},{"name":"parent_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}},{"name":"species_other","type":"string","max_len":80}]},"relationship":{"ops":["list","read","create","update","delete"],"create_fields":["parent_id","target_id","kind","label","since","notes"],"update_fields":["kind","label","since","notes"],"allowed_filters":["data__parent_id","data__target_id","data__kind","status","is_archived","owned_by"],"allowed_sorts":["created_at","data__kind"],"default_sort":"data__kind","max_limit":200,"fields":[{"name":"kind","type":"enum","values":["partner","spouse","parent","child","sibling","friend","colleague","manager","report","mentor","mentee","other"]},{"name":"label","type":"string","max_len":80},{"name":"notes","type":"string","max_len":2000},{"name":"since","type":"string","max_len":32},{"name":"parent_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}},{"name":"target_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}}]},"reminder":{"ops":["list","read","create","update","delete"],"create_fields":["parent_id","message","due_date","completed"],"update_fields":["message","due_date","completed"],"allowed_filters":["data__parent_id","data__completed","status","is_archived","owned_by"],"allowed_sorts":["created_at","data__due_date"],"default_sort":"data__due_date","max_limit":200,"fields":[{"name":"message","type":"string","max_len":400},{"name":"due_date","type":"string","max_len":32},{"name":"completed","type":"bool"},{"name":"parent_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}}]}} """ @retryable_statuses [408, 425, 429, 500, 502, 503, 504] @max_retries 3 @default_timeout 30_000 defmodule ApiError do defexception [:status, :message, :body] @impl true def message(%__MODULE__{status: s, message: m}), do: "HTTP #{s}: #{m}" end defstruct [ :base_url, :token, :device_id, :session_id ] @type t :: %__MODULE__{ base_url: String.t(), token: String.t(), device_id: String.t(), session_id: String.t() } @doc "Decode the per-type metadata baked at generation time." def types do JSON.decode!(String.trim(@types_json)) end @doc "Tenant slug this library was generated against." def app_slug, do: @app_slug @doc "Human-readable app name this library was generated against." def app_name, do: @app_name @doc """ Build a new client. Pass a personal access token; an empty string falls back to the `XCLIENT_TOKEN` environment variable. """ @spec new(String.t() | nil) :: t() def new(token \\ nil) do {:ok, _} = :application.ensure_all_started(:inets) {:ok, _} = :application.ensure_all_started(:ssl) base = (System.get_env("XCLIENT_BASE_URL") || @default_base) |> trim_right_slash() tok = cond do is_binary(token) and token != "" -> token true -> System.get_env("XCLIENT_TOKEN") || "" end %__MODULE__{ base_url: base, token: tok, device_id: load_or_mint_device_id(), session_id: mint_uuid() } end @doc "Override the bearer token on an existing client." def set_token(%__MODULE__{} = c, token), do: %{c | token: token || ""} @doc "Override the base URL on an existing client." def set_base_url(%__MODULE__{} = c, url), do: %{c | base_url: trim_right_slash(url || "")} # ── Identifier persistence ─────────────────────────────────────────── defp state_dir do home = System.get_env("HOME") || System.get_env("USERPROFILE") cond do is_nil(home) or home == "" -> nil true -> d = Path.join(home, "." <> @module_name) case File.mkdir_p(d) do :ok -> d _ -> nil end end end defp mint_uuid do <> = :crypto.strong_rand_bytes(16) c2 = (c &&& 0x0FFF) ||| 0x4000 d2 = (d &&& 0x3FFF) ||| 0x8000 :io_lib.format("~8.16.0b-~4.16.0b-~4.16.0b-~4.16.0b-~12.16.0b", [a, b, c2, d2, e]) |> IO.iodata_to_binary() end defp load_or_mint_device_id do case state_dir() do nil -> mint_uuid() d -> f = Path.join(d, "device.json") case File.read(f) do {:ok, raw} -> case safe_decode(raw) do %{"device_id" => did} when is_binary(did) and byte_size(did) >= 32 -> did _ -> persist_fresh_device_id(f) end _ -> persist_fresh_device_id(f) end end end defp persist_fresh_device_id(f) do fresh = mint_uuid() _ = File.write(f, JSON.encode!(%{"device_id" => fresh})) fresh end defp safe_decode(raw) do try do JSON.decode!(raw) rescue _ -> nil end end defp autoupdate_enabled? do String.downcase(System.get_env("XCLIENT_NO_AUTOUPDATE") || "") not in ["1", "true", "yes"] end # ── Editor / runtime fingerprint ───────────────────────────────────── defp fingerprint do env = System.get_env() tp = String.downcase(env["TERM_PROGRAM"] || "") %{ "elixir_version" => System.version(), "otp_version" => :erlang.system_info(:otp_release) |> List.to_string(), "os" => to_string(:erlang.system_info(:system_architecture)), "term_program" => env["TERM_PROGRAM"], "editor_env" => env["EDITOR"], "ci" => not is_nil(env["CI"]) or not is_nil(env["GITHUB_ACTIONS"]), "claude_code" => not is_nil(env["CLAUDECODE"]) or not is_nil(env["CLAUDE_CODE_ENTRYPOINT"]), "codex" => not is_nil(env["CODEX_HOME"]), "vscode" => tp == "vscode" and is_nil(env["CURSOR_TRACE_ID"]), "cursor" => not is_nil(env["CURSOR_TRACE_ID"]), "antigravity" => not is_nil(env["ANTIGRAVITY_TRACE_ID"]), "jetbrains" => String.contains?(tp, "jetbrains") } end # ── HTTP transport ─────────────────────────────────────────────────── defp user_agent do "#{@module_name}/#{@client_version} (lib/#{@language}; elixir/#{System.version()})" end defp backoff_seconds(attempt, retry_after) do cond do is_number(retry_after) and retry_after >= 0 -> min(retry_after, 60.0) true -> min(:math.pow(2, attempt), 60.0) end end @doc """ Generic transport. Per-type wrappers forward through here. JSON in / JSON out; pass `nil` body for read-only verbs. Retries on 408/425/429/5xx + transport errors with exponential backoff. """ def request_json(%__MODULE__{} = c, method, path, body) do maybe_autoupdate(c) do_request(c, method, path, body, 0) end @doc "List wrapper. Adds the keyword opts as query string." def request_list(%__MODULE__{} = c, path, opts) when is_list(opts) or is_map(opts) do qs = build_qs(opts) sep = if String.contains?(path, "?"), do: "&", else: "?" full = if qs == "", do: path, else: path <> sep <> qs request_json(c, "GET", full, nil) end defp build_qs(opts) do opts |> Enum.flat_map(fn {:filters, m} when is_map(m) -> Enum.map(m, fn {fk, fv} -> encode_pair(to_string(fk), value_to_string(fv)) end) {k, v} -> [encode_pair(to_string(k), value_to_string(v))] end) |> Enum.reject(&is_nil/1) |> Enum.join("&") end defp encode_pair(_k, nil), do: nil defp encode_pair(k, v), do: URI.encode_www_form(k) <> "=" <> URI.encode_www_form(v) defp value_to_string(nil), do: nil defp value_to_string(v) when is_binary(v), do: v defp value_to_string(v) when is_atom(v), do: Atom.to_string(v) defp value_to_string(v) when is_integer(v), do: Integer.to_string(v) defp value_to_string(v) when is_float(v), do: Float.to_string(v) defp value_to_string(true), do: "true" defp value_to_string(false), do: "false" defp value_to_string(v), do: inspect(v) defp do_request(c, method, path, body, attempt) do url = c.base_url <> path json_body = if body == nil, do: nil, else: JSON.encode!(body) case send_following_redirects(c, method, url, json_body, false, 0) do {:ok, status, headers, _raw} when status in @retryable_statuses and attempt + 1 < @max_retries -> ra = parse_retry_after(headers) :timer.sleep(round(backoff_seconds(attempt, ra) * 1000)) do_request(c, method, path, body, attempt + 1) {:ok, status, _headers, raw} when status >= 400 -> parsed = safe_decode(raw) msg = case parsed do %{"detail" => d} when is_binary(d) -> d %{"message" => m} when is_binary(m) -> m _ -> "request failed" end emit_call_event(c, method, path, status, false) raise ApiError, status: status, message: msg, body: parsed {:ok, status, _headers, ""} -> emit_call_event(c, method, path, status, true) {:ok, nil} {:ok, status, _headers, raw} -> parsed = safe_decode(raw) emit_call_event(c, method, path, status, true) {:ok, parsed} {:error, _reason} when attempt + 1 < @max_retries -> :timer.sleep(round(backoff_seconds(attempt, nil) * 1000)) do_request(c, method, path, body, attempt + 1) {:error, reason} -> emit_call_event(c, method, path, 0, false) raise ApiError, status: 0, message: inspect(reason), body: nil end end defp parse_retry_after(headers) do case List.keyfind(headers, ~c"retry-after", 0) do {_, v} -> case Float.parse(List.to_string(v)) do {f, _} -> f _ -> nil end _ -> nil end end # Walk the redirect chain manually so Authorization can be dropped on # cross-origin hops. Caps at 5 hops; mirrors RFC 7231 method-rewrite # semantics. defp send_following_redirects(_c, _m, _u, _b, _strip, hop) when hop >= 5 do {:ok, 0, [], ""} end defp send_following_redirects(c, method, url, body, strip_auth, hop) do headers = build_headers(c, strip_auth) request = cond do body == nil -> {String.to_charlist(url), headers} true -> {String.to_charlist(url), headers, ~c"application/json", body} end method_atom = case String.upcase(method) do "GET" -> :get "POST" -> :post "PATCH" -> :patch "PUT" -> :put "DELETE" -> :delete "HEAD" -> :head _ -> :get end http_options = [timeout: @default_timeout, connect_timeout: 15_000, autoredirect: false] options = [body_format: :binary] case :httpc.request(method_atom, request, http_options, options) do {:ok, {{_, status, _}, hdrs, raw}} -> hmap = Enum.map(hdrs, fn {k, v} -> {k, v} end) cond do status >= 300 and status < 400 and status != 304 -> case List.keyfind(hmap, ~c"location", 0) do {_, loc} -> next_url = List.to_string(loc) strip = strip_auth or origin_of(next_url) != origin_of(url) {next_method, next_body} = cond do status == 303 -> {"GET", nil} status in [301, 302] and method not in ["GET", "HEAD"] -> {"GET", nil} true -> {method, body} end send_following_redirects(c, next_method, next_url, next_body, strip, hop + 1) _ -> {:ok, status, hmap, IO.iodata_to_binary(raw)} end true -> {:ok, status, hmap, IO.iodata_to_binary(raw)} end {:error, reason} -> {:error, reason} end end defp build_headers(c, strip_auth) do base = [ {~c"accept", ~c"application/json"}, {~c"user-agent", String.to_charlist(user_agent())}, {~c"x-client-channel", String.to_charlist("client_" <> @language)}, {~c"x-client-version", String.to_charlist(@client_version)}, {~c"x-analytics-device-id", String.to_charlist(c.device_id)}, {~c"x-analytics-session-id", String.to_charlist(c.session_id)} ] if strip_auth or c.token == "" do base else [{~c"authorization", String.to_charlist("Bearer " <> c.token)} | base] end end defp origin_of(url) do uri = URI.parse(url) "#{uri.scheme}://#{uri.host}:#{uri.port || default_port(uri.scheme)}" end defp default_port("https"), do: 443 defp default_port("http"), do: 80 defp default_port(_), do: 0 # ── Analytics ──────────────────────────────────────────────────────── defp emit_call_event(c, method, path, status, ok) do include_env = process_flag_once(:meta_sent_once) Task.start(fn -> try do path_base = path |> String.split("?") |> List.first() path_base = if String.length(path_base) > 128, do: binary_part(path_base, 0, 128), else: path_base meta = %{ "channel" => "client_" <> @language, "client_version" => @client_version, "module_name" => @module_name, "language" => @language, "elixir_version" => System.version(), "otp_version" => List.to_string(:erlang.system_info(:otp_release)) } meta = if include_env, do: Map.put(meta, "env", fingerprint()), else: meta evt = %{ "type" => "client.call", "ts_client" => System.system_time(:second), "meta" => %{ "method" => String.upcase(method), "path" => path_base, "status" => status, "ok" => ok } } body = JSON.encode!(%{ "device_id" => c.device_id, "session_id" => c.session_id, "events" => [evt], "meta" => meta }) url = String.to_charlist(c.base_url <> "/xapi2/analytics/challenge") headers = [ {~c"user-agent", String.to_charlist(user_agent())} ] :httpc.request(:post, {url, headers, ~c"application/json", body}, [timeout: 4_000, connect_timeout: 2_000], [body_format: :binary]) rescue _ -> :ok catch _, _ -> :ok end end) end # Returns true the first time it's called per-process for a given key. defp process_flag_once(key) do case Process.get({__MODULE__, key}) do true -> false _ -> Process.put({__MODULE__, key}, true) true end end # ── Auto-update ────────────────────────────────────────────────────── defp maybe_autoupdate(c) do case process_flag_once(:autoupdate_attempted) do false -> :ok true -> if autoupdate_enabled?() do Task.start(fn -> run_autoupdate(c) end) end :ok end end defp run_autoupdate(c) do try do do_run_autoupdate(c) rescue _ -> :ok catch _, _ -> :ok end end defp do_run_autoupdate(c) do case state_dir() do nil -> :ok d -> stamp = Path.join(d, "update_check.json") if check_due?(stamp) do _ = File.write(stamp, JSON.encode!(%{"checked_at" => System.system_time(:second)})) probe_and_maybe_replace(c) end end end defp check_due?(stamp) do case File.read(stamp) do {:ok, raw} -> case safe_decode(raw) do %{"checked_at" => last} when is_integer(last) -> System.system_time(:second) - last >= 86_400 _ -> true end _ -> true end end defp probe_and_maybe_replace(c) do url = String.to_charlist(c.base_url <> "/xapi2/clients/version") case :httpc.request(:get, {url, []}, [timeout: 4_000, connect_timeout: 2_000], [body_format: :binary]) do {:ok, {{_, 200, _}, _, raw}} -> case safe_decode(IO.iodata_to_binary(raw)) do %{"version" => latest} when is_binary(latest) and latest != @client_version -> fetch_and_replace(c, latest) _ -> :ok end _ -> :ok end end defp fetch_and_replace(c, _latest) do url = String.to_charlist(c.base_url <> "/xapi2/clients/script." <> @language) case :httpc.request(:get, {url, []}, [timeout: 10_000, connect_timeout: 2_000], [body_format: :binary]) do {:ok, {{_, 200, _}, _, raw}} -> body = IO.iodata_to_binary(raw) if looks_like_valid_client?(body) do target = __ENV__.file if is_binary(target) and File.exists?(target) do tmp = target <> ".tmp." <> Integer.to_string(System.system_time(:nanosecond)) case File.write(tmp, body) do :ok -> case File.rename(tmp, target) do :ok -> :ok _ -> _ = File.rm(tmp); :ok end _ -> :ok end end end _ -> :ok end end defp looks_like_valid_client?(body) when is_binary(body) and byte_size(body) > 2_000 do Enum.all?(["@module_name", "@client_version", "@app_slug", "request_json"], fn m -> String.contains?(body, m) end) end defp looks_like_valid_client?(_), do: false defp trim_right_slash(s) do s |> String.trim_trailing("/") |> String.trim_trailing("/") end # ── Generated per-type wrapper functions ───────────────────────────── # Every model that exposes an op gets one `_` function # below. The runtime above does the heavy lifting; these wrappers # just pin the URL + HTTP verb. @doc "List `activity` rows. Pass any allowed filter as a keyword." def activity_list(client, opts \\ []) do request_list(client, "/xapi2/data/activity", opts) end @doc "Fetch one `activity` row by id." def activity_get(client, id) do request_json(client, "GET", "/xapi2/data/activity/" <> id, nil) end @doc "Create a new `activity` row." def activity_create(client, data) do request_json(client, "POST", "/xapi2/data/activity", data) end @doc "Patch a `activity` row." def activity_update(client, id, data) do request_json(client, "PATCH", "/xapi2/data/activity/" <> id, data) end @doc "Delete a `activity` row." def activity_delete(client, id) do _ = request_json(client, "DELETE", "/xapi2/data/activity/" <> id, nil) true end @doc "List `contact` rows. Pass any allowed filter as a keyword." def contact_list(client, opts \\ []) do request_list(client, "/xapi2/data/contact", opts) end @doc "Fetch one `contact` row by id." def contact_get(client, id) do request_json(client, "GET", "/xapi2/data/contact/" <> id, nil) end @doc "Create a new `contact` row." def contact_create(client, data) do request_json(client, "POST", "/xapi2/data/contact", data) end @doc "Patch a `contact` row." def contact_update(client, id, data) do request_json(client, "PATCH", "/xapi2/data/contact/" <> id, data) end @doc "Delete a `contact` row." def contact_delete(client, id) do _ = request_json(client, "DELETE", "/xapi2/data/contact/" <> id, nil) true end @doc "List `conversation` rows. Pass any allowed filter as a keyword." def conversation_list(client, opts \\ []) do request_list(client, "/xapi2/data/conversation", opts) end @doc "Fetch one `conversation` row by id." def conversation_get(client, id) do request_json(client, "GET", "/xapi2/data/conversation/" <> id, nil) end @doc "Create a new `conversation` row." def conversation_create(client, data) do request_json(client, "POST", "/xapi2/data/conversation", data) end @doc "Patch a `conversation` row." def conversation_update(client, id, data) do request_json(client, "PATCH", "/xapi2/data/conversation/" <> id, data) end @doc "Delete a `conversation` row." def conversation_delete(client, id) do _ = request_json(client, "DELETE", "/xapi2/data/conversation/" <> id, nil) true end @doc "List `custom_field` rows. Pass any allowed filter as a keyword." def custom_field_list(client, opts \\ []) do request_list(client, "/xapi2/data/custom_field", opts) end @doc "Fetch one `custom_field` row by id." def custom_field_get(client, id) do request_json(client, "GET", "/xapi2/data/custom_field/" <> id, nil) end @doc "Create a new `custom_field` row." def custom_field_create(client, data) do request_json(client, "POST", "/xapi2/data/custom_field", data) end @doc "Patch a `custom_field` row." def custom_field_update(client, id, data) do request_json(client, "PATCH", "/xapi2/data/custom_field/" <> id, data) end @doc "Delete a `custom_field` row." def custom_field_delete(client, id) do _ = request_json(client, "DELETE", "/xapi2/data/custom_field/" <> id, nil) true end @doc "List `gift` rows. Pass any allowed filter as a keyword." def gift_list(client, opts \\ []) do request_list(client, "/xapi2/data/gift", opts) end @doc "Fetch one `gift` row by id." def gift_get(client, id) do request_json(client, "GET", "/xapi2/data/gift/" <> id, nil) end @doc "Create a new `gift` row." def gift_create(client, data) do request_json(client, "POST", "/xapi2/data/gift", data) end @doc "Patch a `gift` row." def gift_update(client, id, data) do request_json(client, "PATCH", "/xapi2/data/gift/" <> id, data) end @doc "Delete a `gift` row." def gift_delete(client, id) do _ = request_json(client, "DELETE", "/xapi2/data/gift/" <> id, nil) true end @doc "List `journal_entry` rows. Pass any allowed filter as a keyword." def journal_entry_list(client, opts \\ []) do request_list(client, "/xapi2/data/journal_entry", opts) end @doc "Fetch one `journal_entry` row by id." def journal_entry_get(client, id) do request_json(client, "GET", "/xapi2/data/journal_entry/" <> id, nil) end @doc "Create a new `journal_entry` row." def journal_entry_create(client, data) do request_json(client, "POST", "/xapi2/data/journal_entry", data) end @doc "Patch a `journal_entry` row." def journal_entry_update(client, id, data) do request_json(client, "PATCH", "/xapi2/data/journal_entry/" <> id, data) end @doc "Delete a `journal_entry` row." def journal_entry_delete(client, id) do _ = request_json(client, "DELETE", "/xapi2/data/journal_entry/" <> id, nil) true end @doc "List `life_event` rows. Pass any allowed filter as a keyword." def life_event_list(client, opts \\ []) do request_list(client, "/xapi2/data/life_event", opts) end @doc "Fetch one `life_event` row by id." def life_event_get(client, id) do request_json(client, "GET", "/xapi2/data/life_event/" <> id, nil) end @doc "Create a new `life_event` row." def life_event_create(client, data) do request_json(client, "POST", "/xapi2/data/life_event", data) end @doc "Patch a `life_event` row." def life_event_update(client, id, data) do request_json(client, "PATCH", "/xapi2/data/life_event/" <> id, data) end @doc "Delete a `life_event` row." def life_event_delete(client, id) do _ = request_json(client, "DELETE", "/xapi2/data/life_event/" <> id, nil) true end @doc "List `note` rows. Pass any allowed filter as a keyword." def note_list(client, opts \\ []) do request_list(client, "/xapi2/data/note", opts) end @doc "Fetch one `note` row by id." def note_get(client, id) do request_json(client, "GET", "/xapi2/data/note/" <> id, nil) end @doc "Create a new `note` row." def note_create(client, data) do request_json(client, "POST", "/xapi2/data/note", data) end @doc "Patch a `note` row." def note_update(client, id, data) do request_json(client, "PATCH", "/xapi2/data/note/" <> id, data) end @doc "Delete a `note` row." def note_delete(client, id) do _ = request_json(client, "DELETE", "/xapi2/data/note/" <> id, nil) true end @doc "List `pet` rows. Pass any allowed filter as a keyword." def pet_list(client, opts \\ []) do request_list(client, "/xapi2/data/pet", opts) end @doc "Fetch one `pet` row by id." def pet_get(client, id) do request_json(client, "GET", "/xapi2/data/pet/" <> id, nil) end @doc "Create a new `pet` row." def pet_create(client, data) do request_json(client, "POST", "/xapi2/data/pet", data) end @doc "Patch a `pet` row." def pet_update(client, id, data) do request_json(client, "PATCH", "/xapi2/data/pet/" <> id, data) end @doc "Delete a `pet` row." def pet_delete(client, id) do _ = request_json(client, "DELETE", "/xapi2/data/pet/" <> id, nil) true end @doc "List `relationship` rows. Pass any allowed filter as a keyword." def relationship_list(client, opts \\ []) do request_list(client, "/xapi2/data/relationship", opts) end @doc "Fetch one `relationship` row by id." def relationship_get(client, id) do request_json(client, "GET", "/xapi2/data/relationship/" <> id, nil) end @doc "Create a new `relationship` row." def relationship_create(client, data) do request_json(client, "POST", "/xapi2/data/relationship", data) end @doc "Patch a `relationship` row." def relationship_update(client, id, data) do request_json(client, "PATCH", "/xapi2/data/relationship/" <> id, data) end @doc "Delete a `relationship` row." def relationship_delete(client, id) do _ = request_json(client, "DELETE", "/xapi2/data/relationship/" <> id, nil) true end @doc "List `reminder` rows. Pass any allowed filter as a keyword." def reminder_list(client, opts \\ []) do request_list(client, "/xapi2/data/reminder", opts) end @doc "Fetch one `reminder` row by id." def reminder_get(client, id) do request_json(client, "GET", "/xapi2/data/reminder/" <> id, nil) end @doc "Create a new `reminder` row." def reminder_create(client, data) do request_json(client, "POST", "/xapi2/data/reminder", data) end @doc "Patch a `reminder` row." def reminder_update(client, id, data) do request_json(client, "PATCH", "/xapi2/data/reminder/" <> id, data) end @doc "Delete a `reminder` row." def reminder_delete(client, id) do _ = request_json(client, "DELETE", "/xapi2/data/reminder/" <> id, nil) true end end