Merge pull request #16884 from opf/implementation/58105-item-persistence-service

Implementation/58105 item persistence service
This commit is contained in:
Andreas Pfohl
2024-10-08 14:32:20 +02:00
committed by GitHub
9 changed files with 626 additions and 0 deletions
@@ -0,0 +1,43 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomFields
module Hierarchy
class GenerateRootContract < Dry::Validation::Contract
params do
required(:hierarchy_root)
end
rule(:hierarchy_root) do
key.failure("Hierarchical root already set") unless value.nil?
end
end
end
end
@@ -0,0 +1,57 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomFields
module Hierarchy
class InsertItemContract < Dry::Validation::Contract
params do
required(:parent).filled
required(:label).filled(:string)
optional(:short).filled(:string)
end
rule(:parent) do
if value.is_a?(CustomField::Hierarchy::Item)
unless value.persisted?
key.failure("Parent must exist")
end
else
key.failure("Parent must be of type 'Item'")
end
end
rule(:label) do
if CustomField::Hierarchy::Item.exists?(parent_id: values[:parent], label: value)
key.failure("Label must be unique within the same hierarchy level")
end
end
end
end
end
@@ -0,0 +1,43 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomFields
module Hierarchy
class ServiceInitializationContract < Dry::Validation::Contract
params do
required(:field_format).filled(:string)
end
rule(:field_format) do
key.failure("Custom field must have field format 'hierarchy'") if value != "hierarchy"
end
end
end
end
@@ -0,0 +1,79 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomFields
module Hierarchy
class HierarchicalItemService
include Dry::Monads[:result]
def initialize(custom_field)
validation = ServiceInitializationContract.new.call(field_format: custom_field.field_format)
# rubocop:disable Rails/DeprecatedActiveModelErrorsMethods
raise ArgumentError, "Invalid custom field: #{validation.errors.to_h}" if validation.failure?
# rubocop:enable Rails/DeprecatedActiveModelErrorsMethods
@custom_field = custom_field
end
def generate_root
CustomFields::Hierarchy::GenerateRootContract
.new
.call(hierarchy_root: @custom_field.hierarchy_root)
.to_monad
.bind { create_root_item }
end
def insert_item(parent:, label:, short: nil)
CustomFields::Hierarchy::InsertItemContract
.new
.call({ parent:, label:, short: }.compact)
.to_monad
.bind { |validation| create_child_item(validation) }
end
private
def create_root_item
item = CustomField::Hierarchy::Item.create(custom_field: @custom_field)
return Failure(item.errors) unless item.persisted?
Success(item)
end
def create_child_item(validation)
item = CustomField::Hierarchy::Item
.create(parent: validation[:parent], label: validation[:label], short: validation[:short])
return Failure(item.errors) unless item.persisted?
Success(item)
end
end
end
end
@@ -0,0 +1,78 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require "rails_helper"
RSpec.describe CustomFields::Hierarchy::GenerateRootContract do
subject { described_class.new }
describe "#call" do
context "when hierarchy_root is nil" do
let(:custom_field) { create(:custom_field, field_format: "hierarchy", hierarchy_root: nil) }
it "is valid" do
result = subject.call(hierarchy_root: custom_field.hierarchy_root)
expect(result).to be_success
end
end
context "when hierarchy_root is not nil" do
let(:hierarchy_root) { create(:hierarchy_item) }
let(:custom_field) { create(:custom_field, field_format: "hierarchy", hierarchy_root:) }
it "is invalid" do
result = subject.call(hierarchy_root: custom_field.hierarchy_root)
expect(result).to be_failure
# rubocop:disable Rails/DeprecatedActiveModelErrorsMethods
expect(result.errors.to_h).to include(hierarchy_root: ["Hierarchical root already set"])
# rubocop:enable Rails/DeprecatedActiveModelErrorsMethods
end
end
context "when inputs are valid" do
it "creates a success result" do
[
{ hierarchy_root: nil }
].each { |params| expect(subject.call(params)).to be_success }
end
end
context "when inputs are invalid" do
it "creates a failure result" do
[
{},
{ hierarchy_root: create(:hierarchy_item) },
{ hierarchy_root: "" },
{ hierarchy_root: 42 }
].each { |params| expect(subject.call(params)).to be_failure }
end
end
end
end
@@ -0,0 +1,119 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require "rails_helper"
RSpec.describe CustomFields::Hierarchy::InsertItemContract do
subject { described_class.new }
# rubocop:disable Rails/DeprecatedActiveModelErrorsMethods
describe "#call" do
let(:parent) { create(:hierarchy_item) }
context "when all required fields are valid" do
let(:params) { { parent:, label: "Valid Label" } }
it "is valid" do
result = subject.call(params)
expect(result).to be_success
end
end
context "when parent is not of type 'Item'" do
let(:invalid_parent) { create(:custom_field) }
let(:params) { { parent: invalid_parent, label: "Valid Label" } }
it "is invalid" do
result = subject.call(params)
expect(result).to be_failure
expect(result.errors.to_h).to include(parent: ["Parent must be of type 'Item'"])
end
end
context "when label is not unique within the same hierarchy level" do
before do
create(:hierarchy_item, parent:, label: "Duplicate Label")
end
let(:params) { { parent:, label: "Duplicate Label" } }
it "is invalid" do
result = subject.call(params)
expect(result).to be_failure
expect(result.errors.to_h).to include(label: ["Label must be unique within the same hierarchy level"])
end
end
context "when short is set and is a string" do
let(:params) { { parent:, label: "Valid Label", short: "Valid Short" } }
it "is valid" do
result = subject.call(params)
expect(result).to be_success
end
end
context "when short is set and is not a string" do
let(:params) { { parent:, label: "Valid Label", short: 123 } }
it "is invalid" do
result = subject.call(params)
expect(result).to be_failure
expect(result.errors.to_h).to include(short: ["must be a string"])
end
end
context "when inputs are valid" do
it "creates a success result" do
[
{ parent:, label: "A label", short: "A shorthand" },
{ parent:, label: "A label" }
].each { |params| expect(subject.call(params)).to be_success }
end
end
context "when inputs are invalid" do
it "creates a failure result" do
[
{ parent:, label: "A label", short: "" },
{ parent:, label: "A label", short: nil },
{ parent:, label: "" },
{ parent:, label: nil },
{ parent: },
{ parent: nil },
{ parent: nil, label: "A label" },
{ parent: "parent", label: "A label" },
{ parent: 42, label: "A label" }
].each { |params| expect(subject.call(params)).to be_failure }
end
end
end
# rubocop:enable Rails/DeprecatedActiveModelErrorsMethods
end
@@ -0,0 +1,97 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require "rails_helper"
RSpec.describe CustomFields::Hierarchy::ServiceInitializationContract do
subject { described_class.new }
# rubocop:disable Rails/DeprecatedActiveModelErrorsMethods
describe "#call" do
context "when field_format is 'hierarchy'" do
let(:params) { { field_format: "hierarchy" } }
it "is valid" do
result = subject.call(params)
expect(result).to be_success
end
end
context "when field_format is not 'hierarchy'" do
let(:params) { { field_format: "text" } }
it "is invalid" do
result = subject.call(params)
expect(result).to be_failure
expect(result.errors.to_h).to include(field_format: ["Custom field must have field format 'hierarchy'"])
end
end
context "when field_format is missing" do
let(:params) { {} }
it "is invalid" do
result = subject.call(params)
expect(result).to be_failure
expect(result.errors.to_h).to include(field_format: ["is missing"])
end
end
context "when field_format is nil" do
let(:params) { { field_format: nil } }
it "is invalid" do
result = subject.call(params)
expect(result).to be_failure
expect(result.errors.to_h).to include(field_format: ["must be filled"])
end
end
context "when inputs are valid" do
it "creates a success result" do
[
{ field_format: "hierarchy" }
].each { |params| expect(subject.call(params)).to be_success }
end
end
context "when inputs are invalid" do
it "creates a failure result" do
[
{},
{ field_format: "text" },
{ field_format: nil },
{ field_format: 42 }
].each { |params| expect(subject.call(params)).to be_failure }
end
end
end
# rubocop:enable Rails/DeprecatedActiveModelErrorsMethods
end
+7
View File
@@ -0,0 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
factory :hierarchy_item, class: "CustomField::Hierarchy::Item" do
sequence(:label) { |n| "Item #{n}" }
end
end
@@ -0,0 +1,103 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require "rails_helper"
RSpec.describe CustomFields::Hierarchy::HierarchicalItemService do
let(:custom_field) { create(:custom_field, field_format: "hierarchy", hierarchy_root: nil) }
let(:invalid_custom_field) { create(:custom_field, field_format: "text", hierarchy_root: nil) }
describe "#initialize" do
context "with valid custom field" do
it "initializes successfully" do
expect { described_class.new(custom_field) }.not_to raise_error
end
end
context "with invalid custom field" do
it "raises an ArgumentError" do
expect { described_class.new(invalid_custom_field) }.to raise_error(ArgumentError, /Invalid custom field/)
end
end
end
describe "#generate_root" do
let(:service) { described_class.new(custom_field) }
context "with valid hierarchy root" do
it "creates a root item successfully" do
expect(service.generate_root).to be_success
end
end
context "with persistence of hierarchy root fails" do
it "fails to create a root item" do
allow(CustomField::Hierarchy::Item)
.to receive(:create)
.and_return(instance_double(CustomField::Hierarchy::Item, persisted?: false, errors: "some errors"))
result = service.generate_root
expect(result).to be_failure
end
end
end
describe "#insert_item" do
let(:custom_field) { create(:custom_field, field_format: "hierarchy", hierarchy_root: parent) }
let(:service) { described_class.new(custom_field) }
let(:parent) { create(:hierarchy_item) }
let(:label) { "Child Item" }
let(:short) { "Short Description" }
context "with valid parameters" do
it "inserts an item successfully without short" do
result = service.insert_item(parent:, label:)
expect(result).to be_success
end
it "inserts an item successfully with short" do
result = service.insert_item(parent:, label:, short:)
expect(result).to be_success
end
end
context "with invalid item" do
it "fails to insert an item" do
allow(CustomField::Hierarchy::Item)
.to receive(:create).and_return(instance_double(CustomField::Hierarchy::Item,
persisted?: false, errors: "some errors"))
result = service.insert_item(parent:, label:, short:)
expect(result).to be_failure
end
end
end
end