zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

commit bd1f61de42b733a35982299c5247a14d853ea914
parent 4e8240dbd809a3d04f26cdae2554c3c9325bad17
Author: srfsh <dev@srf.sh>
Date:   Tue, 16 Aug 2022 13:22:57 +0300

Zenflows{Test,}.VF.Intent: init

Diffstat:
Msrc/zenflows/vf/intent.ex | 5+++++
Asrc/zenflows/vf/intent/domain.ex | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/zenflows/vf/intent/resolv.ex | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/zenflows/vf/intent/type.ex | 355+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/vf/intent/domain.test.exs | 320+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/vf/intent/type.test.exs | 299+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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