commit bd1f61de42b733a35982299c5247a14d853ea914
parent 4e8240dbd809a3d04f26cdae2554c3c9325bad17
Author: srfsh <dev@srf.sh>
Date: Tue, 16 Aug 2022 13:22:57 +0300
Zenflows{Test,}.VF.Intent: init
Diffstat:
6 files changed, 1232 insertions(+), 0 deletions(-)
diff --git a/src/zenflows/vf/intent.ex b/src/zenflows/vf/intent.ex
@@ -28,6 +28,7 @@ alias Zenflows.VF.{
EconomicResource,
Measure,
Process,
+ ProposedIntent,
ResourceSpecification,
SpatialThing,
Unit,
@@ -57,6 +58,8 @@ alias Zenflows.VF.{
note: String.t() | nil,
# in_scope_of:
agreed_in: String.t() | nil,
+
+ published_in: [ProposedIntent.t()],
}
schema "vf_intent" do
@@ -90,6 +93,8 @@ schema "vf_intent" do
# field :in_scope_of
field :agreed_in, :string
timestamps()
+
+ has_many :published_in, ProposedIntent, foreign_key: :publishes_id
end
@reqr [:action_id]
diff --git a/src/zenflows/vf/intent/domain.ex b/src/zenflows/vf/intent/domain.ex
@@ -0,0 +1,139 @@
+# Zenflows is designed to implement the Valueflows vocabulary,
+# written and maintained by srfsh <info@dyne.org>.
+# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+defmodule Zenflows.VF.Intent.Domain do
+@moduledoc "Domain logic of Intents."
+
+alias Ecto.Multi
+alias Zenflows.DB.Repo
+alias Zenflows.VF.{
+ Action,
+ Intent,
+ Measure,
+}
+
+@typep repo() :: Ecto.Repo.t()
+@typep chgset() :: Ecto.Changeset.t()
+@typep id() :: Zenflows.DB.Schema.id()
+@typep params() :: Zenflows.DB.Schema.params()
+
+@spec one(repo(), id()) :: {:ok, Intent.t()} | {:error, String.t()}
+def one(repo \\ Repo, id) do
+ one_by(repo, id: id)
+end
+
+@spec one_by(repo(), map() | Keyword.t())
+ :: {:ok, Intent.t()} | {:error, String.t()}
+def one_by(repo \\ Repo, clauses) do
+ case repo.get_by(Intent, clauses) do
+ nil -> {:error, "not found"}
+ found -> {:ok, found}
+ end
+end
+
+@spec create(params()) :: {:ok, Intent.t()} | {:error, chgset()}
+def create(params) do
+ Multi.new()
+ |> Multi.insert(:insert, Intent.chgset(params))
+ |> Repo.transaction()
+ |> case do
+ {:ok, %{insert: i}} -> {:ok, i}
+ {:error, _, cset, _} -> {:error, cset}
+ end
+end
+
+@spec update(id(), params()) ::
+ {:ok, Intent.t()} | {:error, String.t() | chgset()}
+def update(id, params) do
+ Multi.new()
+ |> Multi.put(:id, id)
+ |> Multi.run(:one, &one_by/2)
+ |> Multi.update(:update, &Intent.chgset(&1.one, params))
+ |> Repo.transaction()
+ |> case do
+ {:ok, %{update: i}} -> {:ok, i}
+ {:error, _, msg_or_cset, _} -> {:error, msg_or_cset}
+ end
+end
+
+@spec delete(id()) :: {:ok, Intent.t()} | {:error, String.t() | chgset()}
+def delete(id) do
+ Multi.new()
+ |> Multi.put(:id, id)
+ |> Multi.run(:one, &one_by/2)
+ |> Multi.delete(:delete, &(&1.one))
+ |> Repo.transaction()
+ |> case do
+ {:ok, %{delete: i}} -> {:ok, i}
+ {:error, _, msg_or_cset, _} -> {:error, msg_or_cset}
+ end
+end
+
+@spec preload(Intent.t(), :action | :input_of | :output_of
+ | :provider | :receiver
+ | :resource_inventoried_as | :resource_conforms_to
+ | :resource_quantity | :effort_quantity | :available_quantity
+ | :at_location | :published_in)
+ :: Intent.t()
+def preload(int, :action) do
+ Action.preload(int, :action)
+end
+
+def preload(int, :input_of) do
+ Repo.preload(int, :input_of)
+end
+
+def preload(int, :output_of) do
+ Repo.preload(int, :output_of)
+end
+
+def preload(int, :provider) do
+ Repo.preload(int, :provider)
+end
+
+def preload(int, :receiver) do
+ Repo.preload(int, :receiver)
+end
+
+def preload(int, :resource_inventoried_as) do
+ Repo.preload(int, :resource_inventoried_as)
+end
+
+def preload(int, :resource_conforms_to) do
+ Repo.preload(int, :resource_conforms_to)
+end
+
+def preload(int, :resource_quantity) do
+ Measure.preload(int, :resource_quantity)
+end
+
+def preload(int, :effort_quantity) do
+ Measure.preload(int, :effort_quantity)
+end
+
+def preload(int, :available_quantity) do
+ Measure.preload(int, :available_quantity)
+end
+
+def preload(int, :at_location) do
+ Repo.preload(int, :at_location)
+end
+
+def preload(int, :published_in) do
+ Repo.preload(int, :published_in)
+end
+end
diff --git a/src/zenflows/vf/intent/resolv.ex b/src/zenflows/vf/intent/resolv.ex
@@ -0,0 +1,114 @@
+# Zenflows is designed to implement the Valueflows vocabulary,
+# written and maintained by srfsh <info@dyne.org>.
+# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+defmodule Zenflows.VF.Intent.Resolv do
+@moduledoc "Resolvers of Intent."
+
+alias Zenflows.VF.Intent.Domain
+
+def action(int, _, _) do
+ int = Domain.preload(int, :action)
+ {:ok, int.action}
+end
+
+def input_of(int, _, _) do
+ int = Domain.preload(int, :input_of)
+ {:ok, int.input_of}
+end
+
+def output_of(int, _, _) do
+ int = Domain.preload(int, :output_of)
+ {:ok, int.output_of}
+end
+
+def provider(int, _, _) do
+ int = Domain.preload(int, :provider)
+ {:ok, int.provider}
+end
+
+def receiver(int, _, _) do
+ int = Domain.preload(int, :receiver)
+ {:ok, int.receiver}
+end
+
+def resource_inventoried_as(int, _, _) do
+ int = Domain.preload(int, :resource_inventoried_as)
+ {:ok, int.resource_inventoried_as}
+end
+
+def resource_conforms_to(int, _, _) do
+ int = Domain.preload(int, :resource_conforms_to)
+ {:ok, int.resource_conforms_to}
+end
+
+def resource_quantity(int, _, _) do
+ int = Domain.preload(int, :resource_quantity)
+ {:ok, int.resource_quantity}
+end
+
+def effort_quantity(int, _, _) do
+ int = Domain.preload(int, :effort_quantity)
+ {:ok, int.effort_quantity}
+end
+
+def available_quantity(int, _, _) do
+ int = Domain.preload(int, :available_quantity)
+ {:ok, int.available_quantity}
+end
+
+def at_location(int, _, _) do
+ int = Domain.preload(int, :at_location)
+ {:ok, int.at_location}
+end
+
+def published_in(int, _, _) do
+ int = Domain.preload(int, :published_in)
+ {:ok, int.published_in}
+end
+
+def intent(%{id: id}, _) do
+ Domain.one(id)
+end
+
+def intents(_, _) do
+ {:ok, %{
+ edges: [],
+ page_info: %{
+ has_previous_page: false,
+ has_next_page: false,
+ },
+ }}
+end
+
+def create_intent(%{intent: params}, _) do
+ with {:ok, int} <- Domain.create(params) do
+ {:ok, %{intent: int}}
+ end
+end
+
+def update_intent(%{intent: %{id: id} = params}, _) do
+ with {:ok, int} <- Domain.update(id, params) do
+ {:ok, %{intent: int}}
+ end
+end
+
+def delete_intent(%{id: id}, _) do
+ with {:ok, _} <- Domain.delete(id) do
+ {:ok, true}
+ end
+end
+end
diff --git a/src/zenflows/vf/intent/type.ex b/src/zenflows/vf/intent/type.ex
@@ -0,0 +1,355 @@
+# Zenflows is designed to implement the Valueflows vocabulary,
+# written and maintained by srfsh <info@dyne.org>.
+# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+defmodule Zenflows.VF.Intent.Type do
+@moduledoc "GraphQL types of Intents."
+
+use Absinthe.Schema.Notation
+
+alias Zenflows.VF.Intent.Resolv
+
+@action """
+Relates an intent to a verb, such as consume, produce, work, improve, etc.
+"""
+@action_id "(`Action`) #{@action}"
+@input_of "Defines the process to which this intent is an input."
+@input_of_id "(`Process`) #{@input_of}"
+@output_of "Defines the process to which this intent is an output."
+@output_of_id "(`Process`) #{@output_of}"
+@provider """
+The economic agent from whom the intent is initiated. This implies that
+the intent is an offer.
+"""
+@provider_id "(`Agent`) #{@provider}"
+@receiver """
+The economic agent whom the intent is for. This implies that the intent
+is a request.
+"""
+@receiver_id "(`Agent`) #{@receiver}"
+@resource_inventoried_as """
+When a specific `EconomicResource` is known which can service the
+`Intent`, this defines that resource.
+"""
+@resource_inventoried_as_id "(`EconomicResource`) #{@resource_inventoried_as}"
+@resource_conforms_to """
+The primary resource specification or definition of an existing or
+potential economic resource. A resource will have only one, as this
+specifies exactly what the resource is.
+"""
+@resource_conforms_to_id "(`ResourceSpecification`) #{@resource_conforms_to}"
+@resource_classified_as """
+References a concept in a common taxonomy or other classification scheme
+for purposes of categorization or grouping.
+"""
+@resource_quantity """
+The amount and unit of the economic resource counted or inventoried. This
+is the quantity that could be used to increment or decrement a resource,
+depending on the type of resource and resource effect of action.
+"""
+@effort_quantity """
+The amount and unit of the work or use or citation effort-based action.
+This is often a time duration, but also could be cycle counts or other
+measures of effort or usefulness.
+"""
+@available_quantity "The total quantity of the offered resource available."
+@has_beginning "The planned beginning of the intent."
+@has_end "The planned end of the intent."
+@has_point_in_time """
+The planned date/time for the intent. Can be used instead of beginning
+and end.
+"""
+@due "The time something is expected to be complete."
+@finished """
+The intent is complete or not. This is irrespective of if the original
+goal has been met, and indicates that no more will be done.
+"""
+@at_location "The place where an intent would occur. Usually mappable."
+@at_location_id "(`SpatialThing`) #{@at_location}"
+@image """
+The base64-encoded image binary relevant to the intent, such as a photo.
+"""
+@name """
+An informal or formal textual identifier for an intent. Does not imply
+uniqueness.
+"""
+@note "A textual description or comment."
+@agreed_in """
+Reference to an agreement between agents which specifies the rules or
+policies or calculations which govern this intent.
+"""
+@deletable "The intent can be safely deleted, has no dependent information."
+
+@desc """
+A planned economic flow which has not been committed to, which can lead
+to EconomicEvents (sometimes through Commitments).
+"""
+object :intent do
+ field :id, non_null(:id)
+
+ @desc @action
+ field :action, non_null(:action), resolve: &Resolv.action/3
+
+ @desc @input_of
+ field :input_of, :process, resolve: &Resolv.input_of/3
+
+ @desc @output_of
+ field :output_of, :process, resolve: &Resolv.output_of/3
+
+ @desc @provider
+ field :provider, :agent, resolve: &Resolv.provider/3
+
+ @desc @receiver
+ field :receiver, :agent, resolve: &Resolv.receiver/3
+
+ @desc @resource_inventoried_as
+ field :resource_inventoried_as, :economic_resource,
+ resolve: &Resolv.resource_inventoried_as/3
+
+ @desc @resource_conforms_to
+ field :resource_conforms_to, :economic_resource,
+ resolve: &Resolv.resource_conforms_to/3
+
+ @desc @resource_classified_as
+ field :resource_classified_as, list_of(non_null(:uri))
+
+ @desc @resource_quantity
+ field :resource_quantity, :measure,
+ resolve: &Resolv.resource_quantity/3
+
+ @desc @effort_quantity
+ field :effort_quantity, :measure,
+ resolve: &Resolv.effort_quantity/3
+
+ @desc @available_quantity
+ field :available_quantity, :measure,
+ resolve: &Resolv.available_quantity/3
+
+ @desc @has_beginning
+ field :has_beginning, :datetime
+
+ @desc @has_end
+ field :has_end, :datetime
+
+ @desc @has_point_in_time
+ field :has_point_in_time, :datetime
+
+ @desc @due
+ field :due, :datetime
+
+ @desc @finished
+ field :finished, non_null(:boolean)
+
+ @desc @at_location
+ field :at_location, :spatial_thing, resolve: &Resolv.at_location/3
+
+ @desc @image
+ field :image, :base64
+
+ @desc @name
+ field :name, :string
+
+ @desc @note
+ field :note, :string
+
+ @desc @agreed_in
+ field :agreed_in, :uri
+
+ @desc @deletable
+ field :deletable, non_null(:boolean)
+
+ field :published_in, list_of(non_null(:proposed_intent)),
+ resolve: &Resolv.published_in/3
+end
+
+object :intent_response do
+ field :intent, non_null(:intent)
+end
+
+object :intent_edge do
+ field :cursor, non_null(:string)
+ field :node, non_null(:intent)
+end
+
+object :intent_connection do
+ field :page_info, non_null(:page_info)
+ field :edges, non_null(list_of(non_null(:intent_edge)))
+end
+
+input_object :intent_create_params do
+ @desc @action_id
+ field :action_id, non_null(:string), name: "action"
+
+ @desc @input_of_id
+ field :input_of_id, :id, name: "input_of"
+
+ @desc @output_of_id
+ field :output_of_id, :id, name: "output_of"
+
+ @desc @provider_id
+ field :provider_id, :id, name: "provider"
+
+ @desc @receiver_id
+ field :receiver_id, :id, name: "receiver"
+
+ @desc @resource_inventoried_as_id
+ field :resource_inventoried_as_id, :id, name: "resource_inventoried_as"
+
+ @desc @resource_conforms_to_id
+ field :resource_conforms_to_id, :id, name: "resource_conforms_to"
+
+ @desc @resource_classified_as
+ field :resource_classified_as, list_of(non_null(:uri))
+
+ @desc @resource_quantity
+ field :resource_quantity, :imeasure
+
+ @desc @effort_quantity
+ field :effort_quantity, :imeasure
+
+ @desc @available_quantity
+ field :available_quantity, :imeasure
+
+ @desc @has_beginning
+ field :has_beginning, :datetime
+
+ @desc @has_end
+ field :has_end, :datetime
+
+ @desc @has_point_in_time
+ field :has_point_in_time, :datetime
+
+ @desc @due
+ field :due, :datetime
+
+ @desc @finished
+ field :finished, :boolean
+
+ @desc @at_location_id
+ field :at_location_id, :id, name: "at_location"
+
+ @desc @image
+ field :image, :base64
+
+ @desc @name
+ field :name, :string
+
+ @desc @note
+ field :note, :string
+
+ @desc @agreed_in
+ field :agreed_in, :uri
+end
+
+input_object :intent_update_params do
+ field :id, non_null(:id)
+
+ @desc @action_id
+ field :action_id, :string, name: "action"
+
+ @desc @input_of_id
+ field :input_of_id, :id, name: "input_of"
+
+ @desc @output_of_id
+ field :output_of_id, :id, name: "output_of"
+
+ @desc @provider_id
+ field :provider_id, :id, name: "provider"
+
+ @desc @receiver_id
+ field :receiver_id, :id, name: "receiver"
+
+ @desc @resource_inventoried_as_id
+ field :resource_inventoried_as_id, :id, name: "resource_inventoried_as"
+
+ @desc @resource_conforms_to_id
+ field :resource_conforms_to_id, :id, name: "resource_conforms_to"
+
+ @desc @resource_classified_as
+ field :resource_classified_as, list_of(non_null(:uri))
+
+ @desc @resource_quantity
+ field :resource_quantity, :imeasure
+
+ @desc @effort_quantity
+ field :effort_quantity, :imeasure
+
+ @desc @available_quantity
+ field :available_quantity, :imeasure
+
+ @desc @has_beginning
+ field :has_beginning, :datetime
+
+ @desc @has_end
+ field :has_end, :datetime
+
+ @desc @has_point_in_time
+ field :has_point_in_time, :datetime
+
+ @desc @due
+ field :due, :datetime
+
+ @desc @finished
+ field :finished, :boolean
+
+ @desc @at_location_id
+ field :at_location_id, :id, name: "at_location"
+
+ @desc @image
+ field :image, :base64
+
+ @desc @name
+ field :name, :string
+
+ @desc @note
+ field :note, :string
+
+ @desc @agreed_in
+ field :agreed_in, :uri
+end
+
+object :query_intent do
+ field :intent, :intent do
+ arg :id, non_null(:id)
+ resolve &Resolv.intent/2
+ end
+
+ field :intents, non_null(:intent_connection) do
+ arg :first, :integer
+ arg :after, :string
+ arg :last, :integer
+ arg :before, :string
+ resolve &Resolv.intents/2
+ end
+end
+
+object :mutation_intent do
+ field :create_intent, non_null(:intent_response) do
+ arg :intent, non_null(:intent_create_params)
+ resolve &Resolv.create_intent/2
+ end
+
+ field :update_intent, non_null(:intent_response) do
+ arg :intent, non_null(:intent_update_params)
+ resolve &Resolv.update_intent/2
+ end
+
+ field :delete_intent, non_null(:boolean) do
+ arg :id, non_null(:id)
+ resolve &Resolv.delete_intent/2
+ end
+end
+end
diff --git a/test/vf/intent/domain.test.exs b/test/vf/intent/domain.test.exs
@@ -0,0 +1,320 @@
+# Zenflows is designed to implement the Valueflows vocabulary,
+# written and maintained by srfsh <info@dyne.org>.
+# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+defmodule ZenflowsTest.VF.Intent do
+use ZenflowsTest.Help.EctoCase, async: true
+
+alias Ecto.Changeset
+alias Zenflows.VF.{
+ Action,
+ Agent,
+ EconomicResource,
+ Intent,
+ Intent.Domain,
+ Measure,
+ Process,
+ ResourceSpecification,
+ SpatialThing,
+}
+
+setup do
+ %{
+ params: %{
+ action_id: Factory.build(:action_id),
+ input_of_id: Factory.insert!(:process).id,
+ output_of_id: Factory.insert!(:process).id,
+ provider_id: Factory.insert!(:agent).id,
+ receiver_id: Factory.insert!(:agent).id,
+ resource_inventoried_as_id: Factory.insert!(:economic_resource).id,
+ resource_conforms_to_id: Factory.insert!(:resource_specification).id,
+ resource_classified_as: Factory.uniq_list("uri"),
+ resource_quantity: %{
+ has_unit_id: Factory.insert!(:unit).id,
+ has_numerical_value: Factory.float(),
+ },
+ effort_quantity: %{
+ has_unit_id: Factory.insert!(:unit).id,
+ has_numerical_value: Factory.float(),
+ },
+ available_quantity: %{
+ has_unit_id: Factory.insert!(:unit).id,
+ has_numerical_value: Factory.float(),
+ },
+ has_beginning: Factory.now(),
+ has_end: Factory.now(),
+ has_point_in_time: Factory.now(),
+ due: Factory.now(),
+ finished: Factory.bool(),
+ at_location_id: Factory.insert!(:spatial_thing).id,
+ image: Factory.img(),
+ name: Factory.uniq("name"),
+ note: Factory.uniq("note"),
+ # in_scope_of_id:
+ agreed_in: Factory.uniq("uri"),
+ },
+ inserted: Factory.insert!(:intent),
+ id: Factory.id(),
+ }
+end
+
+describe "one/1" do
+ test "with good id: finds the Intent", %{inserted: %{id: id}} do
+ assert {:ok, %Intent{}} = Domain.one(id)
+ end
+
+ test "with bad id: doesn't find the Intent", %{id: id} do
+ assert {:error, "not found"} = Domain.one(id)
+ end
+end
+
+describe "create/1" do
+ test "with good params (with only :provider): creates an Intent", %{params: params} do
+ params = Map.put(params, :receiver_id, nil)
+ assert {:ok, %Intent{} = int} = Domain.create(params)
+
+ keys = ~w[
+ action_id
+ provider_id receiver_id
+ input_of_id output_of_id
+ resource_inventoried_as_id resource_conforms_to_id resource_classified_as
+ has_beginning has_end has_point_in_time due
+ finished image name note agreed_in at_location_id
+ ]a # in_scope_of_id
+ assert Map.take(int, keys) == Map.take(params, keys)
+
+ assert int.resource_quantity_has_unit_id == params.resource_quantity.has_unit_id
+ assert int.resource_quantity_has_numerical_value == params.resource_quantity.has_numerical_value
+ assert int.effort_quantity_has_unit_id == params.effort_quantity.has_unit_id
+ assert int.effort_quantity_has_numerical_value == params.effort_quantity.has_numerical_value
+ assert int.available_quantity_has_unit_id == params.available_quantity.has_unit_id
+ assert int.available_quantity_has_numerical_value == params.available_quantity.has_numerical_value
+ end
+
+ test "with good params (with only :receiver): creates an Intent", %{params: params} do
+ params = Map.put(params, :provider_id, nil)
+ assert {:ok, %Intent{} = int} = Domain.create(params)
+
+ keys = ~w[
+ action_id
+ provider_id receiver_id
+ input_of_id output_of_id
+ resource_inventoried_as_id resource_conforms_to_id resource_classified_as
+ has_beginning has_end has_point_in_time due
+ finished image name note agreed_in at_location_id
+ ]a # in_scope_of_id
+ assert Map.take(int, keys) == Map.take(params, keys)
+
+ assert int.resource_quantity_has_unit_id == params.resource_quantity.has_unit_id
+ assert int.resource_quantity_has_numerical_value == params.resource_quantity.has_numerical_value
+ assert int.effort_quantity_has_unit_id == params.effort_quantity.has_unit_id
+ assert int.effort_quantity_has_numerical_value == params.effort_quantity.has_numerical_value
+ assert int.available_quantity_has_unit_id == params.available_quantity.has_unit_id
+ assert int.available_quantity_has_numerical_value == params.available_quantity.has_numerical_value
+ end
+
+ test "with bad params (with both :provider and :receiver): doesn't create an Intent", %{params: params} do
+ assert {:error, %Changeset{errors: errs}} = Domain.create(params)
+ assert {:ok, _} = Keyword.fetch(errs, :provider_id)
+ assert {:ok, _} = Keyword.fetch(errs, :receiver_id)
+ end
+
+ test "with bad params: doesn't create an Intent" do
+ assert {:error, %Changeset{}} = Domain.create(%{})
+ end
+end
+
+describe "update/2" do
+ test "with good params (with only :provider): updates the Intent", %{params: params} do
+ params = Map.put(params, :receiver_id, nil)
+ old = Factory.insert!(:intent, %{receiver: nil, provider: Factory.build(:agent)})
+ assert {:ok, %Intent{} = new} = Domain.update(old.id, params)
+
+ keys = ~w[
+ action_id
+ provider_id receiver_id
+ input_of_id output_of_id
+ resource_inventoried_as_id resource_conforms_to_id resource_classified_as
+ has_beginning has_end has_point_in_time due
+ finished image name note agreed_in at_location_id
+ ]a # in_scope_of_id
+ assert Map.take(new, keys) == Map.take(params, keys)
+
+ assert new.resource_quantity_has_unit_id == params.resource_quantity.has_unit_id
+ assert new.resource_quantity_has_numerical_value == params.resource_quantity.has_numerical_value
+ assert new.effort_quantity_has_unit_id == params.effort_quantity.has_unit_id
+ assert new.effort_quantity_has_numerical_value == params.effort_quantity.has_numerical_value
+ assert new.available_quantity_has_unit_id == params.available_quantity.has_unit_id
+ assert new.available_quantity_has_numerical_value == params.available_quantity.has_numerical_value
+ end
+
+ test "with good params (with only :receiver): updates the Intent", %{params: params} do
+ params = Map.put(params, :provider_id, nil)
+ old = Factory.insert!(:intent, %{provider: nil, receiver: Factory.build(:agent)})
+ assert {:ok, %Intent{} = new} = Domain.update(old.id, params)
+
+ keys = ~w[
+ action_id
+ provider_id receiver_id
+ input_of_id output_of_id
+ resource_inventoried_as_id resource_conforms_to_id resource_classified_as
+ has_beginning has_end has_point_in_time due
+ finished image name note agreed_in at_location_id
+ ]a # in_scope_of_id
+ assert Map.take(new, keys) == Map.take(params, keys)
+
+ assert new.resource_quantity_has_unit_id == params.resource_quantity.has_unit_id
+ assert new.resource_quantity_has_numerical_value == params.resource_quantity.has_numerical_value
+ assert new.effort_quantity_has_unit_id == params.effort_quantity.has_unit_id
+ assert new.effort_quantity_has_numerical_value == params.effort_quantity.has_numerical_value
+ assert new.available_quantity_has_unit_id == params.available_quantity.has_unit_id
+ assert new.available_quantity_has_numerical_value == params.available_quantity.has_numerical_value
+ end
+
+ test "with bad params (with both :provider and :receiver): updates the Intent", %{params: params} do
+ int = Factory.insert!(:intent, %{provider: Factory.build(:agent), receiver: nil})
+ assert {:error, %Changeset{errors: errs}} = Domain.update(int.id, params)
+
+ assert {:ok, _} = Keyword.fetch(errs, :provider_id)
+ assert {:ok, _} = Keyword.fetch(errs, :receiver_id)
+ end
+
+ test "with bad params (with both :receiver and :provider): updates the Intent", %{params: params} do
+ int = Factory.insert!(:intent, %{provider: nil, receiver: Factory.build(:agent)})
+ assert {:error, %Changeset{errors: errs}} = Domain.update(int.id, params)
+
+ assert {:ok, _} = Keyword.fetch(errs, :provider_id)
+ assert {:ok, _} = Keyword.fetch(errs, :receiver_id)
+ end
+
+ test "with bad params: doesn't update the Intent", %{inserted: old} do
+ assert {:ok, %Intent{} = new} = Domain.update(old.id, %{})
+ keys = ~w[
+ action_id
+ provider_id receiver_id
+ input_of_id output_of_id
+ resource_inventoried_as_id resource_conforms_to_id resource_classified_as
+ resource_quantity_has_unit_id resource_quantity_has_numerical_value
+ effort_quantity_has_unit_id effort_quantity_has_numerical_value
+ available_quantity_has_unit_id available_quantity_has_numerical_value
+ has_beginning has_end has_point_in_time due
+ finished image name note agreed_in at_location_id
+ ]a # in_scope_of_id
+ assert Map.take(new, keys) == Map.take(old, keys)
+ end
+end
+
+describe "delete/1" do
+ test "with good id: deletes the Intent", %{inserted: %{id: id}} do
+ assert {:ok, %Intent{id: ^id}} = Domain.delete(id)
+ assert {:error, "not found"} = Domain.one(id)
+ end
+
+ test "with bad id: doesn't delete the Intent", %{id: id} do
+ assert {:error, "not found"} = Domain.delete(id)
+ end
+end
+
+describe "preload/2" do
+ test "preloads `:action`", %{inserted: %{id: id}} do
+ assert {:ok, int} = Domain.one(id)
+ int = Domain.preload(int, :action)
+ assert %Action{} = int.action
+ end
+
+ test "preloads `:input_of`", %{inserted: %{id: id}} do
+ assert {:ok, int} = Domain.one(id)
+ int = Domain.preload(int, :input_of)
+ assert %Process{} = int.input_of
+ end
+
+ test "preloads `:output_of`", %{inserted: %{id: id}} do
+ assert {:ok, int} = Domain.one(id)
+ int = Domain.preload(int, :output_of)
+ assert %Process{} = int.output_of
+ end
+
+ test "preloads `:provider`" do
+ %{id: id} =
+ Factory.insert!(:intent, %{provider: Factory.build(:agent), receiver: nil})
+
+ assert {:ok, int} = Domain.one(id)
+ int = Domain.preload(int, :provider)
+ assert %Agent{} = int.provider
+ end
+
+ test "preloads `:receiver`" do
+ %{id: id} =
+ Factory.insert!(:intent, %{provider: nil, receiver: Factory.build(:agent)})
+
+ assert {:ok, int} = Domain.one(id)
+ int = Domain.preload(int, :receiver)
+ assert %Agent{} = int.receiver
+ end
+
+ test "preloads `:resource_inventoried_as`", %{inserted: %{id: id}} do
+ assert {:ok, int} = Domain.one(id)
+ int = Domain.preload(int, :resource_inventoried_as)
+ assert %EconomicResource{} = int.resource_inventoried_as
+ end
+
+ test "preloads `:resource_conforms_to`", %{inserted: %{id: id}} do
+ assert {:ok, int} = Domain.one(id)
+ int = Domain.preload(int, :resource_conforms_to)
+ assert %ResourceSpecification{} = int.resource_conforms_to
+ end
+
+ test "preloads `:resource_quantity`", %{inserted: %{id: id}} do
+ assert {:ok, int} = Domain.one(id)
+ int = Domain.preload(int, :resource_quantity)
+ assert %Measure{} = int.resource_quantity
+ end
+
+ test "preloads `:effort_quantity`", %{inserted: %{id: id}} do
+ assert {:ok, int} = Domain.one(id)
+ int = Domain.preload(int, :effort_quantity)
+ assert %Measure{} = int.effort_quantity
+ end
+
+ test "preloads `:available_quantity`", %{inserted: %{id: id}} do
+ assert {:ok, int} = Domain.one(id)
+ int = Domain.preload(int, :available_quantity)
+ assert %Measure{} = int.available_quantity
+ end
+
+ test "preloads `:at_location`", %{inserted: %{id: id}} do
+ assert {:ok, int} = Domain.one(id)
+ int = Domain.preload(int, :at_location)
+ assert %SpatialThing{} = int.at_location
+ end
+
+ test "preloads `:published_in`", %{inserted: %{id: id}} do
+ assert {:ok, int} = Domain.one(id)
+
+ assert [] = Domain.preload(int, :published_in).published_in
+
+ left =
+ Enum.map(0..9, fn _ ->
+ Factory.insert!(:proposed_intent, %{publishes: int}).id
+ end)
+ right =
+ Domain.preload(int, :published_in)
+ |> Map.fetch!(:published_in)
+ |> Enum.map(& &1.id)
+ assert left -- right == []
+ end
+end
+end
diff --git a/test/vf/intent/type.test.exs b/test/vf/intent/type.test.exs
@@ -0,0 +1,299 @@
+# Zenflows is designed to implement the Valueflows vocabulary,
+# written and maintained by srfsh <info@dyne.org>.
+# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+defmodule ZenflowsTest.VF.Intent.Type do
+use ZenflowsTest.Help.AbsinCase, async: true
+
+alias Zenflows.DB.ID
+
+setup do
+ %{
+ params: %{
+ "action" => Factory.build(:action_id),
+ "inputOf" => Factory.insert!(:process).id,
+ "outputOf" => Factory.insert!(:process).id,
+ "provider" => Factory.insert!(:agent).id,
+ "receiver" => Factory.insert!(:agent).id,
+ "resourceInventoriedAs" => Factory.insert!(:economic_resource).id,
+ "resourceConformsTo" => Factory.insert!(:resource_specification).id,
+ "resourceClassifiedAs" => Factory.uniq_list("uri"),
+ "resourceQuantity" => %{
+ "hasUnit" => Factory.insert!(:unit).id,
+ "hasNumericalValue" => Factory.float(),
+ },
+ "effortQuantity" => %{
+ "hasUnit" => Factory.insert!(:unit).id,
+ "hasNumericalValue" => Factory.float(),
+ },
+ "availableQuantity" => %{
+ "hasUnit" => Factory.insert!(:unit).id,
+ "hasNumericalValue" => Factory.float(),
+ },
+ "hasBeginning" => Factory.iso_now(),
+ "hasEnd" => Factory.iso_now(),
+ "hasPointInTime" => Factory.iso_now(),
+ "due" => Factory.iso_now(),
+ "finished" => Factory.bool(),
+ "atLocation" => Factory.insert!(:spatial_thing).id,
+ "image" => Factory.img(),
+ "name" => Factory.uniq("name"),
+ "note" => Factory.uniq("note"),
+ # inScopeOf:
+ "agreedIn" => Factory.uniq("uri"),
+ },
+ inserted: Factory.insert!(:intent),
+ id: Factory.id(),
+ }
+end
+
+@frag """
+fragment measure on Measure {
+ hasNumericalValue
+ hasUnit {id}
+}
+
+fragment intent on Intent {
+ id
+ action {id}
+ inputOf {id}
+ outputOf {id}
+ provider {id}
+ receiver {id}
+ resourceInventoriedAs {id}
+ resourceConformsTo {id}
+ resourceClassifiedAs
+ resourceQuantity {...measure}
+ effortQuantity {...measure}
+ availableQuantity {...measure}
+ hasBeginning
+ hasEnd
+ hasPointInTime
+ due
+ finished
+ atLocation {id}
+ image
+ name
+ note
+ agreedIn
+}
+"""
+
+describe "Query" do
+ test "intent", %{inserted: int} do
+ assert %{data: %{"intent" => data}} =
+ run!("""
+ #{@frag}
+ query ($id: ID!) {
+ intent(id: $id) {...intent}
+ }
+ """, vars: %{"id" => int.id})
+
+ assert data["id"] == int.id
+ assert data["action"]["id"] == int.action_id
+ assert data["inputOf"]["id"] == int.input_of_id
+ assert data["outputOf"]["id"] == int.output_of_id
+ assert data["provider"]["id"] == int.provider_id
+ assert data["receiver"]["id"] == int.receiver_id
+ assert data["resourceInventoriedAs"]["id"] == int.resource_inventoried_as_id
+ assert data["resourceConformsTo"]["id"] == int.resource_conforms_to_id
+ assert data["resourceClassifiedAs"] == int.resource_classified_as
+ assert data["resourceQuantity"]["hasNumericalValue"] == int.resource_quantity_has_numerical_value
+ assert data["resourceQuantity"]["hasUnit"]["id"] == int.resource_quantity_has_unit_id
+ assert data["effortQuantity"]["hasNumericalValue"] == int.effort_quantity_has_numerical_value
+ assert data["effortQuantity"]["hasUnit"]["id"] == int.effort_quantity_has_unit_id
+ assert data["availableQuantity"]["hasNumericalValue"] == int.available_quantity_has_numerical_value
+ assert data["availableQuantity"]["hasUnit"]["id"] == int.available_quantity_has_unit_id
+ assert data["hasBeginning"] == DateTime.to_iso8601(int.has_beginning)
+ assert data["hasEnd"] == DateTime.to_iso8601(int.has_end)
+ assert data["hasPointInTime"] == DateTime.to_iso8601(int.has_point_in_time)
+ assert data["due"] == DateTime.to_iso8601(int.due)
+ assert data["finished"] == int.finished
+ assert data["atLocation"]["id"] == int.at_location_id
+ assert data["image"] == int.image
+ assert data["name"] == int.name
+ assert data["note"] == int.note
+ assert data["agreedIn"] == int.agreed_in
+ end
+end
+
+describe "Mutation" do
+ test "createIntent with only provider", %{params: params} do
+ params = Map.put(params, "receiver", nil)
+
+ assert %{data: %{"createIntent" => %{"intent" => data}}} =
+ run!("""
+ #{@frag}
+ mutation ($intent: IntentCreateParams!) {
+ createIntent(intent: $intent) {
+ intent {...intent}
+ }
+ }
+ """, vars: %{"intent" => params})
+
+ assert {:ok, _} = ID.cast(data["id"])
+ assert data["action"]["id"] == params["action"]
+ assert data["inputOf"]["id"] == params["inputOf"]
+ assert data["outputOf"]["id"] == params["outputOf"]
+ assert data["provider"]["id"] == params["provider"]
+ assert data["receiver"]["id"] == params["receiver"]
+ assert data["resourceInventoriedAs"]["id"] == params["resourceInventoriedAs"]
+ assert data["resourceConformsTo"]["id"] == params["resourceConformsTo"]
+ assert data["resourceClassifiedAs"] == params["resourceClassifiedAs"]
+ assert data["resourceQuantity"]["hasNumericalValue"] == params["resourceQuantity"]["hasNumericalValue"]
+ assert data["resourceQuantity"]["hasUnit"]["id"] == params["resourceQuantity"]["hasUnit"]
+ assert data["effortQuantity"]["hasNumericalValue"] == params["effortQuantity"]["hasNumericalValue"]
+ assert data["effortQuantity"]["hasUnit"]["id"] == params["effortQuantity"]["hasUnit"]
+ assert data["availableQuantity"]["hasNumericalValue"] == params["availableQuantity"]["hasNumericalValue"]
+ assert data["availableQuantity"]["hasUnit"]["id"] == params["availableQuantity"]["hasUnit"]
+ assert data["atLocation"]["id"] == params["atLocation"]
+
+ Enum.each(~w[
+ hasBeginning hasEnd hasPointInTime due
+ finished image name note agreedIn
+ ], fn x ->
+ assert data[x] == params[x]
+ end)
+ end
+
+ test "createIntent with only receiver", %{params: params} do
+ params = Map.put(params, "provider", nil)
+
+ assert %{data: %{"createIntent" => %{"intent" => data}}} =
+ run!("""
+ #{@frag}
+ mutation ($intent: IntentCreateParams!) {
+ createIntent(intent: $intent) {
+ intent {...intent}
+ }
+ }
+ """, vars: %{"intent" => params})
+
+ assert {:ok, _} = ID.cast(data["id"])
+ assert data["action"]["id"] == params["action"]
+ assert data["inputOf"]["id"] == params["inputOf"]
+ assert data["outputOf"]["id"] == params["outputOf"]
+ assert data["provider"]["id"] == params["provider"]
+ assert data["receiver"]["id"] == params["receiver"]
+ assert data["resourceInventoriedAs"]["id"] == params["resourceInventoriedAs"]
+ assert data["resourceConformsTo"]["id"] == params["resourceConformsTo"]
+ assert data["resourceClassifiedAs"] == params["resourceClassifiedAs"]
+ assert data["resourceQuantity"]["hasNumericalValue"] == params["resourceQuantity"]["hasNumericalValue"]
+ assert data["resourceQuantity"]["hasUnit"]["id"] == params["resourceQuantity"]["hasUnit"]
+ assert data["effortQuantity"]["hasNumericalValue"] == params["effortQuantity"]["hasNumericalValue"]
+ assert data["effortQuantity"]["hasUnit"]["id"] == params["effortQuantity"]["hasUnit"]
+ assert data["availableQuantity"]["hasNumericalValue"] == params["availableQuantity"]["hasNumericalValue"]
+ assert data["availableQuantity"]["hasUnit"]["id"] == params["availableQuantity"]["hasUnit"]
+ assert data["atLocation"]["id"] == params["atLocation"]
+
+ Enum.each(~w[
+ hasBeginning hasEnd hasPointInTime due
+ finished image name note agreedIn
+ ], fn x ->
+ assert data[x] == params[x]
+ end)
+ end
+
+ test "updateIntent with only provider", %{params: params} do
+ params = Map.put(params, "receiver", nil)
+ %{id: id} = Factory.insert!(:intent, %{provider: Factory.build(:agent), receiver: nil})
+ assert %{data: %{"updateIntent" => %{"intent" => data}}} =
+ run!("""
+ #{@frag}
+ mutation ($intent: IntentUpdateParams!) {
+ updateIntent(intent: $intent) {
+ intent {...intent}
+ }
+ }
+ """, vars: %{
+ "intent" => Map.put(params, "id", id),
+ })
+
+ assert data["id"] == id
+ assert data["action"]["id"] == params["action"]
+ assert data["inputOf"]["id"] == params["inputOf"]
+ assert data["outputOf"]["id"] == params["outputOf"]
+ assert data["provider"]["id"] == params["provider"]
+ assert data["receiver"]["id"] == params["receiver"]
+ assert data["resourceInventoriedAs"]["id"] == params["resourceInventoriedAs"]
+ assert data["resourceConformsTo"]["id"] == params["resourceConformsTo"]
+ assert data["resourceClassifiedAs"] == params["resourceClassifiedAs"]
+ assert data["resourceQuantity"]["hasNumericalValue"] == params["resourceQuantity"]["hasNumericalValue"]
+ assert data["resourceQuantity"]["hasUnit"]["id"] == params["resourceQuantity"]["hasUnit"]
+ assert data["effortQuantity"]["hasNumericalValue"] == params["effortQuantity"]["hasNumericalValue"]
+ assert data["effortQuantity"]["hasUnit"]["id"] == params["effortQuantity"]["hasUnit"]
+ assert data["availableQuantity"]["hasNumericalValue"] == params["availableQuantity"]["hasNumericalValue"]
+ assert data["availableQuantity"]["hasUnit"]["id"] == params["availableQuantity"]["hasUnit"]
+ assert data["atLocation"]["id"] == params["atLocation"]
+
+ Enum.each(~w[
+ hasBeginning hasEnd hasPointInTime due
+ finished image name note agreedIn
+ ], fn x ->
+ assert data[x] == params[x]
+ end)
+ end
+
+ test "updateIntent with only receiver", %{params: params} do
+ params = Map.put(params, "provider", nil)
+ %{id: id} = Factory.insert!(:intent, %{provider: nil, receiver: Factory.build(:agent)})
+ assert %{data: %{"updateIntent" => %{"intent" => data}}} =
+ run!("""
+ #{@frag}
+ mutation ($intent: IntentUpdateParams!) {
+ updateIntent(intent: $intent) {
+ intent {...intent}
+ }
+ }
+ """, vars: %{
+ "intent" => Map.put(params, "id", id),
+ })
+
+ assert data["id"] == id
+ assert data["action"]["id"] == params["action"]
+ assert data["inputOf"]["id"] == params["inputOf"]
+ assert data["outputOf"]["id"] == params["outputOf"]
+ assert data["provider"]["id"] == params["provider"]
+ assert data["receiver"]["id"] == params["receiver"]
+ assert data["resourceInventoriedAs"]["id"] == params["resourceInventoriedAs"]
+ assert data["resourceConformsTo"]["id"] == params["resourceConformsTo"]
+ assert data["resourceClassifiedAs"] == params["resourceClassifiedAs"]
+ assert data["resourceQuantity"]["hasNumericalValue"] == params["resourceQuantity"]["hasNumericalValue"]
+ assert data["resourceQuantity"]["hasUnit"]["id"] == params["resourceQuantity"]["hasUnit"]
+ assert data["effortQuantity"]["hasNumericalValue"] == params["effortQuantity"]["hasNumericalValue"]
+ assert data["effortQuantity"]["hasUnit"]["id"] == params["effortQuantity"]["hasUnit"]
+ assert data["availableQuantity"]["hasNumericalValue"] == params["availableQuantity"]["hasNumericalValue"]
+ assert data["availableQuantity"]["hasUnit"]["id"] == params["availableQuantity"]["hasUnit"]
+ assert data["atLocation"]["id"] == params["atLocation"]
+
+ Enum.each(~w[
+ hasBeginning hasEnd hasPointInTime due
+ finished image name note agreedIn
+ ], fn x ->
+ assert data[x] == params[x]
+ end)
+ end
+
+ test "deleteIntent", %{inserted: %{id: id}} do
+ assert %{data: %{"deleteIntent" => true}} =
+ run!("""
+ mutation ($id: ID!) {
+ deleteIntent(id: $id)
+ }
+ """, vars: %{"id" => id})
+ end
+end
+end