diff --git a/app/services/json_schema_loader.rb b/app/services/json_schema_loader.rb new file mode 100644 index 00000000000..f7d97e5009f --- /dev/null +++ b/app/services/json_schema_loader.rb @@ -0,0 +1,73 @@ +# 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. +#++ + +class JsonSchemaLoader + def initialize(base_path: "docs/api/apiv3/components/schemas") + @base_path = base_path + end + + def load(schema_name) + load_file("#{@base_path}/#{schema_name}.yml") + end + + private + + def load_file(file_name) + return schema_cache.fetch(file_name) if schema_cache.key?(file_name) + + schema = YAML.load_file(file_name).deep_symbolize_keys + schema = resolve_references(schema, file_name) + schema_cache[file_name] = schema + end + + def resolve_references(schema, file_name) + if schema.key?(:$ref) + ref_file = File.join(File.dirname(file_name), schema.fetch(:$ref)) + return load_file(ref_file) + end + + schema.to_h do |key, value| + next [key, resolve_references(value, file_name)] if value.is_a?(Hash) + + if value.is_a?(Array) + next [ + key, + value.map { |v| v.is_a?(Hash) ? resolve_references(v, file_name) : v } + ] + end + + [key, value] + end + end + + def schema_cache + @schema_cache ||= {} + end +end diff --git a/spec/fixtures/files/test_schema.yml b/spec/fixtures/files/test_schema.yml new file mode 100644 index 00000000000..cd4b4022007 --- /dev/null +++ b/spec/fixtures/files/test_schema.yml @@ -0,0 +1,15 @@ +type: object +required: foo +properties: + foo: + type: string + enum: + - one + - two + - three + referenced: + $ref: "./test_schema_reference.yml" + combined: + allOf: + - type: string + - $ref: "./test_schema_reference.yml" diff --git a/spec/fixtures/files/test_schema_reference.yml b/spec/fixtures/files/test_schema_reference.yml new file mode 100644 index 00000000000..6ce481b7ecc --- /dev/null +++ b/spec/fixtures/files/test_schema_reference.yml @@ -0,0 +1 @@ +description: "This is a referenced value." diff --git a/spec/services/json_schema_loader_spec.rb b/spec/services/json_schema_loader_spec.rb new file mode 100644 index 00000000000..52c5ad8f008 --- /dev/null +++ b/spec/services/json_schema_loader_spec.rb @@ -0,0 +1,47 @@ +# 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 "spec_helper" + +RSpec.describe JsonSchemaLoader do + subject { described_class.new(base_path: "spec/fixtures/files").load("test_schema") } + + it "loads the schema as a aymbolized hash" do + expect(subject.dig(:properties, :foo, :type)).to eq("string") + end + + it "resolves $ref attributes" do + expect(subject.dig(:properties, :referenced, :description)).to eq("This is a referenced value.") + end + + it "resolves $ref attributes inside arrays" do + expect(subject.dig(:properties, :combined, :allOf, 1, :description)).to eq("This is a referenced value.") + end +end diff --git a/spec/support/matchers/match_json_schema.rb b/spec/support/matchers/match_json_schema.rb new file mode 100644 index 00000000000..8134ef049b9 --- /dev/null +++ b/spec/support/matchers/match_json_schema.rb @@ -0,0 +1,54 @@ +# 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. +#++ + +RSpec::Matchers.define :match_json_schema do |expected| + chain :from_docs do |docs_schema| + @docs_schema = docs_schema + end + + match do |actual| + raise ArgumentError, "Do not pass arguments to match_json_schema, when using .from_docs." if expected && @docs_schema + + schema = @docs_schema ? JsonSchemaLoader.new.load(@docs_schema) : expected + + validator = JSONSchemer.schema(schema) + + @actual = validator.validate(JSON.parse(actual)).map { |result| result.fetch("error") } + @actual.empty? + end + + failure_message do |actual| + actual.join("\n") + end + + failure_message_when_negated do + "expected schema to not match, but it did." + end +end