From cd3326d4b6e0130f114ac293954a6b806466d2bf Mon Sep 17 00:00:00 2001 From: javierg Date: Tue, 23 Dec 2025 14:25:51 -0800 Subject: [PATCH 1/4] Support count response --- lib/cyphi/client/response.ex | 5 +++++ test/cyphi/client_test.exs | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/lib/cyphi/client/response.ex b/lib/cyphi/client/response.ex index 0be8b23..2fc5179 100644 --- a/lib/cyphi/client/response.ex +++ b/lib/cyphi/client/response.ex @@ -22,6 +22,7 @@ defmodule Cyphi.Client.Response do def decode({:ok, %Req.Response{status: status, body: body}}, %{ response: response_spec }) do + case List.keyfind(response_spec, status, 0) do {status, {module, type}} when status in [200, 201] -> {:ok, decode_body(body, module, type)} @@ -49,6 +50,10 @@ defmodule Cyphi.Client.Response do parse(module, map, type) end + defp parse(_module, %{"count" => count}, _type) do + count + end + defp parse(module, map, type) do attrs = module.__fields__(type) diff --git a/test/cyphi/client_test.exs b/test/cyphi/client_test.exs index 7f95f3e..86fe2e4 100644 --- a/test/cyphi/client_test.exs +++ b/test/cyphi/client_test.exs @@ -19,6 +19,18 @@ defmodule Cyphi.ClientTest do end) end + test "count list request returns amount" do + opts = %{ + method: :get, + url: "/badges", + response: [{200, [{Cyphi.Badge, :t}]}] + } + + with_response(%{status: 200, body: %{"count" => 10}}, fn -> + assert {:ok, 10} == Client.request(opts) + end) + end + test "single item request returns decoded response (GET)" do opts = %{ method: :get, From 6a08c6866cc0e4565dd644b29582e8d5c0b884ab Mon Sep 17 00:00:00 2001 From: javierg Date: Tue, 23 Dec 2025 14:38:04 -0800 Subject: [PATCH 2/4] remove extra space --- lib/cyphi/client/response.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/cyphi/client/response.ex b/lib/cyphi/client/response.ex index 2fc5179..28202ff 100644 --- a/lib/cyphi/client/response.ex +++ b/lib/cyphi/client/response.ex @@ -22,7 +22,6 @@ defmodule Cyphi.Client.Response do def decode({:ok, %Req.Response{status: status, body: body}}, %{ response: response_spec }) do - case List.keyfind(response_spec, status, 0) do {status, {module, type}} when status in [200, 201] -> {:ok, decode_body(body, module, type)} From 24580a6982260a18924196b9b5f377b7e61832d5 Mon Sep 17 00:00:00 2001 From: javierg Date: Thu, 24 Sep 2026 09:02:12 -0700 Subject: [PATCH 3/4] allow associated structs --- codegen/renderer.ex | 102 +++++++++++++++++++++++++ config/config.exs | 1 + lib/cyphi/answer.ex | 6 ++ lib/cyphi/assistant.ex | 6 +- lib/cyphi/award.ex | 6 +- lib/cyphi/badge_history.ex | 11 ++- lib/cyphi/batch.ex | 3 + lib/cyphi/course.ex | 9 +++ lib/cyphi/course_template.ex | 3 + lib/cyphi/grade.ex | 12 +++ lib/cyphi/group_membership.ex | 6 +- lib/cyphi/instructor.ex | 6 +- lib/cyphi/learner.ex | 33 ++++++++ lib/cyphi/level_history.ex | 11 ++- lib/cyphi/module.ex | 3 + lib/cyphi/order.ex | 9 +++ lib/cyphi/organization.ex | 4 +- lib/cyphi/player.ex | 11 ++- lib/cyphi/scorm_answer.ex | 17 ++++- lib/cyphi/section.ex | 6 ++ lib/cyphi/session.ex | 4 +- lib/cyphi/user.ex | 15 ++++ lib/cyphi/visit.ex | 3 + mix.exs | 5 +- test/cyphi/client/response_test.exs | 61 +++++++++++++++ test/cyphi/generator/renderer_test.exs | 90 ++++++++++++++++++++++ 26 files changed, 426 insertions(+), 17 deletions(-) create mode 100644 codegen/renderer.ex create mode 100644 test/cyphi/generator/renderer_test.exs diff --git a/codegen/renderer.ex b/codegen/renderer.ex new file mode 100644 index 0000000..eb3d037 --- /dev/null +++ b/codegen/renderer.ex @@ -0,0 +1,102 @@ +defmodule Cyphi.Generator.Renderer do + @moduledoc """ + Adds documented `$include` relationships missing from the upstream schemas. + + Relationships remain raw maps to preserve the client's existing nested-value + decoding behavior. Upstream field definitions always take precedence. + This module is compiled only in development and test environments. + """ + use OpenAPI.Renderer + + alias OpenAPI.Processor.Schema + alias OpenAPI.Processor.Schema.Field + + # Names are relative to output.base_module in the renderer's processed schemas. + @relationships %{ + Answer => [:grade, :question, :user], + Assistant => [:user, :course], + Award => [:user, :awardee], + BadgeHistory => [:badge], + Batch => [:user], + Course => [:parent, :current_module, :organization], + CourseTemplate => [:organization], + Grade => [:user, :grader, :assessment, :course], + GroupMembership => [:user, :group], + Instructor => [:user, :course], + Learner => [ + :user, + :course, + :progress, + :enrolled_by, + :unenrolled_by, + :deactivated_by, + :reactivated_by, + :transferred_by, + :transferred_from, + :transferred_to, + :order_item + ], + LevelHistory => [:level], + Module => [:course], + Order => [:user, :organization, :verifier], + Organization => [:parent], + Player => [:user], + ScormAnswer => [:grade, :user], + Section => [:module, :referenced_course], + Session => [:user], + User => [:organization, :archiver, :job_title, :manager, :added_by], + Visit => [:user] + } + + @impl OpenAPI.Renderer + def render_schema_types(state, schemas) do + super(state, Enum.map(schemas, &add_relationships/1)) + end + + @impl OpenAPI.Renderer + def render_schema_struct(state, schemas) do + super(state, Enum.map(schemas, &add_relationships/1)) + end + + @impl OpenAPI.Renderer + def render_schema_field_function(state, schemas) do + super(state, Enum.map(schemas, &add_relationships/1)) + end + + @impl OpenAPI.Renderer + def format(state, file) do + contents = super(state, file) |> IO.iodata_to_binary() + + field_count = + file.schemas + |> Enum.filter(&(&1.output_format == :struct)) + |> Enum.map(&add_relationships/1) + |> Enum.flat_map(& &1.fields) + |> Enum.uniq_by(& &1.name) + |> length() + + # Large API structs cannot be split without changing the public response shape. + if field_count > 31 do + String.replace(contents, " defstruct ", " # credo:disable-for-next-line\n defstruct ", + global: false + ) + else + contents + end + end + + defp add_relationships(%Schema{output_format: :struct, type_name: :t} = schema) do + existing_names = MapSet.new(schema.fields, & &1.name) + + fields = + @relationships + |> Map.get(schema.module_name, []) + |> Enum.map(&Atom.to_string/1) + |> Enum.reject(&MapSet.member?(existing_names, &1)) + |> Enum.map(&%Field{name: &1, type: :map, nullable: true, required: false}) + + %{schema | fields: schema.fields ++ fields} + end + + defp add_relationships(schema), do: schema +end diff --git a/config/config.exs b/config/config.exs index 2a509d0..35f480b 100644 --- a/config/config.exs +++ b/config/config.exs @@ -7,6 +7,7 @@ config :cyphi, if config_env() == :dev do config :oapi_generator, default: [ + renderer: Cyphi.Generator.Renderer, output: [ base_module: Cyphi, location: "lib/cyphi" diff --git a/lib/cyphi/answer.ex b/lib/cyphi/answer.ex index 53f1c80..a99eeaf 100644 --- a/lib/cyphi/answer.ex +++ b/lib/cyphi/answer.ex @@ -9,9 +9,11 @@ defmodule Cyphi.Answer do id: integer | nil, points: number | nil, position: integer | nil, + question: map | nil, question_id: integer | nil, response: String.t() | nil, score: number | nil, + user: map | nil, user_id: integer | nil } @@ -21,9 +23,11 @@ defmodule Cyphi.Answer do :id, :points, :position, + :question, :question_id, :response, :score, + :user, :user_id ] @@ -38,9 +42,11 @@ defmodule Cyphi.Answer do id: :integer, points: {:number, "float"}, position: :integer, + question: :map, question_id: :integer, response: :string, score: {:number, "float"}, + user: :map, user_id: :integer ] end diff --git a/lib/cyphi/assistant.ex b/lib/cyphi/assistant.ex index bfd273b..b33b7ee 100644 --- a/lib/cyphi/assistant.ex +++ b/lib/cyphi/assistant.ex @@ -4,13 +4,15 @@ defmodule Cyphi.Assistant do """ @type t :: %__MODULE__{ + course: map | nil, course_id: integer | nil, id: integer | nil, last_visited_at: DateTime.t() | nil, + user: map | nil, user_id: integer } - defstruct [:course_id, :id, :last_visited_at, :user_id] + defstruct [:course, :course_id, :id, :last_visited_at, :user, :user_id] @doc false @spec __fields__(atom) :: keyword @@ -18,9 +20,11 @@ defmodule Cyphi.Assistant do def __fields__(:t) do [ + course: :map, course_id: :integer, id: :integer, last_visited_at: {:string, "date-time"}, + user: :map, user_id: :integer ] end diff --git a/lib/cyphi/award.ex b/lib/cyphi/award.ex index fffb54e..c5984ae 100644 --- a/lib/cyphi/award.ex +++ b/lib/cyphi/award.ex @@ -5,13 +5,15 @@ defmodule Cyphi.Award do @type t :: %__MODULE__{ awarded_at: DateTime.t() | nil, + awardee: map | nil, awardee_id: integer | nil, id: integer | nil, note: String.t() | nil, + user: map | nil, user_id: integer } - defstruct [:awarded_at, :awardee_id, :id, :note, :user_id] + defstruct [:awarded_at, :awardee, :awardee_id, :id, :note, :user, :user_id] @doc false @spec __fields__(atom) :: keyword @@ -20,9 +22,11 @@ defmodule Cyphi.Award do def __fields__(:t) do [ awarded_at: {:string, "date-time"}, + awardee: :map, awardee_id: :integer, id: :integer, note: :string, + user: :map, user_id: :integer ] end diff --git a/lib/cyphi/badge_history.ex b/lib/cyphi/badge_history.ex index b80365b..0802cca 100644 --- a/lib/cyphi/badge_history.ex +++ b/lib/cyphi/badge_history.ex @@ -3,15 +3,20 @@ defmodule Cyphi.BadgeHistory do Provides struct and type for a BadgeHistory """ - @type t :: %__MODULE__{awarded_at: DateTime.t() | nil, badge_id: integer, id: integer | nil} + @type t :: %__MODULE__{ + awarded_at: DateTime.t() | nil, + badge: map | nil, + badge_id: integer, + id: integer | nil + } - defstruct [:awarded_at, :badge_id, :id] + defstruct [:awarded_at, :badge, :badge_id, :id] @doc false @spec __fields__(atom) :: keyword def __fields__(type \\ :t) def __fields__(:t) do - [awarded_at: {:string, "date-time"}, badge_id: :integer, id: :integer] + [awarded_at: {:string, "date-time"}, badge: :map, badge_id: :integer, id: :integer] end end diff --git a/lib/cyphi/batch.ex b/lib/cyphi/batch.ex index 6cfa1cb..c8422c7 100644 --- a/lib/cyphi/batch.ex +++ b/lib/cyphi/batch.ex @@ -18,6 +18,7 @@ defmodule Cyphi.Batch do status: String.t() | nil, successful: integer | nil, uri: String.t() | nil, + user: map | nil, user_id: integer } @@ -36,6 +37,7 @@ defmodule Cyphi.Batch do :status, :successful, :uri, + :user, :user_id ] @@ -59,6 +61,7 @@ defmodule Cyphi.Batch do status: :string, successful: :integer, uri: :string, + user: :map, user_id: :integer ] end diff --git a/lib/cyphi/course.ex b/lib/cyphi/course.ex index 92de285..429bd6e 100644 --- a/lib/cyphi/course.ex +++ b/lib/cyphi/course.ex @@ -16,6 +16,7 @@ defmodule Cyphi.Course do class_times: [map] | nil, course_code: String.t() | nil, credits: number | nil, + current_module: map | nil, current_module_id: integer | nil, custom_fields: map | nil, delete_history_on_unenroll: boolean | nil, @@ -32,8 +33,10 @@ defmodule Cyphi.Course do must_repurchase_to_reenroll: boolean | nil, name: String.t() | nil, open_enrollment: boolean | nil, + organization: map | nil, organization_id: integer | nil, organization_name: String.t() | nil, + parent: map | nil, parent_id: integer | nil, path: boolean | nil, picture: String.t() | nil, @@ -68,6 +71,7 @@ defmodule Cyphi.Course do :class_times, :course_code, :credits, + :current_module, :current_module_id, :custom_fields, :delete_history_on_unenroll, @@ -84,8 +88,10 @@ defmodule Cyphi.Course do :must_repurchase_to_reenroll, :name, :open_enrollment, + :organization, :organization_id, :organization_name, + :parent, :parent_id, :path, :picture, @@ -124,6 +130,7 @@ defmodule Cyphi.Course do class_times: [:map], course_code: :string, credits: {:number, "float"}, + current_module: :map, current_module_id: :integer, custom_fields: :map, delete_history_on_unenroll: :boolean, @@ -140,8 +147,10 @@ defmodule Cyphi.Course do must_repurchase_to_reenroll: :boolean, name: :string, open_enrollment: :boolean, + organization: :map, organization_id: :integer, organization_name: :string, + parent: :map, parent_id: :integer, path: :boolean, picture: :string, diff --git a/lib/cyphi/course_template.ex b/lib/cyphi/course_template.ex index 6be5673..420492d 100644 --- a/lib/cyphi/course_template.ex +++ b/lib/cyphi/course_template.ex @@ -25,6 +25,7 @@ defmodule Cyphi.CourseTemplate do must_repurchase_to_reenroll: boolean | nil, name: String.t() | nil, open_enrollment: boolean | nil, + organization: map | nil, organization_id: integer | nil, organization_name: String.t() | nil, path: boolean | nil, @@ -68,6 +69,7 @@ defmodule Cyphi.CourseTemplate do :must_repurchase_to_reenroll, :name, :open_enrollment, + :organization, :organization_id, :organization_name, :path, @@ -115,6 +117,7 @@ defmodule Cyphi.CourseTemplate do must_repurchase_to_reenroll: :boolean, name: :string, open_enrollment: :boolean, + organization: :map, organization_id: :integer, organization_name: :string, path: :boolean, diff --git a/lib/cyphi/grade.ex b/lib/cyphi/grade.ex index 3f50699..b02d2d6 100644 --- a/lib/cyphi/grade.ex +++ b/lib/cyphi/grade.ex @@ -5,7 +5,9 @@ defmodule Cyphi.Grade do @type t :: %__MODULE__{ absent: boolean | nil, + assessment: map | nil, assessment_id: integer | nil, + course: map | nil, course_id: String.t() | nil, excused: boolean | nil, excused_comment: String.t() | nil, @@ -15,6 +17,7 @@ defmodule Cyphi.Grade do grade: String.t() | nil, graded: integer | nil, graded_at: DateTime.t() | nil, + grader: map | nil, grader_id: integer | nil, id: integer | nil, incomplete: boolean | nil, @@ -28,12 +31,15 @@ defmodule Cyphi.Grade do score: number | nil, started: boolean | nil, started_at: DateTime.t() | nil, + user: map | nil, user_id: integer | nil } defstruct [ :absent, + :assessment, :assessment_id, + :course, :course_id, :excused, :excused_comment, @@ -43,6 +49,7 @@ defmodule Cyphi.Grade do :grade, :graded, :graded_at, + :grader, :grader_id, :id, :incomplete, @@ -56,6 +63,7 @@ defmodule Cyphi.Grade do :score, :started, :started_at, + :user, :user_id ] @@ -66,7 +74,9 @@ defmodule Cyphi.Grade do def __fields__(:t) do [ absent: :boolean, + assessment: :map, assessment_id: :integer, + course: :map, course_id: :string, excused: :boolean, excused_comment: :string, @@ -76,6 +86,7 @@ defmodule Cyphi.Grade do grade: :string, graded: :integer, graded_at: {:string, "date-time"}, + grader: :map, grader_id: :integer, id: :integer, incomplete: :boolean, @@ -89,6 +100,7 @@ defmodule Cyphi.Grade do score: {:number, "float"}, started: :boolean, started_at: {:string, "date-time"}, + user: :map, user_id: :integer ] end diff --git a/lib/cyphi/group_membership.ex b/lib/cyphi/group_membership.ex index 2a621a2..d6dc0d8 100644 --- a/lib/cyphi/group_membership.ex +++ b/lib/cyphi/group_membership.ex @@ -4,15 +4,17 @@ defmodule Cyphi.GroupMembership do """ @type t :: %__MODULE__{ + group: map | nil, group_id: integer | nil, id: integer | nil, join_type: String.t() | nil, joined_at: DateTime.t() | nil, last_visited_at: DateTime.t() | nil, + user: map | nil, user_id: integer } - defstruct [:group_id, :id, :join_type, :joined_at, :last_visited_at, :user_id] + defstruct [:group, :group_id, :id, :join_type, :joined_at, :last_visited_at, :user, :user_id] @doc false @spec __fields__(atom) :: keyword @@ -20,11 +22,13 @@ defmodule Cyphi.GroupMembership do def __fields__(:t) do [ + group: :map, group_id: :integer, id: :integer, join_type: :string, joined_at: {:string, "date-time"}, last_visited_at: {:string, "date-time"}, + user: :map, user_id: :integer ] end diff --git a/lib/cyphi/instructor.ex b/lib/cyphi/instructor.ex index 0e04e1f..7f9eee2 100644 --- a/lib/cyphi/instructor.ex +++ b/lib/cyphi/instructor.ex @@ -5,13 +5,15 @@ defmodule Cyphi.Instructor do @type t :: %__MODULE__{ coinstructor: boolean | nil, + course: map | nil, course_id: integer | nil, id: integer | nil, last_visited_at: DateTime.t() | nil, + user: map | nil, user_id: integer } - defstruct [:coinstructor, :course_id, :id, :last_visited_at, :user_id] + defstruct [:coinstructor, :course, :course_id, :id, :last_visited_at, :user, :user_id] @doc false @spec __fields__(atom) :: keyword @@ -20,9 +22,11 @@ defmodule Cyphi.Instructor do def __fields__(:t) do [ coinstructor: :boolean, + course: :map, course_id: :integer, id: :integer, last_visited_at: {:string, "date-time"}, + user: :map, user_id: :integer ] end diff --git a/lib/cyphi/learner.ex b/lib/cyphi/learner.ex index 1ac2000..8e9bdd1 100644 --- a/lib/cyphi/learner.ex +++ b/lib/cyphi/learner.ex @@ -7,36 +7,47 @@ defmodule Cyphi.Learner do completed: boolean | nil, completed_at: DateTime.t() | nil, completed_by_id: integer | nil, + course: map | nil, course_archived: boolean | nil, course_id: integer | nil, deactivated: boolean | nil, deactivated_at: DateTime.t() | nil, + deactivated_by: map | nil, deactivated_by_id: integer | nil, enroll_type: String.t() | nil, enrolled_at: DateTime.t() | nil, + enrolled_by: map | nil, enrolled_by_id: integer | nil, grade: String.t() | nil, id: integer | nil, last_visited_at: DateTime.t() | nil, + order_item: map | nil, order_item_id: integer | nil, override_at: DateTime.t() | nil, override_by_id: integer | nil, override_comment: String.t() | nil, override_percent: number | nil, percent: number | nil, + progress: map | nil, reactivated_at: DateTime.t() | nil, + reactivated_by: map | nil, reactivated_by_id: integer | nil, started: boolean | nil, started_at: DateTime.t() | nil, time_spent: integer | nil, transferred: boolean | nil, transferred_at: DateTime.t() | nil, + transferred_by: map | nil, transferred_by_id: integer | nil, + transferred_from: map | nil, transferred_from_id: integer | nil, + transferred_to: map | nil, transferred_to_id: integer | nil, unenrolled: boolean | nil, unenrolled_at: DateTime.t() | nil, + unenrolled_by: map | nil, unenrolled_by_id: integer | nil, + user: map | nil, user_archived: boolean | nil, user_id: integer } @@ -46,36 +57,47 @@ defmodule Cyphi.Learner do :completed, :completed_at, :completed_by_id, + :course, :course_archived, :course_id, :deactivated, :deactivated_at, + :deactivated_by, :deactivated_by_id, :enroll_type, :enrolled_at, + :enrolled_by, :enrolled_by_id, :grade, :id, :last_visited_at, + :order_item, :order_item_id, :override_at, :override_by_id, :override_comment, :override_percent, :percent, + :progress, :reactivated_at, + :reactivated_by, :reactivated_by_id, :started, :started_at, :time_spent, :transferred, :transferred_at, + :transferred_by, :transferred_by_id, + :transferred_from, :transferred_from_id, + :transferred_to, :transferred_to_id, :unenrolled, :unenrolled_at, + :unenrolled_by, :unenrolled_by_id, + :user, :user_archived, :user_id ] @@ -89,36 +111,47 @@ defmodule Cyphi.Learner do completed: :boolean, completed_at: {:string, "date-time"}, completed_by_id: :integer, + course: :map, course_archived: :boolean, course_id: :integer, deactivated: :boolean, deactivated_at: {:string, "date-time"}, + deactivated_by: :map, deactivated_by_id: :integer, enroll_type: :string, enrolled_at: {:string, "date-time"}, + enrolled_by: :map, enrolled_by_id: :integer, grade: :string, id: :integer, last_visited_at: {:string, "date-time"}, + order_item: :map, order_item_id: :integer, override_at: {:string, "date-time"}, override_by_id: :integer, override_comment: :string, override_percent: {:number, "float"}, percent: {:number, "float"}, + progress: :map, reactivated_at: {:string, "date-time"}, + reactivated_by: :map, reactivated_by_id: :integer, started: :boolean, started_at: {:string, "date-time"}, time_spent: :integer, transferred: :boolean, transferred_at: {:string, "date-time"}, + transferred_by: :map, transferred_by_id: :integer, + transferred_from: :map, transferred_from_id: :integer, + transferred_to: :map, transferred_to_id: :integer, unenrolled: :boolean, unenrolled_at: {:string, "date-time"}, + unenrolled_by: :map, unenrolled_by_id: :integer, + user: :map, user_archived: :boolean, user_id: :integer ] diff --git a/lib/cyphi/level_history.ex b/lib/cyphi/level_history.ex index 1f4b911..ce68606 100644 --- a/lib/cyphi/level_history.ex +++ b/lib/cyphi/level_history.ex @@ -3,15 +3,20 @@ defmodule Cyphi.LevelHistory do Provides struct and type for a LevelHistory """ - @type t :: %__MODULE__{awarded_at: DateTime.t() | nil, id: integer | nil, level_id: integer} + @type t :: %__MODULE__{ + awarded_at: DateTime.t() | nil, + id: integer | nil, + level: map | nil, + level_id: integer + } - defstruct [:awarded_at, :id, :level_id] + defstruct [:awarded_at, :id, :level, :level_id] @doc false @spec __fields__(atom) :: keyword def __fields__(type \\ :t) def __fields__(:t) do - [awarded_at: {:string, "date-time"}, id: :integer, level_id: :integer] + [awarded_at: {:string, "date-time"}, id: :integer, level: :map, level_id: :integer] end end diff --git a/lib/cyphi/module.ex b/lib/cyphi/module.ex index ca506a6..1cfc255 100644 --- a/lib/cyphi/module.ex +++ b/lib/cyphi/module.ex @@ -6,6 +6,7 @@ defmodule Cyphi.Module do @type t :: %__MODULE__{ all_day: boolean | nil, begin_at: DateTime.t() | nil, + course: map | nil, course_id: integer | nil, description: String.t() | nil, end_at: DateTime.t() | nil, @@ -27,6 +28,7 @@ defmodule Cyphi.Module do defstruct [ :all_day, :begin_at, + :course, :course_id, :description, :end_at, @@ -53,6 +55,7 @@ defmodule Cyphi.Module do [ all_day: :boolean, begin_at: {:string, "date-time"}, + course: :map, course_id: :integer, description: :string, end_at: {:string, "date-time"}, diff --git a/lib/cyphi/order.ex b/lib/cyphi/order.ex index 0cf86aa..126aea7 100644 --- a/lib/cyphi/order.ex +++ b/lib/cyphi/order.ex @@ -12,12 +12,14 @@ defmodule Cyphi.Order do id: integer | nil, notes: String.t() | nil, order_number: String.t() | nil, + organization: map | nil, organization_id: integer | nil, tax_amount: number | nil, tax_type: String.t() | nil, total: String.t() | nil, total_without_bulk_discount: String.t() | nil, total_without_tax: String.t() | nil, + user: map | nil, user_id: integer, user_name: String.t() | nil, utm_campaign: String.t() | nil, @@ -26,6 +28,7 @@ defmodule Cyphi.Order do utm_source: String.t() | nil, utm_term: String.t() | nil, verified_at: DateTime.t() | nil, + verifier: map | nil, verifier_id: integer | nil } @@ -38,12 +41,14 @@ defmodule Cyphi.Order do :id, :notes, :order_number, + :organization, :organization_id, :tax_amount, :tax_type, :total, :total_without_bulk_discount, :total_without_tax, + :user, :user_id, :user_name, :utm_campaign, @@ -52,6 +57,7 @@ defmodule Cyphi.Order do :utm_source, :utm_term, :verified_at, + :verifier, :verifier_id ] @@ -69,12 +75,14 @@ defmodule Cyphi.Order do id: :integer, notes: :string, order_number: :string, + organization: :map, organization_id: :integer, tax_amount: {:number, "float"}, tax_type: :string, total: :string, total_without_bulk_discount: :string, total_without_tax: :string, + user: :map, user_id: :integer, user_name: :string, utm_campaign: :string, @@ -83,6 +91,7 @@ defmodule Cyphi.Order do utm_source: :string, utm_term: :string, verified_at: {:string, "date-time"}, + verifier: :map, verifier_id: :integer ] end diff --git a/lib/cyphi/organization.ex b/lib/cyphi/organization.ex index 5a1ec90..9f92766 100644 --- a/lib/cyphi/organization.ex +++ b/lib/cyphi/organization.ex @@ -9,11 +9,12 @@ defmodule Cyphi.Organization do id: integer | nil, internal: boolean | nil, name: String.t() | nil, + parent: map | nil, parent_id: integer | nil, tags: [String.t()] | nil } - defstruct [:custom_fields, :description, :id, :internal, :name, :parent_id, :tags] + defstruct [:custom_fields, :description, :id, :internal, :name, :parent, :parent_id, :tags] @doc false @spec __fields__(atom) :: keyword @@ -26,6 +27,7 @@ defmodule Cyphi.Organization do id: :integer, internal: :boolean, name: :string, + parent: :map, parent_id: :integer, tags: [:string] ] diff --git a/lib/cyphi/player.ex b/lib/cyphi/player.ex index 7859506..da107c0 100644 --- a/lib/cyphi/player.ex +++ b/lib/cyphi/player.ex @@ -7,16 +7,23 @@ defmodule Cyphi.Player do id: integer | nil, level: Cyphi.PlayerLevel.t() | nil, points: integer | nil, + user: map | nil, user_id: integer } - defstruct [:id, :level, :points, :user_id] + defstruct [:id, :level, :points, :user, :user_id] @doc false @spec __fields__(atom) :: keyword def __fields__(type \\ :t) def __fields__(:t) do - [id: :integer, level: {Cyphi.PlayerLevel, :t}, points: :integer, user_id: :integer] + [ + id: :integer, + level: {Cyphi.PlayerLevel, :t}, + points: :integer, + user: :map, + user_id: :integer + ] end end diff --git a/lib/cyphi/scorm_answer.ex b/lib/cyphi/scorm_answer.ex index fe4853e..5c9e00e 100644 --- a/lib/cyphi/scorm_answer.ex +++ b/lib/cyphi/scorm_answer.ex @@ -5,16 +5,29 @@ defmodule Cyphi.ScormAnswer do @type t :: %__MODULE__{ correct_answer: String.t() | nil, + grade: map | nil, grade_id: integer | nil, id: integer | nil, position: integer | nil, response: String.t() | nil, result: String.t() | nil, + user: map | nil, user_id: integer | nil, weighting: number | nil } - defstruct [:correct_answer, :grade_id, :id, :position, :response, :result, :user_id, :weighting] + defstruct [ + :correct_answer, + :grade, + :grade_id, + :id, + :position, + :response, + :result, + :user, + :user_id, + :weighting + ] @doc false @spec __fields__(atom) :: keyword @@ -23,11 +36,13 @@ defmodule Cyphi.ScormAnswer do def __fields__(:t) do [ correct_answer: :string, + grade: :map, grade_id: :integer, id: :integer, position: :integer, response: :string, result: :string, + user: :map, user_id: :integer, weighting: {:number, "float"} ] diff --git a/lib/cyphi/section.ex b/lib/cyphi/section.ex index 1232ab2..0893d0b 100644 --- a/lib/cyphi/section.ex +++ b/lib/cyphi/section.ex @@ -7,11 +7,13 @@ defmodule Cyphi.Section do id: integer | nil, instructions: String.t() | nil, level: integer | nil, + module: map | nil, module_id: integer | nil, name: String.t() | nil, optional_for_completion: boolean | nil, personalized: boolean | nil, position: integer | nil, + referenced_course: map | nil, referenced_course_id: integer | nil, type: String.t() | nil } @@ -20,11 +22,13 @@ defmodule Cyphi.Section do :id, :instructions, :level, + :module, :module_id, :name, :optional_for_completion, :personalized, :position, + :referenced_course, :referenced_course_id, :type ] @@ -38,11 +42,13 @@ defmodule Cyphi.Section do id: :integer, instructions: :string, level: :integer, + module: :map, module_id: :integer, name: :string, optional_for_completion: :boolean, personalized: :boolean, position: :integer, + referenced_course: :map, referenced_course_id: :integer, type: :string ] diff --git a/lib/cyphi/session.ex b/lib/cyphi/session.ex index 9433c0c..3b783d9 100644 --- a/lib/cyphi/session.ex +++ b/lib/cyphi/session.ex @@ -8,10 +8,11 @@ defmodule Cyphi.Session do ip_address: String.t() | nil, login_at: DateTime.t() | nil, logout_at: DateTime.t() | nil, + user: map | nil, user_id: integer | nil } - defstruct [:id, :ip_address, :login_at, :logout_at, :user_id] + defstruct [:id, :ip_address, :login_at, :logout_at, :user, :user_id] @doc false @spec __fields__(atom) :: keyword @@ -23,6 +24,7 @@ defmodule Cyphi.Session do ip_address: :string, login_at: {:string, "date-time"}, logout_at: {:string, "date-time"}, + user: :map, user_id: :integer ] end diff --git a/lib/cyphi/user.ex b/lib/cyphi/user.ex index 2bde3ce..9d50f1d 100644 --- a/lib/cyphi/user.ex +++ b/lib/cyphi/user.ex @@ -5,9 +5,11 @@ defmodule Cyphi.User do @type t :: %__MODULE__{ about: String.t() | nil, + added_by: map | nil, added_by_id: integer | nil, archived: boolean | nil, archived_at: DateTime.t() | nil, + archiver: map | nil, archiver_id: integer | nil, birthdate: Date.t() | nil, city: String.t() | nil, @@ -20,6 +22,7 @@ defmodule Cyphi.User do gender: String.t() | nil, id: integer | nil, instructorID: String.t() | nil, + job_title: map | nil, job_title_id: integer | nil, job_title_name: String.t() | nil, joined_at: DateTime.t() | nil, @@ -28,10 +31,12 @@ defmodule Cyphi.User do last_login_ip: String.t() | nil, last_name: String.t() | nil, learnerID: String.t() | nil, + manager: map | nil, manager_id: integer | nil, manager_name: String.t() | nil, mobile_phone: String.t() | nil, nick_name: String.t() | nil, + organization: map | nil, organization_id: integer | nil, organization_name: String.t() | nil, password: String.t() | nil, @@ -55,9 +60,11 @@ defmodule Cyphi.User do # credo:disable-for-next-line defstruct [ :about, + :added_by, :added_by_id, :archived, :archived_at, + :archiver, :archiver_id, :birthdate, :city, @@ -70,6 +77,7 @@ defmodule Cyphi.User do :gender, :id, :instructorID, + :job_title, :job_title_id, :job_title_name, :joined_at, @@ -78,10 +86,12 @@ defmodule Cyphi.User do :last_login_ip, :last_name, :learnerID, + :manager, :manager_id, :manager_name, :mobile_phone, :nick_name, + :organization, :organization_id, :organization_name, :password, @@ -109,9 +119,11 @@ defmodule Cyphi.User do def __fields__(:t) do [ about: :string, + added_by: :map, added_by_id: :integer, archived: :boolean, archived_at: {:string, "date-time"}, + archiver: :map, archiver_id: :integer, birthdate: {:string, "date"}, city: :string, @@ -124,6 +136,7 @@ defmodule Cyphi.User do gender: :string, id: :integer, instructorID: :string, + job_title: :map, job_title_id: :integer, job_title_name: :string, joined_at: {:string, "date-time"}, @@ -132,10 +145,12 @@ defmodule Cyphi.User do last_login_ip: :string, last_name: :string, learnerID: :string, + manager: :map, manager_id: :integer, manager_name: :string, mobile_phone: :string, nick_name: :string, + organization: :map, organization_id: :integer, organization_name: :string, password: :string, diff --git a/lib/cyphi/visit.ex b/lib/cyphi/visit.ex index 36e8fe2..e24ca4f 100644 --- a/lib/cyphi/visit.ex +++ b/lib/cyphi/visit.ex @@ -12,6 +12,7 @@ defmodule Cyphi.Visit do started_at: DateTime.t() | nil, status: String.t() | nil, time_spent: integer | nil, + user: map | nil, user_agent: String.t() | nil, user_id: integer | nil, user_ip: String.t() | nil, @@ -27,6 +28,7 @@ defmodule Cyphi.Visit do :started_at, :status, :time_spent, + :user, :user_agent, :user_id, :user_ip, @@ -47,6 +49,7 @@ defmodule Cyphi.Visit do started_at: {:string, "date-time"}, status: :string, time_spent: :integer, + user: :map, user_agent: :string, user_id: :integer, user_ip: :string, diff --git a/mix.exs b/mix.exs index 9cc77fe..8d67143 100644 --- a/mix.exs +++ b/mix.exs @@ -36,7 +36,8 @@ defmodule Cyphi.MixProject do end # Specifies which paths to compile per environment. - defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(:dev), do: ["lib", "codegen"] + defp elixirc_paths(:test), do: ["lib", "codegen", "test/support"] defp elixirc_paths(_), do: ["lib"] # Run "mix help deps" to learn about dependencies. @@ -46,7 +47,7 @@ defmodule Cyphi.MixProject do {:ex_doc, "~> 0.39", only: :dev}, {:excoveralls, "~> 0.18", only: :test}, {:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false}, - {:oapi_generator, "~> 0.4", only: :dev, runtime: false}, + {:oapi_generator, "~> 0.4", only: [:dev, :test], runtime: false}, {:req, "~> 0.5"}, {:sobelow, "~> 0.13", only: [:dev, :test], runtime: false} ] diff --git a/test/cyphi/client/response_test.exs b/test/cyphi/client/response_test.exs index 5ec1af3..f3059bf 100644 --- a/test/cyphi/client/response_test.exs +++ b/test/cyphi/client/response_test.exs @@ -66,6 +66,67 @@ defmodule Cyphi.Client.ResponseTest do end end + describe "included relationships" do + test "preserves included objects and their nested keys as raw maps" do + organization = %{"id" => 7, "custom" => %{"active" => true}} + body = %{"id" => 1, "organization" => organization, "manager" => %{"id" => 2}} + req = %Req.Response{status: 200, body: body} + + assert {:ok, user} = Response.decode({:ok, req}, %{response: [{200, {User, :t}}]}) + assert user == %User{id: 1, organization: organization, manager: %{"id" => 2}} + end + + test "absent and null relationships default to nil" do + for body <- [%{"id" => 1}, %{"id" => 1, "organization" => nil}] do + req = %Req.Response{status: 200, body: body} + + assert {:ok, %User{organization: nil, manager: nil} = user} = + Response.decode({:ok, req}, %{response: [{200, {User, :t}}]}) + + assert user == %User{id: 1} + end + end + + test "preserves relations in list responses without changing scalar casting" do + body = [ + %{ + "id" => 1, + "user_id" => 2, + "user" => %{"id" => 2}, + "progress" => %{"percent" => 50} + }, + %{"id" => 3} + ] + + req = %Req.Response{status: 200, body: body} + + assert {:ok, [first, second]} = + Response.decode({:ok, req}, %{response: [{200, [{Cyphi.Learner, :t}]}]}) + + assert first == %Cyphi.Learner{ + id: 1, + user_id: 2, + user: %{"id" => 2}, + progress: %{"percent" => 50} + } + + assert second == %Cyphi.Learner{id: 3} + end + + test "does not change decoding of previously declared nested fields" do + metadata = %{"custom" => "value"} + organization = %{"id" => 2} + body = %{"id" => 1, "metadata" => metadata, "organization" => organization} + req = %Req.Response{status: 200, body: body} + + assert {:ok, course} = + Response.decode({:ok, req}, %{response: [{200, {Cyphi.Course, :t}}]}) + + assert course.metadata == metadata + assert course.organization == organization + end + end + describe "decode/2 with Raw Body (200/202/204)" do test "returns raw body for 202 Accepted (async jobs)" do req = %Req.Response{status: 202, body: "Job Queued"} diff --git a/test/cyphi/generator/renderer_test.exs b/test/cyphi/generator/renderer_test.exs new file mode 100644 index 0000000..83cc4a6 --- /dev/null +++ b/test/cyphi/generator/renderer_test.exs @@ -0,0 +1,90 @@ +defmodule Cyphi.Generator.RendererTest do + use ExUnit.Case, async: true + + alias Cyphi.Generator.Renderer + alias OpenAPI.Processor.Schema + alias OpenAPI.Processor.Schema.Field + alias OpenAPI.Renderer.State + + setup do + {:ok, state: %State{implementation: Renderer, profile: :renderer_test}} + end + + test "adds optional relations consistently to types, structs, and field metadata", %{ + state: state + } do + schemas = [%Schema{module_name: User, type_name: :t, output_format: :struct}] + + types = Renderer.render_schema_types(state, schemas) |> Macro.to_string() + struct_ast = Renderer.render_schema_struct(state, schemas) |> Macro.to_string() + fields = Renderer.render_schema_field_function(state, schemas) |> Macro.to_string() + + assert types =~ "organization: map | nil" + assert struct_ast =~ ":organization" + assert fields =~ "organization: :map" + end + + test "preserves upstream field definitions and does not duplicate relations", %{state: state} do + schemas = [ + %Schema{ + module_name: User, + type_name: :t, + output_format: :struct, + fields: [%Field{name: "organization", type: :string, required: true}] + } + ] + + for callback <- [:render_schema_types, :render_schema_struct, :render_schema_field_function] do + rendered = apply(Renderer, callback, [state, schemas]) |> Macro.to_string() + assert length(Regex.scan(~r/organization/, rendered)) == 1 + end + + types = Renderer.render_schema_types(state, schemas) |> Macro.to_string() + fields = Renderer.render_schema_field_function(state, schemas) |> Macro.to_string() + assert types =~ "organization: String.t()" + refute types =~ "organization: map" + assert fields =~ "organization: :string" + end + + test "keeps the existing Answer.grade type while adding missing relations" do + assert Cyphi.Answer.__fields__()[:grade] == :string + assert Cyphi.Answer.__fields__()[:question] == :map + assert Cyphi.Answer.__fields__()[:user] == :map + assert Cyphi.Player.__fields__()[:level] == {Cyphi.PlayerLevel, :t} + end + + test "preserves the large-struct lint exemption during regeneration", %{state: state} do + schema = %Schema{ + module_name: Large, + type_name: :t, + output_format: :struct, + fields: Enum.map(1..32, &%Field{name: "field_#{&1}", type: :string}) + } + + file = %OpenAPI.Renderer.File{ + module: Large, + schemas: [schema], + ast: quote(do: defmodule(Large, do: defstruct([]))) + } + + assert Renderer.format(state, file) =~ "# credo:disable-for-next-line\n defstruct" + end + + test "leaves unrelated schemas and non-struct request types unchanged", %{state: state} do + for {module, format} <- [{Unrelated, :struct}, {User, :typed_map}] do + schemas = [ + %Schema{ + module_name: module, + type_name: :t, + output_format: format, + fields: [%Field{name: "id", type: :integer}] + } + ] + + for callback <- [:render_schema_types, :render_schema_struct, :render_schema_field_function] do + assert apply(Renderer, callback, [state, schemas]) == + apply(OpenAPI.Renderer, callback, [state, schemas]) + end + end + end +end From ca41f4e8c62dac24e5a13519cb5d4120f22b0b1d Mon Sep 17 00:00:00 2001 From: javierg Date: Thu, 24 Sep 2026 09:04:40 -0700 Subject: [PATCH 4/4] fix vuln --- mix.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mix.lock b/mix.lock index cfce4eb..43f2963 100644 --- a/mix.lock +++ b/mix.lock @@ -5,22 +5,22 @@ "ex_doc": {:hex, :ex_doc, "0.39.3", "519c6bc7e84a2918b737aec7ef48b96aa4698342927d080437f61395d361dcee", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "0590955cf7ad3b625780ee1c1ea627c28a78948c6c0a9b0322bd976a079996e1"}, "excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, - "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, - "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, - "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "finch": {:hex, :finch, "0.23.0", "e3f9287ac25a8832f848b144c2b57346aac65b205e2e0629a52adfe6507fd837", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80e58d3f936f57e3fdf404f83a3642897ae6d9fb642934e46da4d8fe761b99d5"}, + "hpax": {:hex, :hpax, "1.1.0", "782931867cc23217c68fb5f68fe1a11f5e7544c7fda82c8a7019a5df5a4a1cdf", [:mix], [], "hexpm", "0b8d0f05832f55571d65ac720f79bf8994138ffbb133209dc4685eae0ad456a8"}, + "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, - "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, + "mint": {:hex, :mint, "1.10.1", "c53e70867cf74017716884d8d33e0742b08b32e9cdb0031cbc69a429dc5555e3", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "0ba2a904605ed8406393444fb8b3356dc58eb59ee6c7fb94ac3f015e1be129e8"}, "mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "oapi_generator": {:hex, :oapi_generator, "0.4.0", "6e4bdee4c87549e223a8adb046276aa924e6c76756909978d05ae61305858791", [:mix], [{:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "381d8fce6d1dd16a9407b620bdc31f370a9536590a066f3dd47becaec9ce9b3e"}, - "req": {:hex, :req, "0.5.16", "99ba6a36b014458e52a8b9a0543bfa752cb0344b2a9d756651db1281d4ba4450", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "974a7a27982b9b791df84e8f6687d21483795882a7840e8309abdbe08bb06f09"}, + "req": {:hex, :req, "0.7.4", "23e9ffec17de032a46a4b15ed65c09793893bf4a7c680f4bbf6227fce6bdf74d", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "4b192d63253e8dcc6221ef992ea9ebef7d3555166e8423aa5b553e86bc3c69a2"}, "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, - "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, "yaml_elixir": {:hex, :yaml_elixir, "2.12.0", "30343ff5018637a64b1b7de1ed2a3ca03bc641410c1f311a4dbdc1ffbbf449c7", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "ca6bacae7bac917a7155dca0ab6149088aa7bc800c94d0fe18c5238f53b313c6"}, }