From 331bca7906a194ab33d8bdab054cffa0193edf90 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Thu, 10 Dec 2020 10:43:30 -0500 Subject: [PATCH 01/40] Added graph support(RGL), bumped version, helpers to render navigation --- lib/resource_registry.rb | 2 ++ lib/resource_registry/namespace.rb | 33 +++++++++++++++++ lib/resource_registry/rgl.rb | 58 ++++++++++++++++++++++++++++++ lib/resource_registry/version.rb | 4 +-- resource_registry.gemspec | 3 +- 5 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 lib/resource_registry/namespace.rb create mode 100644 lib/resource_registry/rgl.rb diff --git a/lib/resource_registry.rb b/lib/resource_registry.rb index 14763b88..d98f1a45 100644 --- a/lib/resource_registry.rb +++ b/lib/resource_registry.rb @@ -17,8 +17,10 @@ require 'resource_registry/validation/application_contract' require 'resource_registry/railtie' if defined? Rails +require 'resource_registry/rgl' require 'resource_registry/meta' require 'resource_registry/setting' +require 'resource_registry/namespace' require 'resource_registry/feature' require 'resource_registry/feature_dsl' require 'resource_registry/configuration' diff --git a/lib/resource_registry/namespace.rb b/lib/resource_registry/namespace.rb new file mode 100644 index 00000000..bbbd1c60 --- /dev/null +++ b/lib/resource_registry/namespace.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative 'validation/namespace_contract' +require_relative 'operations/namespaces/build' +require_relative 'operations/namespaces/create' +require_relative 'operations/graphs/create' + +module ResourceRegistry + # Define a Feature together with its settings, code hook for dependency injection, and configuration UI attributes + # + # @example Define the feature + # Feature.new(key: :greeter, item: proc { |name| "Hello #{name}!" }) + # Feature.new(key: :logger, item: Logger.new(STDERR), settings: [{default: :warn}]) + class Namespace < Dry::Struct + + # @!attribute [r] key (required) + # Identifier for this Feature. Must be unique within a registry namespace + # @return [Symbol] + attribute :key, Types::Symbol.meta(omittable: false) + + # @!attribute [r] item (required) + # The reference or code to be evaluated when feature is resolved + # @return [Any] + attribute :path, Types::Array.of(Types::RequiredSymbol).default([].freeze).meta(omittable: false) + + # @!attribute [r] meta (optional) + # Configuration settings and attributes that support presenting and updating their values in the User Interface + # @return [ResourceRegistry::Meta] + attribute :meta, ResourceRegistry::Meta.default(Hash.new.freeze).meta(omittable: true) + + attribute :feature_keys, Types::Array.of(Types::RequiredSymbol).default([].freeze).meta(omittable: true) + end +end \ No newline at end of file diff --git a/lib/resource_registry/rgl.rb b/lib/resource_registry/rgl.rb new file mode 100644 index 00000000..11d90309 --- /dev/null +++ b/lib/resource_registry/rgl.rb @@ -0,0 +1,58 @@ +module RGL + class DirectedAdjacencyGraph + include ActionView::Helpers::TagHelper + include ActionView::Context + + def root_vertices + vertices - edges.map{|edge| edge[1]}.uniq + end + + def to_html(element) + end + + def to_h(vertex) + [vertex].inject({}) do |dict, vertex| + dict = vertex.to_h + dict[:features] = dict[:feature_keys].collect{|key| EnrollRegistry[key].feature.to_h.slice(:key, :item, :meta)} + dict[:namespaces] = self.adjacent_vertices(vertex).collect{|adjacent_vertex| self.to_h(adjacent_vertex)} + dict.except(:feature_keys) + end + + # call namespace contract + # return valid hash + end + + def to_ul(vertex, options = {}) + dict = self.to_h(vertex) if vertex.is_a?(ResourceRegistry::Namespace) + + tag.ul(class: (options[:class_name] || 'nav flex-column flex-nowrap overflow-hidden')) do + self.to_li(dict || vertex) + end + end + + def to_li(element, options = {}) + tag.li(class: (options[:class_name] || 'nav-item')) do + content = to_nav_link(element) + if element[:namespaces] || element[:features] + content += tag.div(class: 'collapse', id: "nav_#{element[:key]}", 'aria-expanded': 'false') do + (element[:features] + element[:namespaces]).reduce('') do |list, child_ele| + list += self.to_ul(child_ele, {class_name: 'flex-column nav pl-4'}) + end.html_safe + end + end + content.html_safe + end + end + + def to_nav_link(element) + href_options = {href: element[:item], 'data-remote': true} + href_options = {href: "#nav_#{element[:key]}", class: "nav-link collapsed text-truncate", 'data-toggle': 'collapse', 'data-target': "#nav_#{element[:key]}"} if element[:namespaces] + + tag.a(href_options) do + tag.span do + element[:namespaces] ? element[:path].last.to_s.titleize : element[:meta][:label] + end + end + end + end +end \ No newline at end of file diff --git a/lib/resource_registry/version.rb b/lib/resource_registry/version.rb index f7ef834e..1fe5eb24 100644 --- a/lib/resource_registry/version.rb +++ b/lib/resource_registry/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module ResourceRegistry - VERSION = "0.7.0" -end + VERSION = "0.9.0" +end \ No newline at end of file diff --git a/resource_registry.gemspec b/resource_registry.gemspec index 516d8814..bd8452fe 100644 --- a/resource_registry.gemspec +++ b/resource_registry.gemspec @@ -54,6 +54,8 @@ Gem::Specification.new do |spec| spec.add_dependency 'ox', '~> 2.0' spec.add_dependency 'bootsnap', '~> 1.0' spec.add_dependency 'mime-types' + spec.add_dependency 'pry-byebug' + spec.add_dependency 'rgl'#, '~> 0.5.6' spec.add_development_dependency "bundler", "~> 2.0" spec.add_development_dependency 'rake', '~> 12.0' @@ -66,6 +68,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency "timecop", '~> 0.9' spec.add_development_dependency "rubocop", '~> 0.74.0' spec.add_development_dependency "yard", "~> 0.9" - spec.add_development_dependency 'pry-byebug' end From dcf0c2c94fc3ef0169c69db0f68da54465d4b0f0 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Fri, 11 Dec 2020 09:28:06 -0500 Subject: [PATCH 02/40] Added namespace contract and validate namespace hash against contract --- lib/resource_registry.rb | 2 +- lib/resource_registry/namespace.rb | 6 ++- lib/resource_registry/rgl.rb | 15 +++--- .../validation/namespace_contract.rb | 46 +++++++++++++++++++ 4 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 lib/resource_registry/validation/namespace_contract.rb diff --git a/lib/resource_registry.rb b/lib/resource_registry.rb index d98f1a45..09b34135 100644 --- a/lib/resource_registry.rb +++ b/lib/resource_registry.rb @@ -20,8 +20,8 @@ require 'resource_registry/rgl' require 'resource_registry/meta' require 'resource_registry/setting' -require 'resource_registry/namespace' require 'resource_registry/feature' +require 'resource_registry/namespace' require 'resource_registry/feature_dsl' require 'resource_registry/configuration' require 'resource_registry/registry' diff --git a/lib/resource_registry/namespace.rb b/lib/resource_registry/namespace.rb index bbbd1c60..e9628d67 100644 --- a/lib/resource_registry/namespace.rb +++ b/lib/resource_registry/namespace.rb @@ -29,5 +29,9 @@ class Namespace < Dry::Struct attribute :meta, ResourceRegistry::Meta.default(Hash.new.freeze).meta(omittable: true) attribute :feature_keys, Types::Array.of(Types::RequiredSymbol).default([].freeze).meta(omittable: true) + + attribute :features, Types::Array.of(::ResourceRegistry::Feature).meta(omittable: true) + + attribute :namespaces, Types::Array.of(::ResourceRegistry::Namespace).meta(omittable: true) end -end \ No newline at end of file +end diff --git a/lib/resource_registry/rgl.rb b/lib/resource_registry/rgl.rb index 11d90309..832b64d3 100644 --- a/lib/resource_registry/rgl.rb +++ b/lib/resource_registry/rgl.rb @@ -11,15 +11,18 @@ def to_html(element) end def to_h(vertex) - [vertex].inject({}) do |dict, vertex| + namespace_dict = [vertex].inject({}) do |dict, vertex| dict = vertex.to_h - dict[:features] = dict[:feature_keys].collect{|key| EnrollRegistry[key].feature.to_h.slice(:key, :item, :meta)} + dict[:features] = dict[:feature_keys].collect{|key| EnrollRegistry[key].feature.to_h.except(:settings)} dict[:namespaces] = self.adjacent_vertices(vertex).collect{|adjacent_vertex| self.to_h(adjacent_vertex)} - dict.except(:feature_keys) + attrs_to_skip = [:feature_keys] + attrs_to_skip << :meta if dict[:meta].empty? + dict.except(*attrs_to_skip) end - # call namespace contract - # return valid hash + result = ResourceRegistry::Validation::NamespaceContract.new.call(namespace_dict) + raise "Unable to construct graph due to #{result.errors.to_h}" unless result.success? + result.to_h end def to_ul(vertex, options = {}) @@ -40,7 +43,7 @@ def to_li(element, options = {}) end.html_safe end end - content.html_safe + content end end diff --git a/lib/resource_registry/validation/namespace_contract.rb b/lib/resource_registry/validation/namespace_contract.rb new file mode 100644 index 00000000..4552461d --- /dev/null +++ b/lib/resource_registry/validation/namespace_contract.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module ResourceRegistry + module Validation + # Schema and validation rules for the {ResourceRegistry::Namespace} domain model + class NamespaceContract < ResourceRegistry::Validation::ApplicationContract + + # @!method call(opts) + # @param [Hash] opts the parameters to validate using this contract + # @option opts [Symbol] :key required + # @option opts [Array] :path required + # @option opts [ResourceRegistry::Meta] :meta optional + # @option opts [Array] :feature_keys optional + # @option opts [Array] :features optional + # @option opts [Array] :namespaces optional + # @return [Dry::Monads::Result::Success] if params pass validation + # @return [Dry::Monads::Result::Failure] if params fail validation + params do + required(:key).value(:symbol) + required(:path).array(:symbol) + optional(:meta).maybe(:hash) + optional(:feature_keys).array(:symbol) + optional(:features).array(:hash) + optional(:namespaces).array(:hash) + + # before(:value_coercer) do |result| + # result.to_h.merge!(meta: result[:meta].symbolize_keys) if result[:meta].is_a? Hash + # end + end + + rule(:features).each do + if key? && value + result = ResourceRegistry::Validation::FeatureContract.new.call(value) + key.failure(text: "invalid feature", error: result.errors.to_h) if result && result.failure? + end + end + + rule(:namespaces).each do + if key? && value + result = ResourceRegistry::Validation::NamespaceContract.new.call(value) + key.failure(text: "invalid namespace", error: result.errors.to_h) if result && result.failure? + end + end + end + end +end \ No newline at end of file From 6a40b4c18b10e3ec70fe138d5664cf6661ac768c Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Fri, 11 Dec 2020 19:56:34 -0500 Subject: [PATCH 03/40] Added namespace_path entity, contract and refactored rgl navigation view helpers --- lib/resource_registry.rb | 1 + lib/resource_registry/feature.rb | 3 +- lib/resource_registry/feature_dsl.rb | 6 +- lib/resource_registry/namespace.rb | 2 +- lib/resource_registry/namespace_path.rb | 22 ++++++ .../operations/features/create.rb | 19 +++-- .../operations/graphs/create.rb | 72 ++++++++++++++++++ .../operations/registries/create.rb | 65 ++++++++-------- .../operations/registries/load.rb | 34 ++++++++- lib/resource_registry/rgl.rb | 74 ++++++++++++++----- .../validation/feature_contract.rb | 24 ++++-- .../validation/meta_contract.rb | 2 +- 12 files changed, 253 insertions(+), 71 deletions(-) create mode 100644 lib/resource_registry/namespace_path.rb create mode 100644 lib/resource_registry/operations/graphs/create.rb diff --git a/lib/resource_registry.rb b/lib/resource_registry.rb index 09b34135..3cd81252 100644 --- a/lib/resource_registry.rb +++ b/lib/resource_registry.rb @@ -20,6 +20,7 @@ require 'resource_registry/rgl' require 'resource_registry/meta' require 'resource_registry/setting' +require 'resource_registry/namespace_path' require 'resource_registry/feature' require 'resource_registry/namespace' require 'resource_registry/feature_dsl' diff --git a/lib/resource_registry/feature.rb b/lib/resource_registry/feature.rb index d8f906a6..347c7004 100644 --- a/lib/resource_registry/feature.rb +++ b/lib/resource_registry/feature.rb @@ -24,7 +24,7 @@ class Feature < Dry::Struct # @!attribute [r] namespace (optional) # The registry namespace where this item is stored # @return [Symbol] - attribute :namespace, Types::Array.of(Types::RequiredSymbol).default([].freeze).meta(omittable: false) + attribute :namespace_path, ::ResourceRegistry::NamespacePath.optional.meta(omittable: false) # @!attribute [r] is_enabled (required) # Availability state of this Feature in the application: either enabled or disabled @@ -53,5 +53,6 @@ class Feature < Dry::Struct # @return [Array] attribute :settings, Types::Array.of(ResourceRegistry::Setting).default([].freeze).meta(omittable: true) + end end diff --git a/lib/resource_registry/feature_dsl.rb b/lib/resource_registry/feature_dsl.rb index a3ea719b..ef2a0b27 100644 --- a/lib/resource_registry/feature_dsl.rb +++ b/lib/resource_registry/feature_dsl.rb @@ -25,7 +25,11 @@ def key # @return [String] the namespace under which the feature is stored, in dot notation def namespace - @feature.namespace.map(&:to_s).join('.') + @feature.namespace_path.path.map(&:to_s).join('.') + end + + def assigned_namespace + @feature.namespace.path.map(&:to_s).join('.') end # @!method enabled? diff --git a/lib/resource_registry/namespace.rb b/lib/resource_registry/namespace.rb index e9628d67..da4d6fc5 100644 --- a/lib/resource_registry/namespace.rb +++ b/lib/resource_registry/namespace.rb @@ -28,7 +28,7 @@ class Namespace < Dry::Struct # @return [ResourceRegistry::Meta] attribute :meta, ResourceRegistry::Meta.default(Hash.new.freeze).meta(omittable: true) - attribute :feature_keys, Types::Array.of(Types::RequiredSymbol).default([].freeze).meta(omittable: true) + attribute :feature_keys, Types::Array.of(Types::RequiredSymbol).default([].freeze).meta(omittable: true) attribute :features, Types::Array.of(::ResourceRegistry::Feature).meta(omittable: true) diff --git a/lib/resource_registry/namespace_path.rb b/lib/resource_registry/namespace_path.rb new file mode 100644 index 00000000..bf4c31a9 --- /dev/null +++ b/lib/resource_registry/namespace_path.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require_relative 'validation/namespace_path_contract' + +module ResourceRegistry + # Define a Feature together with its settings, code hook for dependency injection, and configuration UI attributes + # + # @example Define the feature + # Feature.new(key: :greeter, item: proc { |name| "Hello #{name}!" }) + # Feature.new(key: :logger, item: Logger.new(STDERR), settings: [{default: :warn}]) + class NamespacePath < Dry::Struct + # @!attribute [r] item (required) + # The reference or code to be evaluated when feature is resolved + # @return [Any] + attribute :path, Types::Array.of(Types::RequiredSymbol).optional.meta(omittable: false) + + # @!attribute [r] meta (optional) + # Configuration settings and attributes that support presenting and updating their values in the User Interface + # @return [ResourceRegistry::Meta] + attribute :meta, ::ResourceRegistry::Meta.optional.meta(omittable: true) + end +end \ No newline at end of file diff --git a/lib/resource_registry/operations/features/create.rb b/lib/resource_registry/operations/features/create.rb index bc94a503..e27450e7 100644 --- a/lib/resource_registry/operations/features/create.rb +++ b/lib/resource_registry/operations/features/create.rb @@ -8,7 +8,8 @@ class Create send(:include, Dry::Monads[:result, :do]) def call(params) - feature_values = yield validate(params) + feature_params = yield construct(params) + feature_values = yield validate(feature_params) feature = yield create(feature_values) Success(feature) @@ -16,19 +17,17 @@ def call(params) private - def validate(params) - result = ResourceRegistry::Validation::FeatureContract.new.call(params) + def construct(params) + params['namespace_path'] = params['namespace'].is_a?(Hash) ? params.delete('namespace') : {path: params.delete('namespace')} + Success(params) + end - if result.success? - Success(result.to_h) - else - Failure(result) - end + def validate(params) + ResourceRegistry::Validation::FeatureContract.new.call(params) end def create(feature_values) - feature = ResourceRegistry::Feature.new(feature_values) - + feature = ResourceRegistry::Feature.new(feature_values.to_h) Success(feature) end end diff --git a/lib/resource_registry/operations/graphs/create.rb b/lib/resource_registry/operations/graphs/create.rb new file mode 100644 index 00000000..a0aa807b --- /dev/null +++ b/lib/resource_registry/operations/graphs/create.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true +require 'dry/monads' +require 'rgl/adjacency' +require 'rgl/implicit' + +module ResourceRegistry + module Operations + module Graphs + # Create a Feature + class Create + send(:include, Dry::Monads[:result, :do]) + + def call(namespaces, registry) + graph = yield create(namespaces, registry) + # yield validate(graph) + Success(graph) + end + + private + + def create(namespaces, registry) + graph = registry['feature_graph'] if registry.key?('feature_graph') + graph ||= ::RGL::DirectedAdjacencyGraph.new + @vertex_dir ||= {} + namespaces.each do |namespace| + namespace_vertices = namespace_to_vertices(namespace) + namespace_vertices.each_index do |index| + if namespace_vertices[index + 1].present? + graph.add_edge(namespace_vertices[index], namespace_vertices[index+1]) + else + graph.add_vertex(namespace_vertices[index]) + end + end + end + + # validate graph + + Success(graph) + end + + def validate(graph) + # graph.acyclic? && directed? + # return error message with cycles when acyclic failed + end + + # TODO: Output graph when its successful dotted graph in console/log + + def namespace_to_vertices(namespace) + paths = namespace[:path].dup + vertex_path = [] + while true + current = paths.shift + vertex_path.push(current) + (vertices ||= []) << (paths.empty? ? find_or_create_vertex(vertex_path, namespace) : find_or_create_vertex(vertex_path)) + break if paths.empty? + end + vertices + end + + def find_or_create_vertex(vertex_path, namespace_hash = {}) + vertex_key = digest_key_for(vertex_path) + return @vertex_dir[vertex_key] if @vertex_dir[vertex_key] + @vertex_dir[vertex_key] = ResourceRegistry::Namespace.new(namespace_hash.merge(key: vertex_key.to_sym, path: vertex_path)) + end + + def digest_key_for(vertex_path) + Digest::MD5.hexdigest(vertex_path.map(&:to_s).join('_')).slice(0..9) + end + end + end + end +end diff --git a/lib/resource_registry/operations/registries/create.rb b/lib/resource_registry/operations/registries/create.rb index d728d7d4..15b3a09f 100644 --- a/lib/resource_registry/operations/registries/create.rb +++ b/lib/resource_registry/operations/registries/create.rb @@ -5,16 +5,16 @@ module Operations module Registries # Create a Feature class Create - send(:include, Dry::Monads[:result, :do]) + send(:include, Dry::Monads[:result, :do, :try]) def call(path:, registry:) file_io = yield read(path) params = yield deserialize(file_io) feature_hashes = yield serialize(params) features = yield create(feature_hashes) - container = yield register(features, registry) + values = yield register_features(features, registry) - Success(container) + Success(values) end private @@ -50,39 +50,46 @@ def serialize(params) end def create(feature_hashes) - features = feature_hashes.collect do |feature_hash| - feature = ResourceRegistry::Operations::Features::Create.new.call(feature_hash) - - if feature.success? - feature.value! - else - raise "Failed to create feature with #{feature.failure.errors.inspect}" + Try { + feature_hashes.collect do |feature_hash| + result = ResourceRegistry::Operations::Features::Create.new.call(feature_hash) + return result if result.failure? + result.value! end - end - - Success(features) - rescue Exception => e - raise "Error occurred while creating features using #{feature_hashes}. " \ - "Error: #{e.message}" + }.to_result end - def register(features, registry) + def register_features(features, registry) + namespaces = [] features.each do |feature| - if defined?(Rails) && registry.db_connection&.table_exists?(:resource_registry_features) - feature_record = ResourceRegistry::ActiveRecord::Feature.where(key: feature.key).first - - if feature_record.blank? - ResourceRegistry::ActiveRecord::Feature.new(feature.to_h).save - else - result = ResourceRegistry::Operations::Features::Create.new.call(feature_record.to_h) - feature = result.success if result.success? - end - end - + persist_to_rdbms(feature, registry) registry.register_feature(feature) + namespaces << feature_to_namespace(feature) if feature.namespace_path.meta&.content_type.to_s == 'nav' end - Success(registry) + Success({namespace_list: namespaces, registry: registry}) + end + + def feature_to_namespace(feature) + { + key: feature.namespace_path.path.map(&:to_s).join('_'), + path: feature.namespace_path.path, + feature_keys: [feature.key], + meta: feature.namespace_path.meta.to_h + } + end + + def persist_to_rdbms(feature, registry) + if defined?(Rails) && registry.db_connection&.table_exists?(:resource_registry_features) + feature_record = ResourceRegistry::ActiveRecord::Feature.where(key: feature.key).first + + if feature_record.blank? + ResourceRegistry::ActiveRecord::Feature.new(feature.to_h).save + else + result = ResourceRegistry::Operations::Features::Create.new.call(feature_record.to_h) + feature = result.success if result.success? # TODO: Verify Failure Scenario + end + end end end end diff --git a/lib/resource_registry/operations/registries/load.rb b/lib/resource_registry/operations/registries/load.rb index 006bc0a3..eff02080 100644 --- a/lib/resource_registry/operations/registries/load.rb +++ b/lib/resource_registry/operations/registries/load.rb @@ -10,24 +10,50 @@ class Load def call(registry:) paths = yield list_paths(load_path_for(registry)) - result = yield load(paths, registry) + values = yield load(paths, registry) + entities = yield merge_namespaces(values[:namespace_list]) + registry = yield register_graph(entities, values[:registry]) - Success(result) + Success(registry) end private def list_paths(load_path) paths = ResourceRegistry::Stores::File::ListPath.new.call(load_path) - Success(paths) end def load(paths, registry) + namespaces_list = [] + paths.value!.each do |path| - ResourceRegistry::Operations::Registries::Create.new.call(path: path, registry: registry) + result = ResourceRegistry::Operations::Registries::Create.new.call(path: path, registry: registry) + if result.success? + namespaces_list << result.success[:namespace_list] + end + end + + Success({registry: registry, namespace_list: namespaces_list.flatten}) + end + + def merge_namespaces(namespace_list) + namespaces = namespace_list.reduce({}) do |namespaces, ns| + if namespaces[ns[:key]] + namespaces[ns[:key]][:feature_keys] += ns[:feature_keys] + else + namespaces[ns[:key]] = ns + end + namespaces end + Success(namespaces.values) + end + + def register_graph(entities, registry) + graph = ResourceRegistry::Operations::Graphs::Create.new.call(entities, registry) + registry.register_graph(graph.value!) + Success(registry) end diff --git a/lib/resource_registry/rgl.rb b/lib/resource_registry/rgl.rb index 832b64d3..80ace604 100644 --- a/lib/resource_registry/rgl.rb +++ b/lib/resource_registry/rgl.rb @@ -3,11 +3,36 @@ class DirectedAdjacencyGraph include ActionView::Helpers::TagHelper include ActionView::Context + TAG_OPTION_DEFAULTS = { + ul: { + options: {class: 'nav flex-column flex-nowrap overflow-hidden'} + }, + nested_ul: { + options: {class: 'flex-column nav pl-4'} + }, + li: { + options: {class: 'nav-item'} + }, + a: { + namespace_link: { + options: {class: 'nav-link collapsed text-truncate', 'data-toggle': 'collapse'}, + }, + feature_link: { + options: {'data-remote': true} + } + } + } + def root_vertices vertices - edges.map{|edge| edge[1]}.uniq end - def to_html(element) + def tag_options=(options = {}) + @tag_options = TAG_OPTION_DEFAULTS.merge(options) + end + + def tag_options + @tag_options || TAG_OPTION_DEFAULTS end def to_h(vertex) @@ -25,37 +50,48 @@ def to_h(vertex) result.to_h end - def to_ul(vertex, options = {}) + def to_ul(vertex, nested = false) dict = self.to_h(vertex) if vertex.is_a?(ResourceRegistry::Namespace) - tag.ul(class: (options[:class_name] || 'nav flex-column flex-nowrap overflow-hidden')) do - self.to_li(dict || vertex) + tag.ul(tag_options[(nested ? :nested_ul : :ul)][:options]) do + to_li(dict || vertex) end end - def to_li(element, options = {}) - tag.li(class: (options[:class_name] || 'nav-item')) do - content = to_nav_link(element) + private + + def to_li(element) + tag.li(tag_options[:li][:options]) do if element[:namespaces] || element[:features] - content += tag.div(class: 'collapse', id: "nav_#{element[:key]}", 'aria-expanded': 'false') do - (element[:features] + element[:namespaces]).reduce('') do |list, child_ele| - list += self.to_ul(child_ele, {class_name: 'flex-column nav pl-4'}) - end.html_safe - end + namespace_nav_link(element) + content_to_expand(element) + else + feature_nav_link(element) + end + end + end + + def content_to_expand(element) + tag.div(class: 'collapse', id: "nav_#{element[:key]}", 'aria-expanded': 'false') do + (element[:features] + element[:namespaces]).reduce('') do |list, child_ele| + list += self.to_ul(child_ele, true) + end.html_safe + end + end + + def namespace_nav_link(element) + tag.a(tag_options[:a][:namespace_link][:options].merge(href: "#nav_#{element[:key]}", 'data-target': "#nav_#{element[:key]}")) do + tag.span do + element[:namespaces] ? element[:path].last.to_s.titleize : element[:meta][:label] end - content end end - def to_nav_link(element) - href_options = {href: element[:item], 'data-remote': true} - href_options = {href: "#nav_#{element[:key]}", class: "nav-link collapsed text-truncate", 'data-toggle': 'collapse', 'data-target': "#nav_#{element[:key]}"} if element[:namespaces] - - tag.a(href_options) do + def feature_nav_link(element) + tag.a(tag_options[:a][:feature_link][:options].merge(href: element[:item])) do tag.span do element[:namespaces] ? element[:path].last.to_s.titleize : element[:meta][:label] end end end end -end \ No newline at end of file +end diff --git a/lib/resource_registry/validation/feature_contract.rb b/lib/resource_registry/validation/feature_contract.rb index ebacee17..eb2eeeda 100644 --- a/lib/resource_registry/validation/feature_contract.rb +++ b/lib/resource_registry/validation/feature_contract.rb @@ -18,16 +18,14 @@ class FeatureContract < ResourceRegistry::Validation::ApplicationContract # @return [Dry::Monads::Result::Failure] if params fail validation params do required(:key).value(:symbol) - required(:namespace).maybe(:array) + required(:namespace_path).maybe(:hash) required(:is_enabled).value(:bool) optional(:item).value(:any) optional(:options).maybe(:hash) - optional(:meta).maybe(:hash) optional(:settings).array(:hash) before(:value_coercer) do |result| - settings = result[:settings]&.map(&:deep_symbolize_keys)&.collect do |setting| setting.tap do |setting| if setting[:meta] && setting[:meta][:content_type] == :duration @@ -45,11 +43,27 @@ class FeatureContract < ResourceRegistry::Validation::ApplicationContract result.to_h.merge( key: result[:key]&.to_sym, meta: result[:meta]&.symbolize_keys, - settings: settings || [], - namespace: (result[:namespace] || []).map(&:to_sym) + namespace_path: result[:namespace_path].deep_symbolize_keys, + settings: settings || [] ) end end + + # @!macro [attach] rulemacro + # Validates a nested hash of $1 params + # @!method $0($1) + # @param [Symbol] $1 key + # @return [Dry::Monads::Result::Success] if nested $1 params pass validation + # @return [Dry::Monads::Result::Failure] if nested $1 params fail validation + rule(:namespace_path) do + if key? && value + result = ResourceRegistry::Validation::NamespacePathContract.new.call(value) + + # Use dry-validation error form to pass error hash along with text to calling service + # self.result.to_h.merge!({meta: result.to_h}) + key.failure(text: "invalid meta", error: result.errors.to_h) if result && result.failure? + end + end end end end diff --git a/lib/resource_registry/validation/meta_contract.rb b/lib/resource_registry/validation/meta_contract.rb index 7502d5f5..2d7e35ee 100644 --- a/lib/resource_registry/validation/meta_contract.rb +++ b/lib/resource_registry/validation/meta_contract.rb @@ -20,7 +20,7 @@ class MetaContract < ResourceRegistry::Validation::ApplicationContract params do required(:label).value(:string) required(:content_type).value(:symbol) - required(:default).value(:any) + optional(:default).maybe(:any) optional(:value).maybe(:any) optional(:description).maybe(:string) optional(:enum).maybe(:array) From 2695e686016d86c38f590a61e2137a8377234dbe Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Wed, 16 Dec 2020 08:45:33 -0500 Subject: [PATCH 04/40] Updated DSL to render navigation --- lib/resource_registry.rb | 9 +- lib/resource_registry/namespace.rb | 27 ++-- lib/resource_registry/navigation.rb | 123 ++++++++++++++++++ .../operations/graphs/create.rb | 27 ++-- .../operations/registries/load.rb | 7 +- lib/resource_registry/registry.rb | 9 ++ lib/resource_registry/rgl.rb | 98 ++------------ .../validation/namespace_contract.rb | 4 - .../validation/namespace_path_contract.rb | 20 +++ .../aca_shop_market/aca_shop_market.yml | 4 + .../features/invalid/aca_shop_market.yml | 14 ++ .../operations/registries/create_spec.rb | 15 ++- .../validation/feature_contract_spec.rb | 11 ++ spec/support/registry_data_seed.rb | 4 + 14 files changed, 260 insertions(+), 112 deletions(-) create mode 100644 lib/resource_registry/navigation.rb create mode 100644 lib/resource_registry/validation/namespace_path_contract.rb create mode 100644 spec/rails_app/system/config/templates/features/invalid/aca_shop_market.yml diff --git a/lib/resource_registry.rb b/lib/resource_registry.rb index 3cd81252..2903f6c9 100644 --- a/lib/resource_registry.rb +++ b/lib/resource_registry.rb @@ -7,7 +7,6 @@ require 'dry/monads/do' require 'dry-struct' - require 'resource_registry/version' require 'resource_registry/error' @@ -17,6 +16,7 @@ require 'resource_registry/validation/application_contract' require 'resource_registry/railtie' if defined? Rails +require 'resource_registry/navigation' require 'resource_registry/rgl' require 'resource_registry/meta' require 'resource_registry/setting' @@ -29,4 +29,11 @@ module ResourceRegistry + def self.logger + @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT) + end + + def self.logger=(logger) + @@logger = logger + end end diff --git a/lib/resource_registry/namespace.rb b/lib/resource_registry/namespace.rb index da4d6fc5..9dfddc29 100644 --- a/lib/resource_registry/namespace.rb +++ b/lib/resource_registry/namespace.rb @@ -6,21 +6,20 @@ require_relative 'operations/graphs/create' module ResourceRegistry - # Define a Feature together with its settings, code hook for dependency injection, and configuration UI attributes + # Define a namespace container for organizing features # - # @example Define the feature - # Feature.new(key: :greeter, item: proc { |name| "Hello #{name}!" }) - # Feature.new(key: :logger, item: Logger.new(STDERR), settings: [{default: :warn}]) + # @example Define the namespace + # Namespace.new(key: 'ue46632342', path: [:app, :greeter]) class Namespace < Dry::Struct # @!attribute [r] key (required) - # Identifier for this Feature. Must be unique within a registry namespace + # Identifier for this Namespace. 10 character long MD5 digest key # @return [Symbol] attribute :key, Types::Symbol.meta(omittable: false) - # @!attribute [r] item (required) - # The reference or code to be evaluated when feature is resolved - # @return [Any] + # @!attribute [r] path (required) + # The registry namespace where this item is stored + # @return [Array] attribute :path, Types::Array.of(Types::RequiredSymbol).default([].freeze).meta(omittable: false) # @!attribute [r] meta (optional) @@ -28,10 +27,20 @@ class Namespace < Dry::Struct # @return [ResourceRegistry::Meta] attribute :meta, ResourceRegistry::Meta.default(Hash.new.freeze).meta(omittable: true) + # @!attribute [r] feature_keys (optional) + # Key references to the features under this namespace + # @return [Array] attribute :feature_keys, Types::Array.of(Types::RequiredSymbol).default([].freeze).meta(omittable: true) + # @!attribute [r] features (optional) + # @deprecated Use {feature_keys} instead + # List of full feature definitions under this namespace + # @return [Array] attribute :features, Types::Array.of(::ResourceRegistry::Feature).meta(omittable: true) + # @!attribute [r] namespaces (optional) + # Namespaces that are nested under this namespace + # @return [Array] attribute :namespaces, Types::Array.of(::ResourceRegistry::Namespace).meta(omittable: true) end -end +end \ No newline at end of file diff --git a/lib/resource_registry/navigation.rb b/lib/resource_registry/navigation.rb new file mode 100644 index 00000000..dc9567e7 --- /dev/null +++ b/lib/resource_registry/navigation.rb @@ -0,0 +1,123 @@ +module ResourceRegistry + class Navigation + include ActionView::Helpers::TagHelper + include ActionView::Context + + TAG_OPTION_DEFAULTS = { + ul: { + options: {class: 'nav flex-column flex-nowrap overflow-hidden'} + }, + nested_ul: { + options: {class: 'flex-column nav pl-4'} + }, + li: { + options: {class: 'nav-item'} + }, + a: { + namespace_link: { + options: {class: 'nav-link collapsed text-truncate', 'data-toggle': 'collapse'}, + }, + feature_link: { + options: {'data-remote': true} + } + } + } + + AUTHORIZATION_DEFAULTS = { + authorization_defaults: {} + } + + NAMESPACE_OPTION_DEFAULTS = { + include_all_disabled_features: true, + include_no_features_defined: true, + starting_namespaces: [] # start vertices for graph + } + + OPTION_DEFAULTS = { + tag_options: TAG_OPTION_DEFAULTS, + authorization_options: AUTHORIZATION_DEFAULTS, + namespace_options: NAMESPACE_OPTION_DEFAULTS + } + + attr_reader :options, :namespaces + + def initialize(registry, options = {}) + @registry = registry + @graph = registry[:feature_graph] + @options = OPTION_DEFAULTS.deep_merge(options) + + build_namespaces + end + + def render_html + namespaces.collect{|namespace| to_ul(namespace)}.join('').html_safe + end + + private + + def root_vertices + @graph.vertices_for(options[:namespace_options]) + end + + def build_namespaces + @namespaces = root_vertices.collect{|vertex| to_namespace(vertex)} + end + + def to_namespace(vertex) + namespace_dict = [vertex].inject({}) do |dict, vertex| + dict = vertex.to_h + dict[:features] = dict[:feature_keys].collect{|key| @registry[key].feature.to_h.except(:settings)} + dict[:namespaces] = @graph.adjacent_vertices(vertex).collect{|adjacent_vertex| to_namespace(adjacent_vertex)} + attrs_to_skip = [:feature_keys] + attrs_to_skip << :meta if dict[:meta].empty? + dict.except(*attrs_to_skip) + end + + result = ResourceRegistry::Validation::NamespaceContract.new.call(namespace_dict) + raise "Unable to construct graph due to #{result.errors.to_h}" unless result.success? + result.to_h + end + + def to_ul(vertex, nested = false) + dict = to_namespace(vertex) if vertex.is_a?(ResourceRegistry::Namespace) + + tag.ul(options[:tag_options][(nested ? :nested_ul : :ul)][:options]) do + to_li(dict || vertex) + end + end + + def to_li(element) + tag.li(options[:tag_options][:li][:options]) do + if element[:namespaces] || element[:features] + namespace_nav_link(element) + content_to_expand(element) + else + feature_nav_link(element) + end + end + end + + def content_to_expand(element) + tag.div(class: 'collapse', id: "nav_#{element[:key]}", 'aria-expanded': 'false') do + (element[:features] + element[:namespaces]).reduce('') do |list, child_ele| + list += to_ul(child_ele, true) + end.html_safe + end + end + + def namespace_nav_link(element) + tag.a(options[:tag_options][:a][:namespace_link][:options].merge(href: "#nav_#{element[:key]}", 'data-target': "#nav_#{element[:key]}")) do + tag.span do + element[:namespaces] ? element[:path].last.to_s.titleize : element[:meta][:label] + end + end + end + + def feature_nav_link(element) + tag.a(options[:tag_options][:a][:feature_link][:options].merge(href: element[:item])) do + tag.span do + element[:namespaces] ? element[:path].last.to_s.titleize : element[:meta][:label] + end + end + end + end +end diff --git a/lib/resource_registry/operations/graphs/create.rb b/lib/resource_registry/operations/graphs/create.rb index a0aa807b..ddb08b14 100644 --- a/lib/resource_registry/operations/graphs/create.rb +++ b/lib/resource_registry/operations/graphs/create.rb @@ -6,14 +6,13 @@ module ResourceRegistry module Operations module Graphs - # Create a Feature class Create send(:include, Dry::Monads[:result, :do]) def call(namespaces, registry) - graph = yield create(namespaces, registry) - # yield validate(graph) - Success(graph) + graph = yield create(namespaces, registry) + result = yield validate(graph) + Success(result) end private @@ -33,18 +32,28 @@ def create(namespaces, registry) end end - # validate graph - Success(graph) end def validate(graph) - # graph.acyclic? && directed? - # return error message with cycles when acyclic failed + if graph.directed? && graph.cycles.empty? + Success(graph) + else + errors = [] + errors << 'Graph is not a directed graph' unless graph.directed? + errors << "Graph has cycles: #{print_cycles(graph)}" if graph.cycles.present? + + Failure(errors) + end end - # TODO: Output graph when its successful dotted graph in console/log + def print_cycles(graph) + graph.cycles.collect do |cycle| + cycle.inject([]) {|vertices, vertex| vertices << {namespace: vertex.path, features: vertex.feature_keys}} + end + end + # TODO: Output graph when its successful dotted graph in console/log def namespace_to_vertices(namespace) paths = namespace[:path].dup vertex_path = [] diff --git a/lib/resource_registry/operations/registries/load.rb b/lib/resource_registry/operations/registries/load.rb index eff02080..3369af8f 100644 --- a/lib/resource_registry/operations/registries/load.rb +++ b/lib/resource_registry/operations/registries/load.rb @@ -52,7 +52,12 @@ def merge_namespaces(namespace_list) def register_graph(entities, registry) graph = ResourceRegistry::Operations::Graphs::Create.new.call(entities, registry) - registry.register_graph(graph.value!) + + if graph.success? + registry.register_graph(graph.value!) + else + ResourceRegistry.logger.error(graph.failure) + end Success(registry) end diff --git a/lib/resource_registry/registry.rb b/lib/resource_registry/registry.rb index 9faba7f2..c0d5a70f 100644 --- a/lib/resource_registry/registry.rb +++ b/lib/resource_registry/registry.rb @@ -31,6 +31,10 @@ def configure(&block) ResourceRegistry::Operations::Registries::Configure.new.call(self, config.to_h) end + def navigation(options = {}) + ::ResourceRegistry::Navigation.new(self, options) + end + def swap_feature(feature) self._container.delete("feature_index.#{feature.key}") self._container.delete(namespaced(feature.key, feature.namespace)) @@ -57,6 +61,11 @@ def register_feature(feature) self end + def register_graph(graph) + self._container.delete('feature_graph') if key?('feature_graph') + register('feature_graph', graph) + end + # Look up a feature stored in the registry # @param key [Symbol] unique identifier for the subject feature # @raise [ResourceRegistry::Error::FeatureNotFoundError] if a feature with this key isn't found in the registry diff --git a/lib/resource_registry/rgl.rb b/lib/resource_registry/rgl.rb index 80ace604..7b2ec910 100644 --- a/lib/resource_registry/rgl.rb +++ b/lib/resource_registry/rgl.rb @@ -1,97 +1,25 @@ module RGL class DirectedAdjacencyGraph - include ActionView::Helpers::TagHelper - include ActionView::Context - TAG_OPTION_DEFAULTS = { - ul: { - options: {class: 'nav flex-column flex-nowrap overflow-hidden'} - }, - nested_ul: { - options: {class: 'flex-column nav pl-4'} - }, - li: { - options: {class: 'nav-item'} - }, - a: { - namespace_link: { - options: {class: 'nav-link collapsed text-truncate', 'data-toggle': 'collapse'}, - }, - feature_link: { - options: {'data-remote': true} - } - } - } - - def root_vertices - vertices - edges.map{|edge| edge[1]}.uniq - end - - def tag_options=(options = {}) - @tag_options = TAG_OPTION_DEFAULTS.merge(options) - end - - def tag_options - @tag_options || TAG_OPTION_DEFAULTS + def forest + self end - def to_h(vertex) - namespace_dict = [vertex].inject({}) do |dict, vertex| - dict = vertex.to_h - dict[:features] = dict[:feature_keys].collect{|key| EnrollRegistry[key].feature.to_h.except(:settings)} - dict[:namespaces] = self.adjacent_vertices(vertex).collect{|adjacent_vertex| self.to_h(adjacent_vertex)} - attrs_to_skip = [:feature_keys] - attrs_to_skip << :meta if dict[:meta].empty? - dict.except(*attrs_to_skip) - end - - result = ResourceRegistry::Validation::NamespaceContract.new.call(namespace_dict) - raise "Unable to construct graph due to #{result.errors.to_h}" unless result.success? - result.to_h - end - - def to_ul(vertex, nested = false) - dict = self.to_h(vertex) if vertex.is_a?(ResourceRegistry::Namespace) - - tag.ul(tag_options[(nested ? :nested_ul : :ul)][:options]) do - to_li(dict || vertex) - end - end - - private - - def to_li(element) - tag.li(tag_options[:li][:options]) do - if element[:namespaces] || element[:features] - namespace_nav_link(element) + content_to_expand(element) - else - feature_nav_link(element) - end - end - end - - def content_to_expand(element) - tag.div(class: 'collapse', id: "nav_#{element[:key]}", 'aria-expanded': 'false') do - (element[:features] + element[:namespaces]).reduce('') do |list, child_ele| - list += self.to_ul(child_ele, true) - end.html_safe - end + def trees + vertices - edges.map{|edge| edge[1]}.uniq end - def namespace_nav_link(element) - tag.a(tag_options[:a][:namespace_link][:options].merge(href: "#nav_#{element[:key]}", 'data-target': "#nav_#{element[:key]}")) do - tag.span do - element[:namespaces] ? element[:path].last.to_s.titleize : element[:meta][:label] - end - end + def trees_with_features + edges_with_features = self.edges.select{|edge| edge.to_a.any?{|ele| ele.feature_keys.present?}} + edges_with_features.collect{|edge| edge.source}.uniq - edges_with_features.collect{|edge| edge.target}.uniq end - def feature_nav_link(element) - tag.a(tag_options[:a][:feature_link][:options].merge(href: element[:item])) do - tag.span do - element[:namespaces] ? element[:path].last.to_s.titleize : element[:meta][:label] - end + def vertices_for(options) + if ResourceRegistry::Navigation::NAMESPACE_OPTION_DEFAULTS == options + trees + elsif !options[:include_no_features_defined] + trees_with_features end end end -end +end \ No newline at end of file diff --git a/lib/resource_registry/validation/namespace_contract.rb b/lib/resource_registry/validation/namespace_contract.rb index 4552461d..54f5bf76 100644 --- a/lib/resource_registry/validation/namespace_contract.rb +++ b/lib/resource_registry/validation/namespace_contract.rb @@ -22,10 +22,6 @@ class NamespaceContract < ResourceRegistry::Validation::ApplicationContract optional(:feature_keys).array(:symbol) optional(:features).array(:hash) optional(:namespaces).array(:hash) - - # before(:value_coercer) do |result| - # result.to_h.merge!(meta: result[:meta].symbolize_keys) if result[:meta].is_a? Hash - # end end rule(:features).each do diff --git a/lib/resource_registry/validation/namespace_path_contract.rb b/lib/resource_registry/validation/namespace_path_contract.rb new file mode 100644 index 00000000..9c65d7e5 --- /dev/null +++ b/lib/resource_registry/validation/namespace_path_contract.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module ResourceRegistry + module Validation + # Schema and validation rules for the {ResourceRegistry::Namespace} domain model + class NamespacePathContract < ResourceRegistry::Validation::ApplicationContract + + # @!method call(opts) + # @param [Hash] opts the parameters to validate using this contract + # @option opts [Array] :path required + # @option opts [ResourceRegistry::Meta] :meta optional + # @return [Dry::Monads::Result::Success] if params pass validation + # @return [Dry::Monads::Result::Failure] if params fail validation + params do + required(:path).array(:symbol) + optional(:meta).maybe(:hash) + end + end + end +end \ No newline at end of file diff --git a/spec/rails_app/system/config/templates/features/aca_shop_market/aca_shop_market.yml b/spec/rails_app/system/config/templates/features/aca_shop_market/aca_shop_market.yml index 33709ddf..4f0006ed 100644 --- a/spec/rails_app/system/config/templates/features/aca_shop_market/aca_shop_market.yml +++ b/spec/rails_app/system/config/templates/features/aca_shop_market/aca_shop_market.yml @@ -89,6 +89,10 @@ registry: - features: - key: :employer_sic is_enabled: false + namespace: + - :features + - :enroll_app + - :aca_shop_market settings: - key: :effective_period item: {hours: -15} diff --git a/spec/rails_app/system/config/templates/features/invalid/aca_shop_market.yml b/spec/rails_app/system/config/templates/features/invalid/aca_shop_market.yml new file mode 100644 index 00000000..34cfd880 --- /dev/null +++ b/spec/rails_app/system/config/templates/features/invalid/aca_shop_market.yml @@ -0,0 +1,14 @@ +--- +registry: + - features: + - key: :employer_sic_invalid + is_enabled: false + settings: + - key: :effective_period + item: {hours: -15} + meta: + label: Effective Period + is_required: false + is_visible: true + content_type: :duration + default: {hours: 5} \ No newline at end of file diff --git a/spec/resource_registry/operations/registries/create_spec.rb b/spec/resource_registry/operations/registries/create_spec.rb index 9f3adfeb..9731fbf6 100644 --- a/spec/resource_registry/operations/registries/create_spec.rb +++ b/spec/resource_registry/operations/registries/create_spec.rb @@ -8,15 +8,24 @@ subject { described_class.new.call(path: path, registry: registry) } context 'When valid feature hash passed' do - let(:path) { feature_template_path } let(:registry) { ResourceRegistry::Registry.new } - it "should return success with hash output" do subject expect(subject).to be_a Dry::Monads::Result::Success expect(subject.value!).to be_a ResourceRegistry::Registry end end -end + + context 'When invalid features' do + let(:path) { invalid_feature_template_path } + let(:registry) { ResourceRegistry::Registry.new } + + it "should return success with hash output" do + subject + expect(subject).to be_a Dry::Monads::Result::Failure + expect(subject.failure.errors[:namespace]).to eq ["size cannot be less than 1"] + end + end +end \ No newline at end of file diff --git a/spec/resource_registry/validation/feature_contract_spec.rb b/spec/resource_registry/validation/feature_contract_spec.rb index 6659f93f..ddf5456c 100644 --- a/spec/resource_registry/validation/feature_contract_spec.rb +++ b/spec/resource_registry/validation/feature_contract_spec.rb @@ -40,6 +40,17 @@ expect(result.errors.first.text).to eq error_message end end + + context 'namespace missing' do + let(:invalid_params) { required_params.merge(namespace: nil) } + + it "should should fail validation" do + result = subject.call(invalid_params) + + expect(result.success?).to be_falsey + expect(result.errors[:namespace]).to eq ["size cannot be less than 1"] + end + end end context "Given valid parameters" do diff --git a/spec/support/registry_data_seed.rb b/spec/support/registry_data_seed.rb index 4abb4367..44eb278c 100644 --- a/spec/support/registry_data_seed.rb +++ b/spec/support/registry_data_seed.rb @@ -27,6 +27,10 @@ def configuration_file_path Pathname.pwd.join('spec', 'db', 'seedfiles', 'config', 'config.yml') end + def invalid_feature_template_path + Pathname.pwd.join('spec', 'rails_app', 'system', 'config', 'templates', 'features','invalid', 'aca_shop_market.yml') + end + def feature_template_path Pathname.pwd.join('spec', 'rails_app', 'system', 'config', 'templates', 'features','aca_shop_market', 'aca_shop_market.yml') end From 9090ed48e0f4f575695d4afeb1e9b5008f120225 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Wed, 16 Dec 2020 09:16:00 -0500 Subject: [PATCH 05/40] Added spec fixes --- lib/resource_registry/navigation.rb | 2 ++ .../validation/feature_contract.rb | 4 +-- spec/resource_registry/feature_dsl_spec.rb | 6 ++-- spec/resource_registry/feature_spec.rb | 4 +-- .../operations/registries/create_spec.rb | 2 +- spec/resource_registry/registry_spec.rb | 35 +++++++++---------- .../validation/feature_contract_spec.rb | 16 ++++----- 7 files changed, 35 insertions(+), 34 deletions(-) diff --git a/lib/resource_registry/navigation.rb b/lib/resource_registry/navigation.rb index dc9567e7..4ee14506 100644 --- a/lib/resource_registry/navigation.rb +++ b/lib/resource_registry/navigation.rb @@ -1,3 +1,5 @@ +require 'action_view' + module ResourceRegistry class Navigation include ActionView::Helpers::TagHelper diff --git a/lib/resource_registry/validation/feature_contract.rb b/lib/resource_registry/validation/feature_contract.rb index eb2eeeda..fba387a4 100644 --- a/lib/resource_registry/validation/feature_contract.rb +++ b/lib/resource_registry/validation/feature_contract.rb @@ -18,7 +18,7 @@ class FeatureContract < ResourceRegistry::Validation::ApplicationContract # @return [Dry::Monads::Result::Failure] if params fail validation params do required(:key).value(:symbol) - required(:namespace_path).maybe(:hash) + required(:namespace_path).value(:hash) required(:is_enabled).value(:bool) optional(:item).value(:any) optional(:options).maybe(:hash) @@ -43,7 +43,7 @@ class FeatureContract < ResourceRegistry::Validation::ApplicationContract result.to_h.merge( key: result[:key]&.to_sym, meta: result[:meta]&.symbolize_keys, - namespace_path: result[:namespace_path].deep_symbolize_keys, + namespace_path: result[:namespace_path]&.deep_symbolize_keys, settings: settings || [] ) end diff --git a/spec/resource_registry/feature_dsl_spec.rb b/spec/resource_registry/feature_dsl_spec.rb index 7e35c7c7..e9913a4e 100644 --- a/spec/resource_registry/feature_dsl_spec.rb +++ b/spec/resource_registry/feature_dsl_spec.rb @@ -13,7 +13,7 @@ def call(params) end let(:key) { :greeter_feature } - let(:namespace) { [:level_1, :level_2, :level_3]} + let(:namespace) { {path: [:level_1, :level_2, :level_3]} } let(:is_enabled) { false } let(:item) { Greeter.method(:new) } let(:options) { { name: "Dolly" } } @@ -29,7 +29,7 @@ def call(params) let(:min_feature_hash) do { key: key, - namespace: namespace, + namespace_path: namespace, is_enabled: is_enabled, item: item } @@ -38,7 +38,7 @@ def call(params) let(:feature_hash) do { key: key, - namespace: namespace, + namespace_path: namespace, is_enabled: is_enabled, item: item, options: options, diff --git a/spec/resource_registry/feature_spec.rb b/spec/resource_registry/feature_spec.rb index da28ee00..115600a4 100644 --- a/spec/resource_registry/feature_spec.rb +++ b/spec/resource_registry/feature_spec.rb @@ -13,14 +13,14 @@ def call(params) end let(:key) { :greeter_feature } - let(:namespace) { [:level_1,:level_2,:level_3]} + let(:namespace) { {path: [:level_1,:level_2,:level_3]} } let(:is_enabled) { false } let(:item) { Greeter.new } let(:options) { { name: "Dolly" } } let(:meta) { { label: "label", default: 42, content_type: :integer } } let(:settings) { [{ key: :service, item: "weather/forcast" }, { key: :retries, item: 4 }] } - let(:required_params) { { key: key, namespace: namespace, is_enabled: is_enabled, item: item } } + let(:required_params) { { key: key, namespace_path: namespace, is_enabled: is_enabled, item: item } } let(:optional_params) { { options: options, meta: meta, settings: settings } } let(:all_params) { required_params.merge(optional_params) } diff --git a/spec/resource_registry/operations/registries/create_spec.rb b/spec/resource_registry/operations/registries/create_spec.rb index 9731fbf6..36cf5cdf 100644 --- a/spec/resource_registry/operations/registries/create_spec.rb +++ b/spec/resource_registry/operations/registries/create_spec.rb @@ -25,7 +25,7 @@ it "should return success with hash output" do subject expect(subject).to be_a Dry::Monads::Result::Failure - expect(subject.failure.errors[:namespace]).to eq ["size cannot be less than 1"] + expect(subject.failure.errors[:namespace_path]).to eq ["size cannot be less than 1"] end end end \ No newline at end of file diff --git a/spec/resource_registry/registry_spec.rb b/spec/resource_registry/registry_spec.rb index 3228d13a..165ff8cf 100644 --- a/spec/resource_registry/registry_spec.rb +++ b/spec/resource_registry/registry_spec.rb @@ -60,7 +60,7 @@ def call(params) end let(:key) { :greeter_feature } - let(:namespace) { [:level_1, :level_2, :level_3]} + let(:namespace) { {path: [:level_1, :level_2, :level_3]} } let(:namespace_str) { 'level_1.level_2.level_3'} let(:namespace_key) { namespace_str + '.' + key.to_s } let(:is_enabled) { false } @@ -91,7 +91,7 @@ def call(params) let(:feature_hash) do { key: key, - namespace: namespace, + namespace_path: namespace, is_enabled: is_enabled, item: item, meta: meta @@ -225,13 +225,13 @@ def call(params) context "Given an enabled feature with all ancestors enabled" do let(:boat) do ResourceRegistry::Feature.new(key: :boat, - namespace: [:vessel], + namespace_path: {path: [:vessel]}, is_enabled: true) end let(:sailboat) do ResourceRegistry::Feature.new(key: :sailboat, - namespace: [:vessel, :boat], + namespace_path: {path: [:vessel, :boat]}, is_enabled: true) end @@ -253,7 +253,7 @@ def call(params) context "and an enabled feature with a break in ancestor namespace" do let(:canoe) do ResourceRegistry::Feature.new(key: :canoe, - namespace: [:vessel, :boat, :paddleboat], + namespace_path: {path: [:vessel, :boat, :paddleboat]}, is_enabled: false) end before { registry.register_feature(canoe) } @@ -268,14 +268,14 @@ def call(params) context "Given an ancestor feature is disabled" do let(:powerboat) do ResourceRegistry::Feature.new(key: :powerboat, - namespace: [:vessel, :boat], + namespace_path: {path: [:vessel, :boat]}, is_enabled: false) end context "and a child of that feature is enabled" do let(:trawler) do ResourceRegistry::Feature.new(key: :trawler, - namespace: [:vessel, :boat, :powerboat], + namespace_path: {path: [:vessel, :boat, :powerboat]}, is_enabled: true) end @@ -303,17 +303,16 @@ def call(params) describe '#features_by_namespace' do context "Given features registered in different namespaces" do - let(:sail_ns) { [:vessel, :boat, :sail] } - let(:sail_ns_str) { sail_ns.map(&:to_s).join('.') } - let(:boat_ns) { [:vessel, :boat] } - let(:boat_ns_str) { boat_ns.map(&:to_s).join('.') } - - - let(:ski) { ResourceRegistry::Feature.new(key: :ski, namespace: boat_ns, is_enabled: true) } - let(:trawler) { ResourceRegistry::Feature.new(key: :trawler, namespace: boat_ns, is_enabled: true) } - let(:sloop) { ResourceRegistry::Feature.new(key: :sloop, namespace: sail_ns, is_enabled: true) } - let(:ketch) { ResourceRegistry::Feature.new(key: :ketch, namespace: sail_ns, is_enabled: true) } - let(:yawl) { ResourceRegistry::Feature.new(key: :yawl, namespace: sail_ns, is_enabled: true) } + let(:sail_ns) { {path: [:vessel, :boat, :sail]} } + let(:sail_ns_str) { sail_ns[:path].map(&:to_s).join('.') } + let(:boat_ns) { {path: [:vessel, :boat]} } + let(:boat_ns_str) { boat_ns[:path].map(&:to_s).join('.') } + + let(:ski) { ResourceRegistry::Feature.new(key: :ski, namespace_path: boat_ns, is_enabled: true) } + let(:trawler) { ResourceRegistry::Feature.new(key: :trawler, namespace_path: boat_ns, is_enabled: true) } + let(:sloop) { ResourceRegistry::Feature.new(key: :sloop, namespace_path: sail_ns, is_enabled: true) } + let(:ketch) { ResourceRegistry::Feature.new(key: :ketch, namespace_path: sail_ns, is_enabled: true) } + let(:yawl) { ResourceRegistry::Feature.new(key: :yawl, namespace_path: sail_ns, is_enabled: true) } let(:sail_features) { [:sloop, :ketch, :yawl] } let(:boat_features) { [:ski, :trawler] } diff --git a/spec/resource_registry/validation/feature_contract_spec.rb b/spec/resource_registry/validation/feature_contract_spec.rb index ddf5456c..9bddfc35 100644 --- a/spec/resource_registry/validation/feature_contract_spec.rb +++ b/spec/resource_registry/validation/feature_contract_spec.rb @@ -6,14 +6,14 @@ describe "Feature core parameters" do let(:key) { :my_feature } - let(:namespace) { [:level_1, :level_2, :level_3]} + let(:namespace) { {path: [:level_1, :level_2, :level_3]} } let(:is_enabled) { false } let(:item) { ->(val){ val.to_sym } } let(:options) { { name: "Dolly" } } let(:meta) { { label: "label", default: 42, content_type: :integer } } let(:settings) { [{ key: :service, item: "weather/forecast" }, { key: :retries, item: 4 }] } - let(:required_params) { { key: key, namespace: namespace, is_enabled: is_enabled, item: item } } + let(:required_params) { { key: key, namespace_path: namespace, is_enabled: is_enabled, item: item } } let(:optional_params) { { options: options, meta: meta, settings: settings } } let(:all_params) { required_params.merge(optional_params) } @@ -30,7 +30,7 @@ context "and a non-boolean value is passed to :is_enabled" do let(:invalid_is_enabled) { "blue" } - let(:invalid_params) { { key: key, namespace: namespace, is_enabled: invalid_is_enabled } } + let(:invalid_params) { { key: key, namespace_path: namespace, is_enabled: invalid_is_enabled } } let(:error_message) { "must be boolean" } it "should should fail validation" do @@ -42,13 +42,13 @@ end context 'namespace missing' do - let(:invalid_params) { required_params.merge(namespace: nil) } + let(:invalid_params) { required_params.merge(namespace_path: nil) } it "should should fail validation" do result = subject.call(invalid_params) expect(result.success?).to be_falsey - expect(result.errors[:namespace]).to eq ["size cannot be less than 1"] + expect(result.errors[:namespace_path]).to eq ["must be a hash"] end end end @@ -74,7 +74,7 @@ context "and key is passed as string" do let(:key_string) { "my_feature" } - let(:params) { { key: key_string, namespace: namespace, is_enabled: is_enabled } } + let(:params) { { key: key_string, namespace_path: namespace, is_enabled: is_enabled } } it "should coerce stringified key into symbol" do result = subject.call(params) @@ -85,8 +85,8 @@ end context "and passing namespace values in as strings" do - let(:namespace_strings) { namespace.map(&:to_s) } - let(:params) { { key: key, namespace: namespace_strings, is_enabled: is_enabled, item: item } } + let(:namespace_strings) { {path: ['level_1', 'level_2', 'level_3']} } + let(:params) { { key: key, namespace_path: namespace_strings, is_enabled: is_enabled, item: item } } it "should coerce stringified key into symbol" do result = subject.call(params) From 5b9a7a5b1477eb99ab82e7532efc23ad54bc49a7 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Wed, 16 Dec 2020 13:33:56 -0500 Subject: [PATCH 06/40] Added spec fixes --- lib/resource_registry/feature.rb | 2 +- .../operations/features/create.rb | 4 +- .../operations/features/update.rb | 2 +- .../operations/namespaces/build.rb | 39 +++++++++++++++++++ .../operations/namespaces/create.rb | 32 +++++++++++++++ lib/resource_registry/registry.rb | 2 +- .../validation/feature_contract.rb | 2 +- .../operations/features/update_spec.rb | 1 + .../operations/registries/create_spec.rb | 4 +- spec/resource_registry/registry_spec.rb | 16 ++++---- .../validation/meta_contract_spec.rb | 2 +- 11 files changed, 90 insertions(+), 16 deletions(-) create mode 100644 lib/resource_registry/operations/namespaces/build.rb create mode 100644 lib/resource_registry/operations/namespaces/create.rb diff --git a/lib/resource_registry/feature.rb b/lib/resource_registry/feature.rb index 347c7004..39c139b0 100644 --- a/lib/resource_registry/feature.rb +++ b/lib/resource_registry/feature.rb @@ -24,7 +24,7 @@ class Feature < Dry::Struct # @!attribute [r] namespace (optional) # The registry namespace where this item is stored # @return [Symbol] - attribute :namespace_path, ::ResourceRegistry::NamespacePath.optional.meta(omittable: false) + attribute :namespace_path, ::ResourceRegistry::NamespacePath.meta(omittable: false) # @!attribute [r] is_enabled (required) # Availability state of this Feature in the application: either enabled or disabled diff --git a/lib/resource_registry/operations/features/create.rb b/lib/resource_registry/operations/features/create.rb index e27450e7..a36e493e 100644 --- a/lib/resource_registry/operations/features/create.rb +++ b/lib/resource_registry/operations/features/create.rb @@ -18,7 +18,9 @@ def call(params) private def construct(params) - params['namespace_path'] = params['namespace'].is_a?(Hash) ? params.delete('namespace') : {path: params.delete('namespace')} + if params[:namespace_path].blank? + params['namespace_path'] = params['namespace'].is_a?(Hash) ? params.delete('namespace') : {path: params.delete('namespace')} + end Success(params) end diff --git a/lib/resource_registry/operations/features/update.rb b/lib/resource_registry/operations/features/update.rb index 328b8be9..a9b2e4c6 100644 --- a/lib/resource_registry/operations/features/update.rb +++ b/lib/resource_registry/operations/features/update.rb @@ -38,7 +38,7 @@ def create_entity(params) end def update_model(feature_entity) - if defined?(Rails) + if defined?(Rails) && defined? ResourceRegistry::ActiveRecord feature = ResourceRegistry::ActiveRecord::Feature.where(key: feature_entity.key).first diff --git a/lib/resource_registry/operations/namespaces/build.rb b/lib/resource_registry/operations/namespaces/build.rb new file mode 100644 index 00000000..83651690 --- /dev/null +++ b/lib/resource_registry/operations/namespaces/build.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true +require 'dry/monads' + +module ResourceRegistry + module Operations + module Namespaces + # Create a Feature + class Build + send(:include, Dry::Monads[:result, :do]) + + def call(namespaces = {}, feature) + namespaces = yield construct(namespaces, feature) + # values = yield validate(params) + # namespace = yield build(values) + + Success(namespaces) + end + + private + + def construct(namespaces, feature) + namespace_identifier = feature.namespace.map(&:to_s).join('.') + + if namespaces[namespace_identifier] + namespaces[namespace_identifier][:features] << feature.key + else + namespaces[namespace_identifier] = { + key: feature.namespace[-1], + path: feature.namespace, + features: [feature.key] + } + end + + Success(namespaces) + end + end + end + end +end diff --git a/lib/resource_registry/operations/namespaces/create.rb b/lib/resource_registry/operations/namespaces/create.rb new file mode 100644 index 00000000..666c8a12 --- /dev/null +++ b/lib/resource_registry/operations/namespaces/create.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module ResourceRegistry + module Operations + module Namespaces + + # Create a Namespace + class Create + send(:include, Dry::Monads[:result, :do]) + + def call(params) + values = yield validate(params) + namespace = yield create(values) + + Success(namespace) + end + + private + + def validate(params) + ResourceRegistry::Validation::NamespaceContract.new.call(params) + end + + def create(values) + namespace = ResourceRegistry::Namespace.new(values.to_h) + + Success(namespace) + end + end + end + end +end diff --git a/lib/resource_registry/registry.rb b/lib/resource_registry/registry.rb index c0d5a70f..3fb8a06b 100644 --- a/lib/resource_registry/registry.rb +++ b/lib/resource_registry/registry.rb @@ -37,7 +37,7 @@ def navigation(options = {}) def swap_feature(feature) self._container.delete("feature_index.#{feature.key}") - self._container.delete(namespaced(feature.key, feature.namespace)) + self._container.delete(namespaced(feature.key, feature.namespace_path.path)) register_feature(feature) @features_stale = false end diff --git a/lib/resource_registry/validation/feature_contract.rb b/lib/resource_registry/validation/feature_contract.rb index fba387a4..780b4242 100644 --- a/lib/resource_registry/validation/feature_contract.rb +++ b/lib/resource_registry/validation/feature_contract.rb @@ -39,7 +39,7 @@ class FeatureContract < ResourceRegistry::Validation::ApplicationContract end end end - + result[:namespace_path][:path] = result[:namespace_path][:path].map(&:to_sym) if result[:namespace_path] && result[:namespace_path][:path] result.to_h.merge( key: result[:key]&.to_sym, meta: result[:meta]&.symbolize_keys, diff --git a/spec/resource_registry/operations/features/update_spec.rb b/spec/resource_registry/operations/features/update_spec.rb index 78409886..ea53901a 100644 --- a/spec/resource_registry/operations/features/update_spec.rb +++ b/spec/resource_registry/operations/features/update_spec.rb @@ -20,6 +20,7 @@ { :key => :aca_shop_market, :is_enabled => true, + :namespace_path => {:path=>[:features, :enroll_app]}, :settings => [ { enroll_prior_to_effective_on_max: { days: 10 } }, { enroll_after_effective_on_max: { days: 60 } }, diff --git a/spec/resource_registry/operations/registries/create_spec.rb b/spec/resource_registry/operations/registries/create_spec.rb index 36cf5cdf..744bc589 100644 --- a/spec/resource_registry/operations/registries/create_spec.rb +++ b/spec/resource_registry/operations/registries/create_spec.rb @@ -14,7 +14,7 @@ it "should return success with hash output" do subject expect(subject).to be_a Dry::Monads::Result::Success - expect(subject.value!).to be_a ResourceRegistry::Registry + expect(subject.value![:registry]).to be_a ResourceRegistry::Registry end end @@ -25,7 +25,7 @@ it "should return success with hash output" do subject expect(subject).to be_a Dry::Monads::Result::Failure - expect(subject.failure.errors[:namespace_path]).to eq ["size cannot be less than 1"] + expect(subject.failure.errors[:namespace_path]).to eq [{:text=>"invalid meta", :error=>{:path=>["must be an array"]}}] end end end \ No newline at end of file diff --git a/spec/resource_registry/registry_spec.rb b/spec/resource_registry/registry_spec.rb index 165ff8cf..4942381c 100644 --- a/spec/resource_registry/registry_spec.rb +++ b/spec/resource_registry/registry_spec.rb @@ -127,7 +127,7 @@ def call(params) end context "given feature with no namespace" do - let(:namespace) { [] } + let(:namespace) { {path: []} } it "should register feature under root" do registry.register_feature(feature) @@ -212,15 +212,15 @@ def call(params) end describe '#enabled?' do - context "Given features without a namespace that is enabled" do - let(:vessel) { ResourceRegistry::Feature.new(key: :vessel, is_enabled: true) } + # context "Given features without a namespace that is enabled" do + # let(:vessel) { ResourceRegistry::Feature.new(key: :vessel, is_enabled: true) } - before { registry.register_feature(vessel) } + # before { registry.register_feature(vessel) } - it "the feature should be enabled" do - expect(registry.feature_enabled?(:vessel)).to be_truthy - end - end + # it "the feature should be enabled" do + # expect(registry.feature_enabled?(:vessel)).to be_truthy + # end + # end context "Given an enabled feature with all ancestors enabled" do let(:boat) do diff --git a/spec/resource_registry/validation/meta_contract_spec.rb b/spec/resource_registry/validation/meta_contract_spec.rb index 8c4c26b2..43227085 100644 --- a/spec/resource_registry/validation/meta_contract_spec.rb +++ b/spec/resource_registry/validation/meta_contract_spec.rb @@ -26,7 +26,7 @@ let(:all_params) { required_params.merge(optional_params) } context "Validation with invalid input" do - let(:required_params_error) { { :default => ["is missing"], :label => ["is missing"], :content_type => ["is missing"] } } + let(:required_params_error) { { :label => ["is missing"], :content_type => ["is missing"] } } context "Given hash params are empty" do From 0bdab031e7426ce89805f601a5f3404eb87b280b Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Fri, 18 Dec 2020 11:36:46 -0500 Subject: [PATCH 07/40] Added mongoid support --- .../helpers/view_controls.rb | 59 ++++++++++++------- .../models/mongoid/feature.rb | 15 +++-- lib/resource_registry/models/mongoid/meta.rb | 6 +- .../models/mongoid/namespace_path.rb | 13 ++++ .../models/mongoid/setting.rb | 7 +-- lib/resource_registry/navigation.rb | 8 ++- .../operations/registries/create.rb | 28 ++++++++- lib/resource_registry/railtie.rb | 9 +-- lib/resource_registry/rgl.rb | 8 +-- 9 files changed, 103 insertions(+), 50 deletions(-) create mode 100644 lib/resource_registry/models/mongoid/namespace_path.rb diff --git a/lib/resource_registry/helpers/view_controls.rb b/lib/resource_registry/helpers/view_controls.rb index 3ad6737d..26279ce9 100644 --- a/lib/resource_registry/helpers/view_controls.rb +++ b/lib/resource_registry/helpers/view_controls.rb @@ -4,28 +4,27 @@ module RegistryViewControls def render_feature(feature, form = nil) feature = feature.feature if feature.is_a?(ResourceRegistry::FeatureDSL) tag.div(class: 'card') do - tag.div(class: 'card-header') do + content = tag.div(class: 'card-header') do tag.h4(feature.setting(:label)&.item || feature.key.to_s.titleize) - end + - tag.div(class: 'card-body row') do - tag.div(class: 'col-6') do - content = if ['legend'].include?(feature.meta.content_type.to_s) - form.hidden_field(:is_enabled) - else - build_option_field(feature, form) - end - - (content + feature.settings.collect do |setting| - build_option_field(setting, form) if setting.meta - end.compact.join('')).html_safe - end - end + end + content += tag.div(class: 'card-body') do + # tag.div do + content = if ['legend'].include?(feature.meta.content_type.to_s) + form.hidden_field(:is_enabled) + else + build_option_field(feature, form) + end + (content + feature.settings.collect do |setting| + build_option_field(setting, form).html_safe if setting.meta + end.compact.join.html_safe) + # end + end + content.html_safe end end def build_option_field(option, form) type = option.meta.content_type&.to_sym - input_control = case type when :swatch input_swatch_control(option, form) @@ -207,12 +206,15 @@ def input_text_control(setting, form) meta = setting[:meta] input_value = value_for(setting, form) || setting.item || meta&.default + # aria_describedby = id - is_required = meta&.is_required == false ? meta.is_required : true + + is_required = meta[:is_required] == false ? meta[:is_required] : true placeholder = "Enter #{meta[:label]}".gsub('*','') if meta[:description].blank? # if meta[:attribute] # tag.input(nil, type: "text", value: input_value, id: id, name: form&.object_name.to_s + "[#{id}]",class: "form-control", required: true) # else + tag.input(nil, type: "text", value: input_value, id: id, name: input_name_for(setting, form), placeholder: placeholder, class: "form-control", required: is_required) # end end @@ -375,15 +377,20 @@ def list_group_menu(nested_namespaces = nil, features = nil, options = {}) end end - def list_tab_panels(features, _feature_registry, _options = {}) - tag.div(class: "tab-content", id: "nav-tabContent") do + def list_tab_panels(features, feature_registry, _options = {}) + # tag.div(class: "tab-content", id: "nav-tabContent") do content = '' features.each do |feature_key| - feature = ResourceRegistry::ActiveRecord::Feature.where(key: feature_key).first + feature = if defined? Rails + find_feature(feature_key) + else + feature_registry[feature_key].feature + end + next if feature.blank? - content += tag.div(class: 'tab-pane fade', id: feature_key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature_key}-list") do - form_for(feature, as: 'feature', url: configuration_path(feature), method: :patch, remote: true, authenticity_token: true) do |form| + content += tag.div(id: feature_key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature_key}-list") do + form_for(feature, as: 'feature', url: configuration_update_exchanges_hbx_profiles_path(feature), method: :patch, remote: true, authenticity_token: true) do |form| form.hidden_field(:key) + render_feature(feature, form) + tag.div(class: 'row mt-3') do @@ -399,6 +406,14 @@ def list_tab_panels(features, _feature_registry, _options = {}) end content.html_safe + # end + end + + def find_feature(feature_key) + if defined? ResourceRegistry::Mongoid + ResourceRegistry::Mongoid::Feature.where(key: feature_key).first + else + ResourceRegistry::ActiveRecord::Feature.where(key: feature_key).first end end end diff --git a/lib/resource_registry/models/mongoid/feature.rb b/lib/resource_registry/models/mongoid/feature.rb index 31923b79..490f1215 100644 --- a/lib/resource_registry/models/mongoid/feature.rb +++ b/lib/resource_registry/models/mongoid/feature.rb @@ -3,17 +3,20 @@ module ResourceRegistry module Mongoid class Feature - include Mongoid::Document - include Mongoid::Timestamps + include ::Mongoid::Document + include ::Mongoid::Timestamps field :key, type: Symbol - field :namespace, type: Array field :is_enabled, type: Boolean field :item, type: String - embeds_one :meta, class_name: 'ResourceRegistry::Mongoid::Meta' - embeds_many :settings, class_name: 'ResourceRegistry::Mongoid::Setting' + embeds_one :namespace_path, class_name: '::ResourceRegistry::Mongoid::NamespacePath' + embeds_one :meta, class_name: '::ResourceRegistry::Mongoid::Meta' + embeds_many :settings, class_name: '::ResourceRegistry::Mongoid::Setting' + def setting(key) + settings.detect{|setting| setting.key.to_s == key.to_s} + end end end -end +end \ No newline at end of file diff --git a/lib/resource_registry/models/mongoid/meta.rb b/lib/resource_registry/models/mongoid/meta.rb index 2a760cf5..143cc56a 100644 --- a/lib/resource_registry/models/mongoid/meta.rb +++ b/lib/resource_registry/models/mongoid/meta.rb @@ -3,11 +3,11 @@ module ResourceRegistry module Mongoid class Meta - include Mongoid::Document - include Mongoid::Timestamps + include ::Mongoid::Document + include ::Mongoid::Timestamps field :label, type: String - field :type, type: Symbol + field :content_type, type: Symbol field :default, type: String field :value, type: String field :description, type: String diff --git a/lib/resource_registry/models/mongoid/namespace_path.rb b/lib/resource_registry/models/mongoid/namespace_path.rb new file mode 100644 index 00000000..f271e5b5 --- /dev/null +++ b/lib/resource_registry/models/mongoid/namespace_path.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module ResourceRegistry + module Mongoid + class NamespacePath + include ::Mongoid::Document + include ::Mongoid::Timestamps + + field :path, type: Array + embeds_one :meta, class_name: '::ResourceRegistry::Mongoid::Meta' + end + end +end diff --git a/lib/resource_registry/models/mongoid/setting.rb b/lib/resource_registry/models/mongoid/setting.rb index 871fffb8..a88e74f5 100644 --- a/lib/resource_registry/models/mongoid/setting.rb +++ b/lib/resource_registry/models/mongoid/setting.rb @@ -2,15 +2,14 @@ module ResourceRegistry module Mongoid class Setting - include Mongoid::Document - include Mongoid::Timestamps + include ::Mongoid::Document + include ::Mongoid::Timestamps field :key, type: Symbol field :options, type: Array field :item, type: String - embeds_one :meta, class_name: 'ResourceRegistry::Mongoid::Meta' - + embeds_one :meta, class_name: '::ResourceRegistry::Mongoid::Meta' end end end diff --git a/lib/resource_registry/navigation.rb b/lib/resource_registry/navigation.rb index 4ee14506..24e2a88d 100644 --- a/lib/resource_registry/navigation.rb +++ b/lib/resource_registry/navigation.rb @@ -7,7 +7,7 @@ class Navigation TAG_OPTION_DEFAULTS = { ul: { - options: {class: 'nav flex-column flex-nowrap overflow-hidden'} + options: {class: 'nav flex-column flex-nowrap overflow-auto'} }, nested_ul: { options: {class: 'flex-column nav pl-4'} @@ -20,7 +20,7 @@ class Navigation options: {class: 'nav-link collapsed text-truncate', 'data-toggle': 'collapse'}, }, feature_link: { - options: {'data-remote': true} + options: {class: 'nav-link', 'data-remote': true} } } } @@ -115,7 +115,9 @@ def namespace_nav_link(element) end def feature_nav_link(element) - tag.a(options[:tag_options][:a][:feature_link][:options].merge(href: element[:item])) do + feature_url = element[:item] if element[:item].to_s.match?(/^\/.*$/) + feature_url ||= ('/exchanges/hbx_profiles/edit_feature?feature_key=' + element[:key].to_s) + tag.a(options[:tag_options][:a][:feature_link][:options].merge(href: feature_url)) do tag.span do element[:namespaces] ? element[:path].last.to_s.titleize : element[:meta][:label] end diff --git a/lib/resource_registry/operations/registries/create.rb b/lib/resource_registry/operations/registries/create.rb index 15b3a09f..c4910d58 100644 --- a/lib/resource_registry/operations/registries/create.rb +++ b/lib/resource_registry/operations/registries/create.rb @@ -62,9 +62,9 @@ def create(feature_hashes) def register_features(features, registry) namespaces = [] features.each do |feature| - persist_to_rdbms(feature, registry) + persist_to_dbms(feature, registry) if defined? Rails registry.register_feature(feature) - namespaces << feature_to_namespace(feature) if feature.namespace_path.meta&.content_type.to_s == 'nav' + namespaces << feature_to_namespace(feature) if feature.namespace_path.meta&.content_type.to_s == 'nav' && feature.meta&.label.present? end Success({namespace_list: namespaces, registry: registry}) @@ -79,8 +79,30 @@ def feature_to_namespace(feature) } end + def persist_to_dbms(feature, registry) + if defined?(ResourceRegistry::Mongoid) + persist_to_mongodb(feature, registry) + else + persist_to_rdbms(feature, registry) + end + end + + def persist_to_mongodb(feature, registry) + feature_record = ResourceRegistry::Mongoid::Feature.where(key: feature.key).first + feature_record&.delete + ResourceRegistry::Mongoid::Feature.new(feature.to_h).save + + # if feature_record.blank? + # ResourceRegistry::Mongoid::Feature.new(feature.to_h).save + # else + # feature_record.update_attributes(feature.to_h) + # # # result = ResourceRegistry::Operations::Features::Create.new.call(feature_record.to_h) + # # # feature = result.success if result.success? # TODO: Verify Failure Scenario + # end + end + def persist_to_rdbms(feature, registry) - if defined?(Rails) && registry.db_connection&.table_exists?(:resource_registry_features) + if registry.db_connection&.table_exists?(:resource_registry_features) feature_record = ResourceRegistry::ActiveRecord::Feature.where(key: feature.key).first if feature_record.blank? diff --git a/lib/resource_registry/railtie.rb b/lib/resource_registry/railtie.rb index 9ed7e4e7..04e2025d 100644 --- a/lib/resource_registry/railtie.rb +++ b/lib/resource_registry/railtie.rb @@ -9,11 +9,12 @@ def gem_available?(gem_name, version = nil) installer = Gem::DependencyInstaller.new if gem_available?('mongoid') - # installer.install 'mongoid' + installer.install 'mongoid' - # require 'resource_registry/models/mongoid/feature' - # require 'resource_registry/models/mongoid/setting' - # require 'resource_registry/models/mongoid/meta' + require 'resource_registry/models/mongoid/namespace_path' + require 'resource_registry/models/mongoid/feature' + require 'resource_registry/models/mongoid/setting' + require 'resource_registry/models/mongoid/meta' else installer.install 'activerecord' diff --git a/lib/resource_registry/rgl.rb b/lib/resource_registry/rgl.rb index 7b2ec910..c9181eeb 100644 --- a/lib/resource_registry/rgl.rb +++ b/lib/resource_registry/rgl.rb @@ -15,11 +15,9 @@ def trees_with_features end def vertices_for(options) - if ResourceRegistry::Navigation::NAMESPACE_OPTION_DEFAULTS == options - trees - elsif !options[:include_no_features_defined] - trees_with_features - end + return options[:starting_namespaces]if options[:starting_namespaces].present? + return trees_with_features unless options[:include_no_features_defined] + trees end end end \ No newline at end of file From e14d079d9ed594d52a0b630cfb4fcd76a11ad035 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Tue, 22 Dec 2020 10:37:05 -0500 Subject: [PATCH 08/40] Fixed mongoid models and persistance issues --- .../helpers/view_controls.rb | 5 +++-- .../models/mongoid/feature.rb | 6 +++--- lib/resource_registry/models/mongoid/meta.rb | 1 + .../models/mongoid/namespace_path.rb | 8 ++++++- .../models/mongoid/setting.rb | 4 +++- lib/resource_registry/navigation.rb | 2 +- .../operations/features/update.rb | 21 +++++++++++++------ .../operations/registries/create.rb | 4 ++-- 8 files changed, 35 insertions(+), 16 deletions(-) diff --git a/lib/resource_registry/helpers/view_controls.rb b/lib/resource_registry/helpers/view_controls.rb index 26279ce9..8d968af5 100644 --- a/lib/resource_registry/helpers/view_controls.rb +++ b/lib/resource_registry/helpers/view_controls.rb @@ -10,7 +10,8 @@ def render_feature(feature, form = nil) content += tag.div(class: 'card-body') do # tag.div do content = if ['legend'].include?(feature.meta.content_type.to_s) - form.hidden_field(:is_enabled) + form.hidden_field(:is_enabled) + + form.hidden_field(:namespace, value: feature.namespace_path.dotted_path) else build_option_field(feature, form) end @@ -390,7 +391,7 @@ def list_tab_panels(features, feature_registry, _options = {}) next if feature.blank? content += tag.div(id: feature_key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature_key}-list") do - form_for(feature, as: 'feature', url: configuration_update_exchanges_hbx_profiles_path(feature), method: :patch, remote: true, authenticity_token: true) do |form| + form_for(feature, as: 'feature', url: exchanges_configuration_path(feature), method: :patch, remote: true, authenticity_token: true) do |form| form.hidden_field(:key) + render_feature(feature, form) + tag.div(class: 'row mt-3') do diff --git a/lib/resource_registry/models/mongoid/feature.rb b/lib/resource_registry/models/mongoid/feature.rb index 490f1215..4720753e 100644 --- a/lib/resource_registry/models/mongoid/feature.rb +++ b/lib/resource_registry/models/mongoid/feature.rb @@ -10,9 +10,9 @@ class Feature field :is_enabled, type: Boolean field :item, type: String - embeds_one :namespace_path, class_name: '::ResourceRegistry::Mongoid::NamespacePath' - embeds_one :meta, class_name: '::ResourceRegistry::Mongoid::Meta' - embeds_many :settings, class_name: '::ResourceRegistry::Mongoid::Setting' + embeds_one :meta, as: :metable, class_name: '::ResourceRegistry::Mongoid::Meta', cascade_callbacks: true + embeds_one :namespace_path, class_name: '::ResourceRegistry::Mongoid::NamespacePath', cascade_callbacks: true + embeds_many :settings, class_name: '::ResourceRegistry::Mongoid::Setting', cascade_callbacks: true def setting(key) settings.detect{|setting| setting.key.to_s == key.to_s} diff --git a/lib/resource_registry/models/mongoid/meta.rb b/lib/resource_registry/models/mongoid/meta.rb index 143cc56a..437c33ec 100644 --- a/lib/resource_registry/models/mongoid/meta.rb +++ b/lib/resource_registry/models/mongoid/meta.rb @@ -15,6 +15,7 @@ class Meta field :is_required, type: Boolean field :is_visible, type: Boolean + embedded_in :metable, polymorphic: true end end end diff --git a/lib/resource_registry/models/mongoid/namespace_path.rb b/lib/resource_registry/models/mongoid/namespace_path.rb index f271e5b5..7f2afb34 100644 --- a/lib/resource_registry/models/mongoid/namespace_path.rb +++ b/lib/resource_registry/models/mongoid/namespace_path.rb @@ -7,7 +7,13 @@ class NamespacePath include ::Mongoid::Timestamps field :path, type: Array - embeds_one :meta, class_name: '::ResourceRegistry::Mongoid::Meta' + + embeds_one :meta, as: :metable, class_name: '::ResourceRegistry::Mongoid::Meta', cascade_callbacks: true + embedded_in :feature, class_name: '::ResourceRegistry::Mongoid::Feature' + + def dotted_path + path.map(&:to_s).join('.') + end end end end diff --git a/lib/resource_registry/models/mongoid/setting.rb b/lib/resource_registry/models/mongoid/setting.rb index a88e74f5..1c432176 100644 --- a/lib/resource_registry/models/mongoid/setting.rb +++ b/lib/resource_registry/models/mongoid/setting.rb @@ -9,7 +9,9 @@ class Setting field :options, type: Array field :item, type: String - embeds_one :meta, class_name: '::ResourceRegistry::Mongoid::Meta' + embeds_one :meta, as: :metable, class_name: '::ResourceRegistry::Mongoid::Meta', cascade_callbacks: true + embedded_in :feature, class_name: '::ResourceRegistry::Mongoid::Feature' + end end end diff --git a/lib/resource_registry/navigation.rb b/lib/resource_registry/navigation.rb index 24e2a88d..dac9510d 100644 --- a/lib/resource_registry/navigation.rb +++ b/lib/resource_registry/navigation.rb @@ -116,7 +116,7 @@ def namespace_nav_link(element) def feature_nav_link(element) feature_url = element[:item] if element[:item].to_s.match?(/^\/.*$/) - feature_url ||= ('/exchanges/hbx_profiles/edit_feature?feature_key=' + element[:key].to_s) + feature_url ||= ('/exchanges/configurations/' + element[:key].to_s + '/edit') tag.a(options[:tag_options][:a][:feature_link][:options].merge(href: feature_url)) do tag.span do element[:namespaces] ? element[:path].last.to_s.titleize : element[:meta][:label] diff --git a/lib/resource_registry/operations/features/update.rb b/lib/resource_registry/operations/features/update.rb index a9b2e4c6..01299907 100644 --- a/lib/resource_registry/operations/features/update.rb +++ b/lib/resource_registry/operations/features/update.rb @@ -22,6 +22,7 @@ def call(params) def build_params(params) feature_params = params.deep_symbolize_keys + feature_params[:namespace_path] = {path: params.delete(:namespace).split('.')} feature_params[:settings] = feature_params[:settings].collect do |setting_hash| if setting_hash.is_a?(Hash) {key: setting_hash.keys[0], item: setting_hash.values[0]} @@ -38,15 +39,18 @@ def create_entity(params) end def update_model(feature_entity) - if defined?(Rails) && defined? ResourceRegistry::ActiveRecord - - feature = ResourceRegistry::ActiveRecord::Feature.where(key: feature_entity.key).first + if defined?(Rails) + feature = model_class.where(key: feature_entity.key).first + feature.is_enabled = feature_entity.is_enabled feature_entity.settings.each do |setting_entity| - feature.update(is_enabled: feature_entity.is_enabled) - setting = feature.settings.where(key: setting_entity.key).first - setting.update(item: setting_entity.item) + if setting = feature.settings.detect{|setting| setting.key == setting_entity.key} + setting.item = setting_entity.item + else + feature.settings.build(setting_entity.to_h) + end end + feature.save Success(feature) else @@ -54,6 +58,11 @@ def update_model(feature_entity) end end + def model_class + return ResourceRegistry::Mongoid::Feature if defined? ResourceRegistry::Mongoid + ResourceRegistry::ActiveRecord::Feature + end + def update_registry(new_feature, registry) registered_feature_hash = registry[new_feature.key].feature.to_h registered_feature_hash[:is_enabled] = new_feature.is_enabled diff --git a/lib/resource_registry/operations/registries/create.rb b/lib/resource_registry/operations/registries/create.rb index c4910d58..15260962 100644 --- a/lib/resource_registry/operations/registries/create.rb +++ b/lib/resource_registry/operations/registries/create.rb @@ -89,8 +89,8 @@ def persist_to_dbms(feature, registry) def persist_to_mongodb(feature, registry) feature_record = ResourceRegistry::Mongoid::Feature.where(key: feature.key).first - feature_record&.delete - ResourceRegistry::Mongoid::Feature.new(feature.to_h).save + # feature_record&.delete + ResourceRegistry::Mongoid::Feature.new(feature.to_h).save unless feature_record # if feature_record.blank? # ResourceRegistry::Mongoid::Feature.new(feature.to_h).save From 04daf6e8b9f4bf28e1ea7b3b03af32469fddc4b1 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Mon, 4 Jan 2021 12:44:26 -0500 Subject: [PATCH 09/40] empty setting file should not raise exception --- lib/resource_registry/serializers/features/serialize.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/resource_registry/serializers/features/serialize.rb b/lib/resource_registry/serializers/features/serialize.rb index e1f5f3de..a78633a5 100644 --- a/lib/resource_registry/serializers/features/serialize.rb +++ b/lib/resource_registry/serializers/features/serialize.rb @@ -18,8 +18,10 @@ def call(params) private def transform(params) - features = params['registry'].reduce([]) do |features_list, namespace| + return Success([]) if params.empty? || params['registry'].blank? + features = params['registry'].reduce([]) do |features_list, namespace| + next unless namespace['features'] path = namespace['namespace'] if namespace.key?('namespace') namespace_features = namespace['features'].reduce([]) do |ns_features_list, feature_hash| From a380e1b306f0bbdc7b34d1dd3ac3eee30cb54687 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Mon, 4 Jan 2021 21:25:51 -0500 Subject: [PATCH 10/40] Added operations to persist features --- Gemfile.lock | 10 +- .../helpers/view_controls.rb | 109 ++++++++++-------- lib/resource_registry/navigation.rb | 17 ++- .../operations/features/update.rb | 52 ++------- .../operations/registries/create.rb | 33 +----- lib/resource_registry/stores.rb | 4 + .../stores/active_record/persist.rb | 35 ++++++ .../stores/active_record/update.rb | 38 ++++++ .../stores/container/update.rb | 39 +++++++ .../stores/mongoid/persist.rb | 39 +++++++ .../stores/mongoid/update.rb | 38 ++++++ 11 files changed, 291 insertions(+), 123 deletions(-) create mode 100644 lib/resource_registry/stores/active_record/persist.rb create mode 100644 lib/resource_registry/stores/active_record/update.rb create mode 100644 lib/resource_registry/stores/container/update.rb create mode 100644 lib/resource_registry/stores/mongoid/persist.rb create mode 100644 lib/resource_registry/stores/mongoid/update.rb diff --git a/Gemfile.lock b/Gemfile.lock index 79623493..29ec4d2f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - resource_registry (0.7.0) + resource_registry (0.9.0) bootsnap (~> 1.0) deep_merge (>= 1.0.0) dry-configurable (= 0.9) @@ -16,7 +16,9 @@ PATH mime-types nokogiri (>= 1.9.1) ox (~> 2.0) + pry-byebug rack (>= 1.6.13) + rgl GEM remote: https://rubygems.org/ @@ -107,6 +109,7 @@ GEM concurrent-ruby (~> 1.0) ice_nine (0.11.2) jaro_winkler (1.5.4) + lazy_priority_queue (0.1.1) loofah (2.5.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -150,6 +153,9 @@ GEM thor (>= 0.19.0, < 2.0) rainbow (3.0.0) rake (12.3.3) + rgl (0.5.2) + lazy_priority_queue (~> 0.1.0) + stream (~> 0.5.0) rspec (3.9.0) rspec-core (~> 3.9.0) rspec-expectations (~> 3.9.0) @@ -183,6 +189,7 @@ GEM docile (~> 1.1) simplecov-html (~> 0.11) simplecov-html (0.12.2) + stream (0.5) thor (1.0.1) thread_safe (0.3.6) timecop (0.9.1) @@ -200,7 +207,6 @@ DEPENDENCIES bundler (~> 2.0) database_cleaner (~> 1.7) mongoid (~> 6.0) - pry-byebug rake (~> 12.0) resource_registry! rspec (~> 3.9) diff --git a/lib/resource_registry/helpers/view_controls.rb b/lib/resource_registry/helpers/view_controls.rb index 8d968af5..65e90bf1 100644 --- a/lib/resource_registry/helpers/view_controls.rb +++ b/lib/resource_registry/helpers/view_controls.rb @@ -1,27 +1,18 @@ # frozen_string_literal: true module RegistryViewControls + def render_feature(feature, form = nil) feature = feature.feature if feature.is_a?(ResourceRegistry::FeatureDSL) - tag.div(class: 'card') do - content = tag.div(class: 'card-header') do - tag.h4(feature.setting(:label)&.item || feature.key.to_s.titleize) - end - content += tag.div(class: 'card-body') do - # tag.div do - content = if ['legend'].include?(feature.meta.content_type.to_s) - form.hidden_field(:is_enabled) + - form.hidden_field(:namespace, value: feature.namespace_path.dotted_path) - else - build_option_field(feature, form) - end - (content + feature.settings.collect do |setting| - build_option_field(setting, form).html_safe if setting.meta - end.compact.join.html_safe) - # end - end - content.html_safe - end + content = if ['legend'].include?(feature.meta.content_type.to_s) + form.hidden_field(:is_enabled) + + form.hidden_field(:namespace, value: feature.namespace_path.dotted_path) + else + build_option_field(feature, form) + end + + content += feature.settings.collect{|setting| build_option_field(setting, form).html_safe if setting.meta}.compact.join.html_safe + content.html_safe end def build_option_field(option, form) @@ -45,17 +36,43 @@ def build_option_field(option, form) input_date_control(option, form) when :currency input_currency_control(option, form) + when :toggle_enabled + toggle_enabled_control(option, form) + when :slider_switch + slider_switch_control(option, form) else input_text_control(option, form) end + # else :text_field + # input_text_control(option, form) + # else + # # :dan_check_box + # # find dan_check_box_control helper + # # else + # # custom_helper #catch_all for custom types + # end if [:radio_select, :checkbox_select].include?(type) custom_form_group(option, input_control) else + return input_control if type == :toggle_enabled form_group(option, input_control) end end + def toggle_enabled_control(option, form) + tag.div(class: "form-group") do + tag.label(for: option.key.to_s, value: option.key.to_s.titleize, class: 'pr-2') do + option.key.to_s.titleize + end + + tag.input(nil, id: 'featureToggle', type: "checkbox", checked: option.is_enabled, data: {toggle: 'toggle', style: 'ios'}) + end + end + + def slider_switch_control(option, form) + + end + def select_control(setting, form) id = setting[:key].to_s selected_option = "Choose..." @@ -379,35 +396,35 @@ def list_group_menu(nested_namespaces = nil, features = nil, options = {}) end def list_tab_panels(features, feature_registry, _options = {}) - # tag.div(class: "tab-content", id: "nav-tabContent") do - content = '' - - features.each do |feature_key| - feature = if defined? Rails - find_feature(feature_key) - else - feature_registry[feature_key].feature - end - - next if feature.blank? - content += tag.div(id: feature_key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature_key}-list") do - form_for(feature, as: 'feature', url: exchanges_configuration_path(feature), method: :patch, remote: true, authenticity_token: true) do |form| - form.hidden_field(:key) + - render_feature(feature, form) + - tag.div(class: 'row mt-3') do - tag.div(class: 'col-4') do - form.submit(class: 'btn btn-primary') - end + - tag.div(class: 'col-6') do - tag.div(class: 'flash-message', id: feature_key.to_s + '-alert') - end - end + tag.div(class: 'card') do + # content = tag.div(class: 'card-header') do + # tag.h4(feature.setting(:label)&.item || feature.key.to_s.titleize) + # end + + content = tag.div(class: 'card-body') do + feature_content = '' + features.each do |feature_key| + feature = defined?(Rails) ? find_feature(feature_key) : feature_registry[feature_key].feature + next if feature.blank? + + feature_content += tag.div(id: feature_key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature_key}-list") do + form_for(feature, as: 'feature', url: exchanges_configuration_path(feature), method: :patch, remote: true, authenticity_token: true) do |form| + form.hidden_field(:key) + + render_feature(feature, form) + + tag.div(class: 'row mt-3') do + # tag.div(class: 'col-4') do + # form.submit(class: 'btn btn-primary') + # end + + tag.div(class: 'col-6') do + tag.div(class: 'flash-message', id: feature_key.to_s + '-alert') + end + end + end end end - end - - content.html_safe - # end + feature_content.html_safe + end.html_safe + end end def find_feature(feature_key) diff --git a/lib/resource_registry/navigation.rb b/lib/resource_registry/navigation.rb index dac9510d..38be3019 100644 --- a/lib/resource_registry/navigation.rb +++ b/lib/resource_registry/navigation.rb @@ -32,6 +32,7 @@ class Navigation NAMESPACE_OPTION_DEFAULTS = { include_all_disabled_features: true, include_no_features_defined: true, + active_item: [:aca_shop, :benefit_market_catalogs, :catalog_2019], # TODO starting_namespaces: [] # start vertices for graph } @@ -74,7 +75,6 @@ def to_namespace(vertex) attrs_to_skip << :meta if dict[:meta].empty? dict.except(*attrs_to_skip) end - result = ResourceRegistry::Validation::NamespaceContract.new.call(namespace_dict) raise "Unable to construct graph due to #{result.errors.to_h}" unless result.success? result.to_h @@ -107,9 +107,18 @@ def content_to_expand(element) end def namespace_nav_link(element) - tag.a(options[:tag_options][:a][:namespace_link][:options].merge(href: "#nav_#{element[:key]}", 'data-target': "#nav_#{element[:key]}")) do - tag.span do - element[:namespaces] ? element[:path].last.to_s.titleize : element[:meta][:label] + if element[:meta].blank? || element[:meta][:content_type] == :nav + tag.a(options[:tag_options][:a][:namespace_link][:options].merge(href: "#nav_#{element[:key]}", 'data-target': "#nav_#{element[:key]}")) do + tag.span do + element[:namespaces] ? element[:path].last.to_s.titleize : element[:meta][:label] + end + end + else + namespace_url = "/exchanges/configurations/#{element[:features].first[:key]}/namespace_edit" + tag.a(options[:tag_options][:a][:feature_link][:options].merge(href: namespace_url)) do + tag.span do + element[:namespaces] ? element[:path].last.to_s.titleize : element[:meta][:label] + end end end end diff --git a/lib/resource_registry/operations/features/update.rb b/lib/resource_registry/operations/features/update.rb index 01299907..c0bc7a44 100644 --- a/lib/resource_registry/operations/features/update.rb +++ b/lib/resource_registry/operations/features/update.rb @@ -10,10 +10,10 @@ class Update def call(params) registry = params[:registry] - feature_params = yield build_params(params[:feature].to_h) - feature_entity = yield create_entity(feature_params) - feature = yield update_model(feature_entity) - yield update_registry(feature_entity, registry) + feature_params = yield build_params(params[:feature].to_h) + feature_entity = yield create_entity(feature_params) + feature = yield update_model(feature_entity) if defined? Rails + updated_feature = yield update_registry(feature_entity, registry) Success(feature) end @@ -22,7 +22,7 @@ def call(params) def build_params(params) feature_params = params.deep_symbolize_keys - feature_params[:namespace_path] = {path: params.delete(:namespace).split('.')} + feature_params[:namespace_path] = {path: params.delete(:namespace)&.split('.')} if params[:namespace].present? feature_params[:settings] = feature_params[:settings].collect do |setting_hash| if setting_hash.is_a?(Hash) {key: setting_hash.keys[0], item: setting_hash.values[0]} @@ -39,45 +39,15 @@ def create_entity(params) end def update_model(feature_entity) - if defined?(Rails) - feature = model_class.where(key: feature_entity.key).first - feature.is_enabled = feature_entity.is_enabled - - feature_entity.settings.each do |setting_entity| - if setting = feature.settings.detect{|setting| setting.key == setting_entity.key} - setting.item = setting_entity.item - else - feature.settings.build(setting_entity.to_h) - end - end - feature.save - - Success(feature) - else - Success(feature_entity) + if defined? ResourceRegistry::Mongoid + ResourceRegistry::Stores::Mongoid::Update.new.call(feature_entity) + elsif defined? ResourceRegistry::ActiveRecord + ResourceRegistry::Stores::ActiveRecord::Update.new.call(feature_entity) end end - def model_class - return ResourceRegistry::Mongoid::Feature if defined? ResourceRegistry::Mongoid - ResourceRegistry::ActiveRecord::Feature - end - - def update_registry(new_feature, registry) - registered_feature_hash = registry[new_feature.key].feature.to_h - registered_feature_hash[:is_enabled] = new_feature.is_enabled - - new_feature.settings.each do |setting| - registered_feature_hash[:settings].each do |setting_hash| - setting_hash[:item] = setting.item if setting.key == setting_hash[:key] - end - end - - updated_feature = ResourceRegistry::Operations::Features::Create.new.call(registered_feature_hash).value! - - registry.swap_feature(updated_feature) - - Success(updated_feature) + def update_registry(feature_entity, registry) + ResourceRegistry::Stores::Container::Update.new.call(feature_entity, registry) end end end diff --git a/lib/resource_registry/operations/registries/create.rb b/lib/resource_registry/operations/registries/create.rb index 15260962..d4eeb71e 100644 --- a/lib/resource_registry/operations/registries/create.rb +++ b/lib/resource_registry/operations/registries/create.rb @@ -64,7 +64,7 @@ def register_features(features, registry) features.each do |feature| persist_to_dbms(feature, registry) if defined? Rails registry.register_feature(feature) - namespaces << feature_to_namespace(feature) if feature.namespace_path.meta&.content_type.to_s == 'nav' && feature.meta&.label.present? + namespaces << feature_to_namespace(feature) if %w[feature_list nav].include?(feature.namespace_path.meta&.content_type.to_s) && feature.meta&.label.present? end Success({namespace_list: namespaces, registry: registry}) @@ -81,36 +81,9 @@ def feature_to_namespace(feature) def persist_to_dbms(feature, registry) if defined?(ResourceRegistry::Mongoid) - persist_to_mongodb(feature, registry) + ResourceRegistry::Stores::Mongoid::Persist.new.call(feature, registry) else - persist_to_rdbms(feature, registry) - end - end - - def persist_to_mongodb(feature, registry) - feature_record = ResourceRegistry::Mongoid::Feature.where(key: feature.key).first - # feature_record&.delete - ResourceRegistry::Mongoid::Feature.new(feature.to_h).save unless feature_record - - # if feature_record.blank? - # ResourceRegistry::Mongoid::Feature.new(feature.to_h).save - # else - # feature_record.update_attributes(feature.to_h) - # # # result = ResourceRegistry::Operations::Features::Create.new.call(feature_record.to_h) - # # # feature = result.success if result.success? # TODO: Verify Failure Scenario - # end - end - - def persist_to_rdbms(feature, registry) - if registry.db_connection&.table_exists?(:resource_registry_features) - feature_record = ResourceRegistry::ActiveRecord::Feature.where(key: feature.key).first - - if feature_record.blank? - ResourceRegistry::ActiveRecord::Feature.new(feature.to_h).save - else - result = ResourceRegistry::Operations::Features::Create.new.call(feature_record.to_h) - feature = result.success if result.success? # TODO: Verify Failure Scenario - end + # ResourceRegistry::Stores::ActiveRecord::Persist.new.call(feature, registry) end end end diff --git a/lib/resource_registry/stores.rb b/lib/resource_registry/stores.rb index 4ba571e9..6cef1fba 100644 --- a/lib/resource_registry/stores.rb +++ b/lib/resource_registry/stores.rb @@ -2,3 +2,7 @@ require_relative 'stores/file/read' require_relative 'stores/file/list_path' +require_relative 'stores/container/update' +require_relative 'stores/mongoid/update' +require_relative 'stores/mongoid/persist' +require_relative 'stores/active_record/update' diff --git a/lib/resource_registry/stores/active_record/persist.rb b/lib/resource_registry/stores/active_record/persist.rb new file mode 100644 index 00000000..cadc51b3 --- /dev/null +++ b/lib/resource_registry/stores/active_record/persist.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module ResourceRegistry + module Stores + module ActiveRecord + # Instantiate a new Dry::Container object + class Persist + send(:include, Dry::Monads[:result, :do]) + + # @param [Dry::Struct] feature_entity feature entity object + # @return [ResourceRegistry::Mongoid::Feature] persisted feature record wrapped in Dry::Monads::Result + def call(feature_entity, registry) + feature = yield persist(feature_entity, registry) + + Success(feature) + end + + private + + def persist(feature_entity, registry) + # if registry.db_connection&.table_exists?(:resource_registry_features) + feature = ResourceRegistry::ActiveRecord::Feature.where(key: feature.key).first + feature = ResourceRegistry::ActiveRecord::Feature.new(feature.to_h).save unless feature + # else + # result = ResourceRegistry::Operations::Features::Create.new.call(feature_record.to_h) + # feature = result.success if result.success? # TODO: Verify Failure Scenario + # end + # end + + Success(feature) + end + end + end + end +end diff --git a/lib/resource_registry/stores/active_record/update.rb b/lib/resource_registry/stores/active_record/update.rb new file mode 100644 index 00000000..167b54f9 --- /dev/null +++ b/lib/resource_registry/stores/active_record/update.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module ResourceRegistry + module Stores + module ActiveRecord + # Instantiate a new Dry::Container object + class Update + send(:include, Dry::Monads[:result, :do]) + + # @param [Dry::Struct] feature_entity feature entity object + # @return [ResourceRegistry::Mongoid::Feature] persisted feature record wrapped in Dry::Monads::Result + def call(feature_entity) + feature = yield update(feature_entity) + + Success(feature) + end + + private + + def update(feature_entity) + feature = ResourceRegistry::ActiveRecord::Feature.where(key: feature_entity.key).first + feature.is_enabled = feature_entity.is_enabled + + feature_entity.settings.each do |setting_entity| + if setting = feature.settings.detect{|setting| setting.key == setting_entity.key} + setting.item = setting_entity.item + else + feature.settings.build(setting_entity.to_h) + end + end + feature.save + + Success(feature) + end + end + end + end +end diff --git a/lib/resource_registry/stores/container/update.rb b/lib/resource_registry/stores/container/update.rb new file mode 100644 index 00000000..f177db63 --- /dev/null +++ b/lib/resource_registry/stores/container/update.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module ResourceRegistry + module Stores + module Container + # Instantiate a new Dry::Container object + class Update + send(:include, Dry::Monads[:result, :do]) + + # @param [ResourceRegistry::Entities::Registry] container the container instance to which the constant will be assigned + # @param [String] constant_name the name to assign to the container and its associated dependency injector + # @return [Dry::Container] A non-finalized Dry::Container with associated configuration values wrapped in Dry::Monads::Result + def call(new_feature, container) + updated_feature = yield update(new_feature, container) + + Success(updated_feature) + end + + private + + def update(container, new_feature) + registered_feature_hash = container[new_feature.key].feature.to_h + registered_feature_hash[:is_enabled] = new_feature.is_enabled + + new_feature.settings.each do |setting| + registered_feature_hash[:settings].each do |setting_hash| + setting_hash[:item] = setting.item if setting.key == setting_hash[:key] + end + end + + updated_feature = ResourceRegistry::Operations::Features::Create.new.call(registered_feature_hash).value! + container.swap_feature(updated_feature) + + Success(updated_feature) + end + end + end + end +end diff --git a/lib/resource_registry/stores/mongoid/persist.rb b/lib/resource_registry/stores/mongoid/persist.rb new file mode 100644 index 00000000..85d3d979 --- /dev/null +++ b/lib/resource_registry/stores/mongoid/persist.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module ResourceRegistry + module Stores + module Mongoid + # Instantiate a new Dry::Container object + class Persist + send(:include, Dry::Monads[:result, :do]) + + # @param [ResourceRegistry::Entities::Registry] container the container instance to which the constant will be assigned + # @param [String] constant_name the name to assign to the container and its associated dependency injector + # @return [Dry::Container] A non-finalized Dry::Container with associated configuration values wrapped in Dry::Monads::Result + def call(feature_entity, registry) + feature = yield persist(feature_entity, registry) + + Success(feature) + end + + private + + def persist(feature_entity, registry) + feature = ResourceRegistry::Mongoid::Feature.where(key: feature_entity.key).first + # feature&.delete + feature = ResourceRegistry::Mongoid::Feature.new(feature_entity.to_h).save unless feature + + # if feature.blank? + # ResourceRegistry::Mongoid::Feature.new(feature.to_h).save + # else + # feature.update_attributes(feature.to_h) + # result = ResourceRegistry::Operations::Features::Create.new.call(feature.to_h) + # feature = result.success if result.success? # TODO: Verify Failure Scenario + # end + Success(feature) + end + end + end + end +end + diff --git a/lib/resource_registry/stores/mongoid/update.rb b/lib/resource_registry/stores/mongoid/update.rb new file mode 100644 index 00000000..f23a0495 --- /dev/null +++ b/lib/resource_registry/stores/mongoid/update.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module ResourceRegistry + module Stores + module Mongoid + # Instantiate a new Dry::Container object + class Update + send(:include, Dry::Monads[:result, :do]) + + # @param [Dry::Struct] feature_entity feature entity object + # @return [ResourceRegistry::Mongoid::Feature] persisted feature record wrapped in Dry::Monads::Result + def call(feature_entity) + feature = yield update(feature_entity) + + Success(feature) + end + + private + + def update(feature_entity) + feature = ResourceRegistry::Mongoid::Feature.where(key: feature_entity.key).first + feature.is_enabled = feature_entity.is_enabled + + feature_entity.settings.each do |setting_entity| + if setting = feature.settings.detect{|setting| setting.key == setting_entity.key} + setting.item = setting_entity.item + else + feature.settings.build(setting_entity.to_h) + end + end + feature.save + + Success(feature) + end + end + end + end +end From 0060236650ce8fedb492a9f081012c5f8d7c54fe Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Mon, 4 Jan 2021 23:45:03 -0500 Subject: [PATCH 11/40] Updated contracts and value coercion --- .../operations/features/update.rb | 2 ++ .../operations/registries/create.rb | 7 ++-- .../stores/container/update.rb | 2 +- .../validation/application_contract.rb | 28 +++++++++++----- .../validation/feature_contract.rb | 33 ++++--------------- .../validation/setting_contract.rb | 14 ++++++-- .../validation/feature_contract_spec.rb | 3 +- 7 files changed, 47 insertions(+), 42 deletions(-) diff --git a/lib/resource_registry/operations/features/update.rb b/lib/resource_registry/operations/features/update.rb index c0bc7a44..0381f791 100644 --- a/lib/resource_registry/operations/features/update.rb +++ b/lib/resource_registry/operations/features/update.rb @@ -43,6 +43,8 @@ def update_model(feature_entity) ResourceRegistry::Stores::Mongoid::Update.new.call(feature_entity) elsif defined? ResourceRegistry::ActiveRecord ResourceRegistry::Stores::ActiveRecord::Update.new.call(feature_entity) + else + Success(true) end end diff --git a/lib/resource_registry/operations/registries/create.rb b/lib/resource_registry/operations/registries/create.rb index d4eeb71e..ecf55e5b 100644 --- a/lib/resource_registry/operations/registries/create.rb +++ b/lib/resource_registry/operations/registries/create.rb @@ -64,7 +64,8 @@ def register_features(features, registry) features.each do |feature| persist_to_dbms(feature, registry) if defined? Rails registry.register_feature(feature) - namespaces << feature_to_namespace(feature) if %w[feature_list nav].include?(feature.namespace_path.meta&.content_type.to_s) && feature.meta&.label.present? + namespace_meta = feature.namespace_path.meta + namespaces << feature_to_namespace(feature) if namespace_meta.present? && [:feature_list, :nav].include?(namespace_meta.content_type) end Success({namespace_list: namespaces, registry: registry}) @@ -82,8 +83,8 @@ def feature_to_namespace(feature) def persist_to_dbms(feature, registry) if defined?(ResourceRegistry::Mongoid) ResourceRegistry::Stores::Mongoid::Persist.new.call(feature, registry) - else - # ResourceRegistry::Stores::ActiveRecord::Persist.new.call(feature, registry) + elsif defined? ResourceRegistry::ActiveRecord + ResourceRegistry::Stores::ActiveRecord::Persist.new.call(feature, registry) end end end diff --git a/lib/resource_registry/stores/container/update.rb b/lib/resource_registry/stores/container/update.rb index f177db63..2eda55d3 100644 --- a/lib/resource_registry/stores/container/update.rb +++ b/lib/resource_registry/stores/container/update.rb @@ -18,7 +18,7 @@ def call(new_feature, container) private - def update(container, new_feature) + def update(new_feature, container) registered_feature_hash = container[new_feature.key].feature.to_h registered_feature_hash[:is_enabled] = new_feature.is_enabled diff --git a/lib/resource_registry/validation/application_contract.rb b/lib/resource_registry/validation/application_contract.rb index a684e328..e78bece4 100644 --- a/lib/resource_registry/validation/application_contract.rb +++ b/lib/resource_registry/validation/application_contract.rb @@ -41,11 +41,20 @@ def create_contract_klass(rule_keys) # @!macro ruleeach # Validates a nested array of $0 params # @!method rule(settings) - rule(:settings).each do + rule(:settings) do if key? && value - result = ResourceRegistry::Validation::SettingContract.new.call(value) - # Use dry-validation metadata error form to pass error hash along with text to calling service - key.failure(text: "invalid settings", error: result.errors.to_h) if result && result.failure? + setting_results = value.inject([]) do |results, setting_hash| + result = ResourceRegistry::Validation::SettingContract.new.call(setting_hash) + + if result.failure? + # Use dry-validation metadata error form to pass error hash along with text to calling service + key.failure(text: "invalid settings", error: result.errors.to_h) if result && result.failure? + else + results << result.to_h + end + end + + values.merge!(settings: setting_results) end end @@ -59,12 +68,15 @@ def create_contract_klass(rule_keys) if key? && value result = ResourceRegistry::Validation::MetaContract.new.call(value) - # Use dry-validation error form to pass error hash along with text to calling service - # self.result.to_h.merge!({meta: result.to_h}) - key.failure(text: "invalid meta", error: result.errors.to_h) if result && result.failure? + if result.failure? + # Use dry-validation error form to pass error hash along with text to calling service + # self.result.to_h.merge!({meta: result.to_h}) + key.failure(text: "invalid meta", error: result.errors.to_h) + else + values.merge!(meta: result.to_h) + end end end - end end end diff --git a/lib/resource_registry/validation/feature_contract.rb b/lib/resource_registry/validation/feature_contract.rb index 780b4242..766aa16c 100644 --- a/lib/resource_registry/validation/feature_contract.rb +++ b/lib/resource_registry/validation/feature_contract.rb @@ -24,29 +24,6 @@ class FeatureContract < ResourceRegistry::Validation::ApplicationContract optional(:options).maybe(:hash) optional(:meta).maybe(:hash) optional(:settings).array(:hash) - - before(:value_coercer) do |result| - settings = result[:settings]&.map(&:deep_symbolize_keys)&.collect do |setting| - setting.tap do |setting| - if setting[:meta] && setting[:meta][:content_type] == :duration - setting[:item] = Types::Duration[setting[:item]] - elsif setting[:item].is_a? String - dates = setting[:item].scan(/(\d{4}\-\d{2}\-\d{2})\.\.(\d{4}\-\d{2}\-\d{2})/).flatten - if dates.present? - dates = dates.collect{|str| Date.strptime(str, "%Y-%m-%d") } - setting[:item] = Range.new(*dates) - end - end - end - end - result[:namespace_path][:path] = result[:namespace_path][:path].map(&:to_sym) if result[:namespace_path] && result[:namespace_path][:path] - result.to_h.merge( - key: result[:key]&.to_sym, - meta: result[:meta]&.symbolize_keys, - namespace_path: result[:namespace_path]&.deep_symbolize_keys, - settings: settings || [] - ) - end end # @!macro [attach] rulemacro @@ -58,10 +35,12 @@ class FeatureContract < ResourceRegistry::Validation::ApplicationContract rule(:namespace_path) do if key? && value result = ResourceRegistry::Validation::NamespacePathContract.new.call(value) - - # Use dry-validation error form to pass error hash along with text to calling service - # self.result.to_h.merge!({meta: result.to_h}) - key.failure(text: "invalid meta", error: result.errors.to_h) if result && result.failure? + if result.failure? + # Use dry-validation error form to pass error hash along with text to calling service + key.failure(text: "invalid meta", error: result.errors.to_h) + else + values.merge!(namespace_path: result.to_h) + end end end end diff --git a/lib/resource_registry/validation/setting_contract.rb b/lib/resource_registry/validation/setting_contract.rb index 73b6448f..2558d52f 100644 --- a/lib/resource_registry/validation/setting_contract.rb +++ b/lib/resource_registry/validation/setting_contract.rb @@ -19,8 +19,18 @@ class SettingContract < ResourceRegistry::Validation::ApplicationContract optional(:options).maybe(:hash) optional(:meta).maybe(:hash) - before(:value_coercer) do |result| - result.to_h.merge!(meta: result[:meta].symbolize_keys) if result[:meta].is_a? Hash + before(:value_coercer) do |setting| + item = if setting[:meta] && setting[:meta][:content_type] == :duration + Types::Duration[setting[:item]] + elsif setting[:item].is_a? String + dates = setting[:item].scan(/(\d{4}\-\d{2}\-\d{2})\.\.(\d{4}\-\d{2}\-\d{2})/).flatten + if dates.present? + dates = dates.collect{|str| Date.strptime(str, "%Y-%m-%d") } + Range.new(*dates) + end + end + + setting.to_h.merge(item: item) if item end end diff --git a/spec/resource_registry/validation/feature_contract_spec.rb b/spec/resource_registry/validation/feature_contract_spec.rb index 9bddfc35..1fa356a7 100644 --- a/spec/resource_registry/validation/feature_contract_spec.rb +++ b/spec/resource_registry/validation/feature_contract_spec.rb @@ -54,7 +54,8 @@ end context "Given valid parameters" do - let(:required_attr_defaults) { {meta: nil, settings: []} } + # let(:required_attr_defaults) { {meta: nil, settings: []} } + let(:required_attr_defaults) { {} } let(:required_attr_out) { required_params.merge(required_attr_defaults) } context "and required parameters only" do From 6a5523f45fc998c51f4bc39c44fcfffeb9084ca0 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Mon, 11 Jan 2021 10:24:46 -0500 Subject: [PATCH 12/40] refactored operations for features, namespaces --- .../helpers/view_controls.rb | 14 ++++- lib/resource_registry/models/mongoid/meta.rb | 8 +++ .../operations/features/create.rb | 14 +++-- .../operations/features/update.rb | 22 +++----- .../operations/namespaces/build.rb | 56 ++++++++++++------- .../operations/registries/create.rb | 36 +++++------- .../operations/registries/load.rb | 6 +- lib/resource_registry/stores.rb | 15 ++++- .../stores/mongoid/persist.rb | 52 +++++++++++------ .../stores/mongoid/update.rb | 38 ------------- .../validation/feature_contract.rb | 4 ++ .../validation/feature_contract_spec.rb | 2 +- 12 files changed, 143 insertions(+), 124 deletions(-) delete mode 100644 lib/resource_registry/stores/mongoid/update.rb diff --git a/lib/resource_registry/helpers/view_controls.rb b/lib/resource_registry/helpers/view_controls.rb index 65e90bf1..b92c8ddd 100644 --- a/lib/resource_registry/helpers/view_controls.rb +++ b/lib/resource_registry/helpers/view_controls.rb @@ -55,7 +55,7 @@ def build_option_field(option, form) if [:radio_select, :checkbox_select].include?(type) custom_form_group(option, input_control) else - return input_control if type == :toggle_enabled + return input_control if [:toggle_enabled, :slider_switch].include?(type) form_group(option, input_control) end end @@ -70,7 +70,19 @@ def toggle_enabled_control(option, form) end def slider_switch_control(option, form) + meta = option.meta + # input_value = value_for(option, form) || option.item || meta&.default + tag.div(class: "form-group") do + tag.label(for: option.key.to_s, value: option.key.to_s.titleize, class: 'pr-2') do + option.key.to_s.titleize + end + + tag.input(nil, id: 'settingToggle', type: "checkbox", name: input_name_for(option, form), checked: true, data: {toggle: 'toggle', style: 'ios'}) + + tag.label(class: 'pl-2') { meta&.label } + + tag.a(class: "btn btn-secondary ml-4", role: "button", href: '#') do + "Configure #{meta.value_hash['key'].to_s.titleize}" + end + end end def select_control(setting, form) diff --git a/lib/resource_registry/models/mongoid/meta.rb b/lib/resource_registry/models/mongoid/meta.rb index 437c33ec..d6f87b83 100644 --- a/lib/resource_registry/models/mongoid/meta.rb +++ b/lib/resource_registry/models/mongoid/meta.rb @@ -16,6 +16,14 @@ class Meta field :is_visible, type: Boolean embedded_in :metable, polymorphic: true + + def value_hash + JSON.parse(value) + end + + def value=(val) + super(val.to_json) + end end end end diff --git a/lib/resource_registry/operations/features/create.rb b/lib/resource_registry/operations/features/create.rb index a36e493e..e379171e 100644 --- a/lib/resource_registry/operations/features/create.rb +++ b/lib/resource_registry/operations/features/create.rb @@ -8,9 +8,9 @@ class Create send(:include, Dry::Monads[:result, :do]) def call(params) - feature_params = yield construct(params) - feature_values = yield validate(feature_params) - feature = yield create(feature_values) + params = yield construct(params) + values = yield validate(params) + feature = yield create(values) Success(feature) end @@ -21,6 +21,7 @@ def construct(params) if params[:namespace_path].blank? params['namespace_path'] = params['namespace'].is_a?(Hash) ? params.delete('namespace') : {path: params.delete('namespace')} end + Success(params) end @@ -28,11 +29,12 @@ def validate(params) ResourceRegistry::Validation::FeatureContract.new.call(params) end - def create(feature_values) - feature = ResourceRegistry::Feature.new(feature_values.to_h) + def create(values) + feature = ResourceRegistry::Feature.new(values.to_h) + Success(feature) end end end end -end +end \ No newline at end of file diff --git a/lib/resource_registry/operations/features/update.rb b/lib/resource_registry/operations/features/update.rb index 0381f791..6e0c2369 100644 --- a/lib/resource_registry/operations/features/update.rb +++ b/lib/resource_registry/operations/features/update.rb @@ -11,9 +11,9 @@ def call(params) registry = params[:registry] feature_params = yield build_params(params[:feature].to_h) - feature_entity = yield create_entity(feature_params) - feature = yield update_model(feature_entity) if defined? Rails - updated_feature = yield update_registry(feature_entity, registry) + entity = yield create_entity(feature_params) + feature = yield save_record(entity) if defined? Rails + updated_feature = yield update_registry(entity, registry) Success(feature) end @@ -38,20 +38,14 @@ def create_entity(params) ResourceRegistry::Operations::Features::Create.new.call(params) end - def update_model(feature_entity) - if defined? ResourceRegistry::Mongoid - ResourceRegistry::Stores::Mongoid::Update.new.call(feature_entity) - elsif defined? ResourceRegistry::ActiveRecord - ResourceRegistry::Stores::ActiveRecord::Update.new.call(feature_entity) - else - Success(true) - end + def save_record(entity) + ResourceRegistry::Stores.persist(entity) || Success(entity) end - def update_registry(feature_entity, registry) - ResourceRegistry::Stores::Container::Update.new.call(feature_entity, registry) + def update_registry(entity, registry) + ResourceRegistry::Stores::Container::Update.new.call(entity, registry) end end end end -end +end \ No newline at end of file diff --git a/lib/resource_registry/operations/namespaces/build.rb b/lib/resource_registry/operations/namespaces/build.rb index 83651690..bc805c43 100644 --- a/lib/resource_registry/operations/namespaces/build.rb +++ b/lib/resource_registry/operations/namespaces/build.rb @@ -4,35 +4,51 @@ module ResourceRegistry module Operations module Namespaces - # Create a Feature class Build send(:include, Dry::Monads[:result, :do]) - def call(namespaces = {}, feature) - namespaces = yield construct(namespaces, feature) - # values = yield validate(params) - # namespace = yield build(values) - - Success(namespaces) + NAVIGATION_TYPES = %w[feature_list nav] + + def call(feature) + feature = yield validate(feature) + values = yield build(feature) + + Success(values) end private - def construct(namespaces, feature) - namespace_identifier = feature.namespace.map(&:to_s).join('.') - - if namespaces[namespace_identifier] - namespaces[namespace_identifier][:features] << feature.key - else - namespaces[namespace_identifier] = { - key: feature.namespace[-1], - path: feature.namespace, - features: [feature.key] - } - end + def validate(feature) + return Failure("feature meta can't be empty") if feature.meta.to_h.empty? + return Failure("feature namespace path can't be empty") if feature.namespace_path.to_h.empty? + return Failure("namesapce content type should be #{NAVIGATION_TYPES}") unless NAVIGATION_TYPES.include?(feature.namespace_path.meta&.content_type&.to_s) + Success(feature) + end - Success(namespaces) + def build(feature) + namespace_path = feature.namespace_path + + Success({ + key: namespace_path.path.map(&:to_s).join('_'), + path: namespace_path.path, + feature_keys: [feature.key], + meta: namespace_path.meta.to_h + }) end + + # def construct(namespaces, feature) + # namespace_identifier = feature.namespace.map(&:to_s).join('.') + # if namespaces[namespace_identifier] + # namespaces[namespace_identifier][:features] << feature.key + # else + # namespaces[namespace_identifier] = { + # key: feature.namespace[-1], + # path: feature.namespace, + # features: [feature.key] + # } + # end + # Success(namespaces) + # end end end end diff --git a/lib/resource_registry/operations/registries/create.rb b/lib/resource_registry/operations/registries/create.rb index ecf55e5b..5f7f9942 100644 --- a/lib/resource_registry/operations/registries/create.rb +++ b/lib/resource_registry/operations/registries/create.rb @@ -12,9 +12,10 @@ def call(path:, registry:) params = yield deserialize(file_io) feature_hashes = yield serialize(params) features = yield create(feature_hashes) - values = yield register_features(features, registry) + registry = yield save_and_register_feature(features, registry) + namespaces = yield build_namespaces(features) - Success(values) + Success(namespace_list: namespaces, registry: registry) end private @@ -59,33 +60,22 @@ def create(feature_hashes) }.to_result end - def register_features(features, registry) - namespaces = [] + def save_and_register_feature(features, registry) features.each do |feature| - persist_to_dbms(feature, registry) if defined? Rails + ResourceRegistry::Stores.persist(feature) if defined? Rails registry.register_feature(feature) - namespace_meta = feature.namespace_path.meta - namespaces << feature_to_namespace(feature) if namespace_meta.present? && [:feature_list, :nav].include?(namespace_meta.content_type) end - Success({namespace_list: namespaces, registry: registry}) + Success(registry) end - def feature_to_namespace(feature) - { - key: feature.namespace_path.path.map(&:to_s).join('_'), - path: feature.namespace_path.path, - feature_keys: [feature.key], - meta: feature.namespace_path.meta.to_h - } - end - - def persist_to_dbms(feature, registry) - if defined?(ResourceRegistry::Mongoid) - ResourceRegistry::Stores::Mongoid::Persist.new.call(feature, registry) - elsif defined? ResourceRegistry::ActiveRecord - ResourceRegistry::Stores::ActiveRecord::Persist.new.call(feature, registry) - end + def build_namespaces(features) + Try { + features.collect do |feature| + namespace = ResourceRegistry::Operations::Namespaces::Build.new.call(feature) + namespace.success if namespace.success? + end.compact + }.to_result end end end diff --git a/lib/resource_registry/operations/registries/load.rb b/lib/resource_registry/operations/registries/load.rb index 3369af8f..af2ee75f 100644 --- a/lib/resource_registry/operations/registries/load.rb +++ b/lib/resource_registry/operations/registries/load.rb @@ -29,12 +29,10 @@ def load(paths, registry) paths.value!.each do |path| result = ResourceRegistry::Operations::Registries::Create.new.call(path: path, registry: registry) - if result.success? - namespaces_list << result.success[:namespace_list] - end + namespaces_list << result.success[:namespace_list] if result.success? end - Success({registry: registry, namespace_list: namespaces_list.flatten}) + Success(registry: registry, namespace_list: namespaces_list.flatten) end def merge_namespaces(namespace_list) diff --git a/lib/resource_registry/stores.rb b/lib/resource_registry/stores.rb index 6cef1fba..e7601eed 100644 --- a/lib/resource_registry/stores.rb +++ b/lib/resource_registry/stores.rb @@ -3,6 +3,19 @@ require_relative 'stores/file/read' require_relative 'stores/file/list_path' require_relative 'stores/container/update' -require_relative 'stores/mongoid/update' require_relative 'stores/mongoid/persist' require_relative 'stores/active_record/update' + + +module ResourceRegistry + module Stores + + def self.persist(entity) + if defined?(ResourceRegistry::Mongoid) + ResourceRegistry::Stores::Mongoid::Persist.new.call(entity) + elsif defined? ResourceRegistry::ActiveRecord + ResourceRegistry::Stores::ActiveRecord::Persist.new.call(entity) + end + end + end +end diff --git a/lib/resource_registry/stores/mongoid/persist.rb b/lib/resource_registry/stores/mongoid/persist.rb index 85d3d979..9bfce850 100644 --- a/lib/resource_registry/stores/mongoid/persist.rb +++ b/lib/resource_registry/stores/mongoid/persist.rb @@ -5,33 +5,53 @@ module Stores module Mongoid # Instantiate a new Dry::Container object class Persist - send(:include, Dry::Monads[:result, :do]) + send(:include, Dry::Monads[:result, :do, :try]) # @param [ResourceRegistry::Entities::Registry] container the container instance to which the constant will be assigned # @param [String] constant_name the name to assign to the container and its associated dependency injector # @return [Dry::Container] A non-finalized Dry::Container with associated configuration values wrapped in Dry::Monads::Result - def call(feature_entity, registry) - feature = yield persist(feature_entity, registry) - + def call(entity) + feature = yield find(entity) + feature = yield persist(entity, feature) + Success(feature) end private - def persist(feature_entity, registry) - feature = ResourceRegistry::Mongoid::Feature.where(key: feature_entity.key).first - # feature&.delete - feature = ResourceRegistry::Mongoid::Feature.new(feature_entity.to_h).save unless feature - - # if feature.blank? - # ResourceRegistry::Mongoid::Feature.new(feature.to_h).save - # else - # feature.update_attributes(feature.to_h) - # result = ResourceRegistry::Operations::Features::Create.new.call(feature.to_h) - # feature = result.success if result.success? # TODO: Verify Failure Scenario - # end + def find(entity) + feature = ResourceRegistry::Mongoid::Feature.where(key: entity.key).first + Success(feature) end + + def persist(entity, feature) + if feature.blank? + create(entity) + else + update(entity, feature) + end + end + + def create(entity) + Try { + ResourceRegistry::Mongoid::Feature.new(entity.to_h).save + }.to_result + end + + def update(entity, feature) + Try { + feature.is_enabled = entity.is_enabled + entity.settings.each do |setting_entity| + if setting = feature.settings.detect{|setting| setting.key == setting_entity.key} + setting.item = setting_entity.item + else + feature.settings.build(setting_entity.to_h) + end + end + feature.save + }.to_result + end end end end diff --git a/lib/resource_registry/stores/mongoid/update.rb b/lib/resource_registry/stores/mongoid/update.rb deleted file mode 100644 index f23a0495..00000000 --- a/lib/resource_registry/stores/mongoid/update.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module ResourceRegistry - module Stores - module Mongoid - # Instantiate a new Dry::Container object - class Update - send(:include, Dry::Monads[:result, :do]) - - # @param [Dry::Struct] feature_entity feature entity object - # @return [ResourceRegistry::Mongoid::Feature] persisted feature record wrapped in Dry::Monads::Result - def call(feature_entity) - feature = yield update(feature_entity) - - Success(feature) - end - - private - - def update(feature_entity) - feature = ResourceRegistry::Mongoid::Feature.where(key: feature_entity.key).first - feature.is_enabled = feature_entity.is_enabled - - feature_entity.settings.each do |setting_entity| - if setting = feature.settings.detect{|setting| setting.key == setting_entity.key} - setting.item = setting_entity.item - else - feature.settings.build(setting_entity.to_h) - end - end - feature.save - - Success(feature) - end - end - end - end -end diff --git a/lib/resource_registry/validation/feature_contract.rb b/lib/resource_registry/validation/feature_contract.rb index 766aa16c..c53ab8f6 100644 --- a/lib/resource_registry/validation/feature_contract.rb +++ b/lib/resource_registry/validation/feature_contract.rb @@ -24,6 +24,10 @@ class FeatureContract < ResourceRegistry::Validation::ApplicationContract optional(:options).maybe(:hash) optional(:meta).maybe(:hash) optional(:settings).array(:hash) + + before(:value_coercer) do |feature| + feature.to_h.merge(meta: nil) if feature[:meta].blank? + end end # @!macro [attach] rulemacro diff --git a/spec/resource_registry/validation/feature_contract_spec.rb b/spec/resource_registry/validation/feature_contract_spec.rb index 1fa356a7..4d581165 100644 --- a/spec/resource_registry/validation/feature_contract_spec.rb +++ b/spec/resource_registry/validation/feature_contract_spec.rb @@ -55,7 +55,7 @@ context "Given valid parameters" do # let(:required_attr_defaults) { {meta: nil, settings: []} } - let(:required_attr_defaults) { {} } + let(:required_attr_defaults) { {meta: nil} } let(:required_attr_out) { required_params.merge(required_attr_defaults) } context "and required parameters only" do From b4a99e94fe735b9cff7380b47b49065057457802 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Mon, 18 Jan 2021 19:24:47 -0500 Subject: [PATCH 13/40] Added namespace operations to render/update multiple features --- .../helpers/view_controls.rb | 186 ++++++++++++++---- .../models/mongoid/feature.rb | 8 + .../models/mongoid/setting.rb | 7 + lib/resource_registry/namespace.rb | 7 + .../operations/features/update.rb | 23 ++- .../operations/graphs/create.rb | 1 + .../operations/namespaces/form.rb | 57 ++++++ .../operations/namespaces/update_features.rb | 57 ++++++ .../operations/registries/load.rb | 4 +- lib/resource_registry/stores.rb | 36 +++- .../stores/active_record/find.rb | 29 +++ .../stores/active_record/persist.rb | 6 +- lib/resource_registry/stores/mongoid/find.rb | 29 +++ .../stores/mongoid/persist.rb | 1 + .../validation/namespace_contract.rb | 18 +- .../aca_shop_market/model_features.yml | 66 +++++++ .../operations/namespaces/form_spec.rb | 28 +++ .../namespaces/update_features_spec.rb | 44 +++++ .../validation/namespace_contract_spec.rb | 0 19 files changed, 549 insertions(+), 58 deletions(-) create mode 100644 lib/resource_registry/operations/namespaces/form.rb create mode 100644 lib/resource_registry/operations/namespaces/update_features.rb create mode 100644 lib/resource_registry/stores/active_record/find.rb create mode 100644 lib/resource_registry/stores/mongoid/find.rb create mode 100644 spec/rails_app/system/config/templates/features/aca_shop_market/model_features.yml create mode 100644 spec/resource_registry/operations/namespaces/form_spec.rb create mode 100644 spec/resource_registry/operations/namespaces/update_features_spec.rb create mode 100644 spec/resource_registry/validation/namespace_contract_spec.rb diff --git a/lib/resource_registry/helpers/view_controls.rb b/lib/resource_registry/helpers/view_controls.rb index b92c8ddd..70f2b79a 100644 --- a/lib/resource_registry/helpers/view_controls.rb +++ b/lib/resource_registry/helpers/view_controls.rb @@ -4,18 +4,43 @@ module RegistryViewControls def render_feature(feature, form = nil) feature = feature.feature if feature.is_a?(ResourceRegistry::FeatureDSL) - content = if ['legend'].include?(feature.meta.content_type.to_s) - form.hidden_field(:is_enabled) + + return render_attributes_feature(feature, form) if feature.meta.content_type == :model_attributes + content = form.hidden_field(:is_enabled) + content += if ['legend'].include?(feature.meta.content_type.to_s) form.hidden_field(:namespace, value: feature.namespace_path.dotted_path) else build_option_field(feature, form) end - content += feature.settings.collect{|setting| build_option_field(setting, form).html_safe if setting.meta}.compact.join.html_safe content.html_safe end - def build_option_field(option, form) + def render_attributes_feature(feature, form) + item = JSON.parse(feature.item) + model_klass = item['class_name'].constantize + + record = if item['scope'].present? + model_klass.send(item['scope']['name'], *item['scope']['arguments']) + elsif item['where'].present? + criteria = item['where']['arguments'] + model_klass.where(criteria) + end + + content = form.hidden_field(:is_enabled) + + form.hidden_field(:namespace, value: feature.namespace_path.path.map(&:to_s).join('.')) + + + feature.settings.each_with_index do |setting, index| + next unless setting.meta + content += form.fields_for :settings, setting, {index: index} do |setting_form| + build_option_field(setting, setting_form, {record: record}) + end + end + + content.html_safe + end + + def build_option_field(option, form, attrs = {}) type = option.meta.content_type&.to_sym input_control = case type when :swatch @@ -36,12 +61,12 @@ def build_option_field(option, form) input_date_control(option, form) when :currency input_currency_control(option, form) - when :toggle_enabled - toggle_enabled_control(option, form) + when :feature_enabled + feature_enabled_control(option, form) when :slider_switch slider_switch_control(option, form) else - input_text_control(option, form) + input_text_control(option, form, attrs) end # else :text_field # input_text_control(option, form) @@ -55,36 +80,65 @@ def build_option_field(option, form) if [:radio_select, :checkbox_select].include?(type) custom_form_group(option, input_control) else - return input_control if [:toggle_enabled, :slider_switch].include?(type) + return input_control if [:feature_enabled, :slider_switch].include?(type) form_group(option, input_control) end end - def toggle_enabled_control(option, form) + def feature_enabled_control(option, form) tag.div(class: "form-group") do - tag.label(for: option.key.to_s, value: option.key.to_s.titleize, class: 'pr-2') do - option.key.to_s.titleize - end + - tag.input(nil, id: 'featureToggle', type: "checkbox", checked: option.is_enabled, data: {toggle: 'toggle', style: 'ios'}) + content = option.key.to_s.titleize + content += tag.label(class: 'switch') do + tag.input(type: 'hidden', value: option.key, name: 'feature_key') + + tag.input(type: "checkbox", checked: option.is_enabled) + + tag.span(class: "slider") + end + + content += tag.div(class: "spinner-border d-none text-success", role: "status") do + tag.span(class: "sr-only") do + "Loading..." + end + end + + content.html_safe end end def slider_switch_control(option, form) - meta = option.meta - # input_value = value_for(option, form) || option.item || meta&.default - tag.div(class: "form-group") do - tag.label(for: option.key.to_s, value: option.key.to_s.titleize, class: 'pr-2') do - option.key.to_s.titleize - end + - tag.input(nil, id: 'settingToggle', type: "checkbox", name: input_name_for(option, form), checked: true, data: {toggle: 'toggle', style: 'ios'}) + - tag.label(class: 'pl-2') { meta&.label } + - tag.a(class: "btn btn-secondary ml-4", role: "button", href: '#') do - "Configure #{meta.value_hash['key'].to_s.titleize}" + content = option.key.to_s.titleize + content += tag.label(class: 'switch') do + tag.input(type: 'hidden', value: option.key, name: 'feature_key') + + tag.input(type: "checkbox", checked: option.item) + + tag.span(class: "slider") end + + # content += tag.div(class: "spinner-border d-none text-success", role: "status") do + # tag.span(class: "sr-only") do + # "Loading..." + # end + # end + + content.html_safe end end + # def slider_switch_control(option, form) + # meta = option.meta + # # input_value = value_for(option, form) || option.item || meta&.default + + # tag.div(class: "form-group") do + # tag.label(for: option.key.to_s, value: option.key.to_s.titleize, class: 'pr-2') do + # option.key.to_s.titleize + # end + + # tag.input(nil, id: 'settingToggle', type: "checkbox", name: input_name_for(option, form), checked: true, data: {toggle: 'toggle', style: 'ios'}) + + # tag.label(class: 'pl-2') { meta&.label } + + # tag.a(class: "btn btn-secondary ml-4", role: "button", href: '#') do + # "Configure #{meta.value_hash['key'].to_s.titleize}" + # end + # end + # end + def select_control(setting, form) id = setting[:key].to_s selected_option = "Choose..." @@ -212,7 +266,12 @@ def input_group tag.div(yield, class: "input-group") end - def value_for(setting, form) + def value_for(setting, form, options = {}) + if options[:record].present? + item = JSON.parse(setting.item) + return options[:record].send(item['attribute']) + end + value = if form.object.class.to_s.match(/^ResourceRegistry.*/).present? form.object.settings.where(key: setting.key).first&.item else @@ -225,17 +284,21 @@ def value_for(setting, form) def input_name_for(setting, form) if form.object.class.to_s.match(/^ResourceRegistry.*/).present? - form&.object_name.to_s + "[settings][#{setting.key}]" + if form.index.present? + form&.object_name.to_s + "[#{form.index}][#{setting.key}]" + else + form&.object_name.to_s + "[settings][#{setting.key}]" + end else form&.object_name.to_s + "[#{setting.key}]" end end - def input_text_control(setting, form) + def input_text_control(setting, form, options = {}) id = setting[:key].to_s meta = setting[:meta] - input_value = value_for(setting, form) || setting.item || meta&.default + input_value = value_for(setting, form, options) || setting.item || meta&.default # aria_describedby = id @@ -424,9 +487,9 @@ def list_tab_panels(features, feature_registry, _options = {}) form.hidden_field(:key) + render_feature(feature, form) + tag.div(class: 'row mt-3') do - # tag.div(class: 'col-4') do - # form.submit(class: 'btn btn-primary') - # end + + tag.div(class: 'col-4') do + form.submit(class: 'btn btn-primary') + end + tag.div(class: 'col-6') do tag.div(class: 'flash-message', id: feature_key.to_s + '-alert') end @@ -439,11 +502,62 @@ def list_tab_panels(features, feature_registry, _options = {}) end end - def find_feature(feature_key) - if defined? ResourceRegistry::Mongoid - ResourceRegistry::Mongoid::Feature.where(key: feature_key).first - else - ResourceRegistry::ActiveRecord::Feature.where(key: feature_key).first + def feature_panel(feature_key, feature_registry, _options = {}) + tag.div(class: 'card') do + content = tag.div(class: 'card-body') do + feature = defined?(Rails) ? find_feature(feature_key) : feature_registry[feature_key].feature + next if feature.blank? + tag.div(id: feature_key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature_key}-list") do + form_for(feature, as: 'feature', url: update_feature_exchanges_configuration_path(feature), method: :post, remote: true, authenticity_token: true) do |form| + form.hidden_field(:key) + + render_feature(feature, form) + + tag.div(class: 'row mt-3') do + tag.div(class: 'col-4') do + form.submit(class: 'btn btn-primary') + end + + tag.div(class: 'col-6') do + tag.div(class: 'flash-message', id: feature_key.to_s + '-alert') + end + end + end + end.html_safe + end + end + end + + def namespace_panel(namespace, feature_registry, _options = {}) + tag.div(class: 'card') do + tag.div(class: 'card-body') do + form_for(namespace, as: 'namespace', url: update_namespace_exchanges_configurations_path, method: :post, remote: true, authenticity_token: true) do |form| + namespace_content = form.hidden_field(:path, value: namespace.path.map(&:to_s).join('.')) + + namespace.features.each_with_index do |feature, index| + namespace_content += form.fields_for :features, feature, {index: index} do |feature_form| + tag.div(id: feature.key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature.key}-list") do + feature_form.hidden_field(:key) + + render_feature(feature, feature_form) + end + end + end + + namespace_content += tag.div(class: 'row mt-3') do + tag.div(class: 'col-4') do + form.submit(class: 'btn btn-primary') + end + + tag.div(class: 'col-6') do + tag.div(class: 'flash-message', id: namespace.path.map(&:to_s).join('-') + '-alert') + end + end + + namespace_content.html_safe + end + end.html_safe end end -end + + def find_feature(feature_key) + feature_class = ResourceRegistry::Stores.feature_model + return unless feature_class + feature_class.where(key: feature_key).first + end +end \ No newline at end of file diff --git a/lib/resource_registry/models/mongoid/feature.rb b/lib/resource_registry/models/mongoid/feature.rb index 4720753e..f1b6f287 100644 --- a/lib/resource_registry/models/mongoid/feature.rb +++ b/lib/resource_registry/models/mongoid/feature.rb @@ -17,6 +17,14 @@ class Feature def setting(key) settings.detect{|setting| setting.key.to_s == key.to_s} end + + def item + JSON.parse(super) if super.present? + end + + def item=(value) + write_attribute(:item, value.to_json) + end end end end \ No newline at end of file diff --git a/lib/resource_registry/models/mongoid/setting.rb b/lib/resource_registry/models/mongoid/setting.rb index 1c432176..b782bb5b 100644 --- a/lib/resource_registry/models/mongoid/setting.rb +++ b/lib/resource_registry/models/mongoid/setting.rb @@ -12,6 +12,13 @@ class Setting embeds_one :meta, as: :metable, class_name: '::ResourceRegistry::Mongoid::Meta', cascade_callbacks: true embedded_in :feature, class_name: '::ResourceRegistry::Mongoid::Feature' + def item + JSON.parse(super) if super.present? + end + + def item=(value) + write_attribute(:item, value.to_json) + end end end end diff --git a/lib/resource_registry/namespace.rb b/lib/resource_registry/namespace.rb index 9dfddc29..dfe27e1c 100644 --- a/lib/resource_registry/namespace.rb +++ b/lib/resource_registry/namespace.rb @@ -3,6 +3,8 @@ require_relative 'validation/namespace_contract' require_relative 'operations/namespaces/build' require_relative 'operations/namespaces/create' +require_relative 'operations/namespaces/form' +require_relative 'operations/namespaces/update_features' require_relative 'operations/graphs/create' module ResourceRegistry @@ -42,5 +44,10 @@ class Namespace < Dry::Struct # Namespaces that are nested under this namespace # @return [Array] attribute :namespaces, Types::Array.of(::ResourceRegistry::Namespace).meta(omittable: true) + + + def persisted? + false + end end end \ No newline at end of file diff --git a/lib/resource_registry/operations/features/update.rb b/lib/resource_registry/operations/features/update.rb index 6e0c2369..139dcf27 100644 --- a/lib/resource_registry/operations/features/update.rb +++ b/lib/resource_registry/operations/features/update.rb @@ -8,28 +8,37 @@ class Update send(:include, Dry::Monads[:result, :do]) def call(params) - registry = params[:registry] - - feature_params = yield build_params(params[:feature].to_h) + feature_params = yield build_params(params[:feature].to_h, params[:registry]) entity = yield create_entity(feature_params) feature = yield save_record(entity) if defined? Rails - updated_feature = yield update_registry(entity, registry) + updated_feature = yield update_registry(entity, params[:registry]) - Success(feature) + Success(feature || entity) end private - def build_params(params) + def build_params(params, registry) + return feature_toggle_params(params, registry) if params[:toggle_feature] + feature_params = params.deep_symbolize_keys feature_params[:namespace_path] = {path: params.delete(:namespace)&.split('.')} if params[:namespace].present? - feature_params[:settings] = feature_params[:settings].collect do |setting_hash| + feature_params[:settings] = feature_params[:settings]&.collect do |setting_hash| if setting_hash.is_a?(Hash) {key: setting_hash.keys[0], item: setting_hash.values[0]} else {key: setting_hash[0], item: setting_hash[1]} end end + feature_params[:settings] ||= [] + + Success(feature_params) + end + + def feature_toggle_params(params, registry) + feature_params = ResourceRegistry::Stores.find(params[:key])&.success&.attributes&.deep_symbolize_keys + feature_params ||= registry[params[:key]].feature.to_h + feature_params[:is_enabled] = params[:is_enabled] Success(feature_params) end diff --git a/lib/resource_registry/operations/graphs/create.rb b/lib/resource_registry/operations/graphs/create.rb index ddb08b14..833b448c 100644 --- a/lib/resource_registry/operations/graphs/create.rb +++ b/lib/resource_registry/operations/graphs/create.rb @@ -2,6 +2,7 @@ require 'dry/monads' require 'rgl/adjacency' require 'rgl/implicit' +require 'digest/md5' module ResourceRegistry module Operations diff --git a/lib/resource_registry/operations/namespaces/form.rb b/lib/resource_registry/operations/namespaces/form.rb new file mode 100644 index 00000000..7c4b5ce0 --- /dev/null +++ b/lib/resource_registry/operations/namespaces/form.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module ResourceRegistry + module Operations + module Namespaces + + # Create a Namespace + class Form + send(:include, Dry::Monads[:result, :do, :try]) + + def call(namespace:, registry:) + features = yield find_features(namespace, registry) + params = yield construct_params(namespace, features) + values = yield validate(params) + namespace = yield create(values) + + Success(namespace) + end + + private + + def find_features(namespace, registry) + feature_keys = registry[:feature_graph].vertices.detect{|v| v.path == namespace}.feature_keys + features = feature_keys.collect{|feature_key| find_feature(feature_key, registry)} + + Success(features) + end + + def construct_params(namespace, features) + params = { + key: namespace.last, + path: namespace, + feature_keys: features.collect{|feature| feature[:key]}, + features: features + } + + Success(params) + end + + def validate(params) + ResourceRegistry::Validation::NamespaceContract.new.call(params) + end + + def create(values) + namespace = ResourceRegistry::Namespace.new(values.to_h) + + Success(namespace) + end + + def find_feature(feature_key, registry) + feature = ResourceRegistry::Stores.find(feature_key) if defined? Rails + feature&.success&.attributes || registry[feature_key].feature + end + end + end + end +end diff --git a/lib/resource_registry/operations/namespaces/update_features.rb b/lib/resource_registry/operations/namespaces/update_features.rb new file mode 100644 index 00000000..057cce73 --- /dev/null +++ b/lib/resource_registry/operations/namespaces/update_features.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module ResourceRegistry + module Operations + module Namespaces + class UpdateFeatures + send(:include, Dry::Monads[:result, :do]) + + def call(params) + feature_params = yield extract(params[:namespace]) + registry = yield update(feature_params, params[:registry]) + + Success(registry) + end + + private + + def extract(params) + features = params[:features].values + + Success(features) + end + + def update(feature_params, registry) + feature_params.each do |params| + feature = registry[params[:key]] + update_model_attributes(params, registry) if feature.meta&.content_type == :model_attributes + ResourceRegistry::Operations::Features::Update.new.call(feature: feature_params.first, registry: registry) + end + + Success(registry) + end + + def update_model_attributes(params, registry) + feature = registry[params[:key]] + item = feature.item + + model_klass = item['class_name'].constantize + + record = if item['scope'].present? + model_klass.send(item['scope']['name'], *item['scope']['arguments']) + elsif item['where'].present? + criteria = item['where']['arguments'] + model_klass.where(criteria) + end + + if record + record.assign_attributes(params[:settings].values.reduce({}, :merge)) + record.save(validate: false) + end + rescue NameError => e + Failure("Exception #{e} occured while updating feature #{params[:key]} ") + end + end + end + end +end diff --git a/lib/resource_registry/operations/registries/load.rb b/lib/resource_registry/operations/registries/load.rb index af2ee75f..f3b65c40 100644 --- a/lib/resource_registry/operations/registries/load.rb +++ b/lib/resource_registry/operations/registries/load.rb @@ -10,7 +10,7 @@ class Load def call(registry:) paths = yield list_paths(load_path_for(registry)) - values = yield load(paths, registry) + values = yield load_features(paths, registry) entities = yield merge_namespaces(values[:namespace_list]) registry = yield register_graph(entities, values[:registry]) @@ -24,7 +24,7 @@ def list_paths(load_path) Success(paths) end - def load(paths, registry) + def load_features(paths, registry) namespaces_list = [] paths.value!.each do |path| diff --git a/lib/resource_registry/stores.rb b/lib/resource_registry/stores.rb index e7601eed..b0ee9d83 100644 --- a/lib/resource_registry/stores.rb +++ b/lib/resource_registry/stores.rb @@ -3,19 +3,43 @@ require_relative 'stores/file/read' require_relative 'stores/file/list_path' require_relative 'stores/container/update' +require_relative 'stores/mongoid/find' require_relative 'stores/mongoid/persist' +require_relative 'stores/active_record/find' require_relative 'stores/active_record/update' module ResourceRegistry module Stores + class << self - def self.persist(entity) - if defined?(ResourceRegistry::Mongoid) - ResourceRegistry::Stores::Mongoid::Persist.new.call(entity) - elsif defined? ResourceRegistry::ActiveRecord - ResourceRegistry::Stores::ActiveRecord::Persist.new.call(entity) + def persist(entity) + return unless store_namespace + + "#{store_namespace}::Persist".constantize.new.call(entity) + end + + def find(feature_key) + return unless store_namespace + + "#{store_namespace}::Find".constantize.new.call(feature_key) + end + + def store_namespace + "ResourceRegistry::Stores::#{orm}".constantize if orm + end + + def feature_model + "ResourceRegistry::#{orm}::Feature".constantize if orm + end + + def orm + if defined? ResourceRegistry::Mongoid + 'Mongoid' + elsif defined? ResourceRegistry::ActiveRecord + 'ActiveRecord' + end end end end -end +end \ No newline at end of file diff --git a/lib/resource_registry/stores/active_record/find.rb b/lib/resource_registry/stores/active_record/find.rb new file mode 100644 index 00000000..a2b18593 --- /dev/null +++ b/lib/resource_registry/stores/active_record/find.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module ResourceRegistry + module Stores + module ActiveRecord + # Instantiate a new Dry::Container object + class Find + send(:include, Dry::Monads[:result, :do]) + + # @param [String] key unique feature key + # @return [Dry::Container] A non-finalized Dry::Container with associated configuration values wrapped in Dry::Monads::Result + def call(key) + feature = yield find(key) + + Success(feature) + end + + private + + def find(key) + feature = ResourceRegistry::ActiveRecord::Feature.where(key: key).first + + Success(feature) + end + end + end + end +end + diff --git a/lib/resource_registry/stores/active_record/persist.rb b/lib/resource_registry/stores/active_record/persist.rb index cadc51b3..5ae1f340 100644 --- a/lib/resource_registry/stores/active_record/persist.rb +++ b/lib/resource_registry/stores/active_record/persist.rb @@ -9,15 +9,15 @@ class Persist # @param [Dry::Struct] feature_entity feature entity object # @return [ResourceRegistry::Mongoid::Feature] persisted feature record wrapped in Dry::Monads::Result - def call(feature_entity, registry) - feature = yield persist(feature_entity, registry) + def call(feature_entity) + feature = yield persist(feature_entity) Success(feature) end private - def persist(feature_entity, registry) + def persist(feature_entity) # if registry.db_connection&.table_exists?(:resource_registry_features) feature = ResourceRegistry::ActiveRecord::Feature.where(key: feature.key).first feature = ResourceRegistry::ActiveRecord::Feature.new(feature.to_h).save unless feature diff --git a/lib/resource_registry/stores/mongoid/find.rb b/lib/resource_registry/stores/mongoid/find.rb new file mode 100644 index 00000000..58dbf9b9 --- /dev/null +++ b/lib/resource_registry/stores/mongoid/find.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module ResourceRegistry + module Stores + module Mongoid + # Instantiate a new Dry::Container object + class Find + send(:include, Dry::Monads[:result, :do]) + + # @param [String] key unique feature key + # @return [Dry::Container] A non-finalized Dry::Container with associated configuration values wrapped in Dry::Monads::Result + def call(key) + feature = yield find(key) + + Success(feature) + end + + private + + def find(key) + feature = ResourceRegistry::Mongoid::Feature.where(key: key).first + + Success(feature) + end + end + end + end +end + diff --git a/lib/resource_registry/stores/mongoid/persist.rb b/lib/resource_registry/stores/mongoid/persist.rb index 9bfce850..19d7119c 100644 --- a/lib/resource_registry/stores/mongoid/persist.rb +++ b/lib/resource_registry/stores/mongoid/persist.rb @@ -50,6 +50,7 @@ def update(entity, feature) end end feature.save + feature }.to_result end end diff --git a/lib/resource_registry/validation/namespace_contract.rb b/lib/resource_registry/validation/namespace_contract.rb index 54f5bf76..cd23e643 100644 --- a/lib/resource_registry/validation/namespace_contract.rb +++ b/lib/resource_registry/validation/namespace_contract.rb @@ -20,14 +20,24 @@ class NamespaceContract < ResourceRegistry::Validation::ApplicationContract required(:path).array(:symbol) optional(:meta).maybe(:hash) optional(:feature_keys).array(:symbol) - optional(:features).array(:hash) + optional(:features).value(:array) optional(:namespaces).array(:hash) end - rule(:features).each do + rule(:features) do if key? && value - result = ResourceRegistry::Validation::FeatureContract.new.call(value) - key.failure(text: "invalid feature", error: result.errors.to_h) if result && result.failure? + return if value.any?{|feature| feature.is_a?(ResourceRegistry::Feature)} + feature_results = value.inject([]) do |results, feature_hash| + result = ResourceRegistry::Validation::FeatureContract.new.call(feature_hash) + + if result.failure? + key.failure(text: "invalid feature", error: result.errors.to_h) if result && result.failure? + else + results << result.to_h + end + end + + values.merge!(features: feature_results) end end diff --git a/spec/rails_app/system/config/templates/features/aca_shop_market/model_features.yml b/spec/rails_app/system/config/templates/features/aca_shop_market/model_features.yml new file mode 100644 index 00000000..6bd8178c --- /dev/null +++ b/spec/rails_app/system/config/templates/features/aca_shop_market/model_features.yml @@ -0,0 +1,66 @@ +--- +registry: + - namespace: + path: + - :shop + - :2021 + meta: + label: 'Shop 2021' + content_type: :feature_list + description: 'Configuration settings for 2019 Contribution models' + is_required: true + is_visible: true + features: + - key: :health + is_enabled: true + meta: + label: 'Health' + content_type: :feature_enabled + description: Health Products Offered + is_required: true + is_visible: true + - key: :dental + is_enabled: true + meta: + label: 'Dental' + content_type: :feature_enabled + description: Dental Products Offered + is_required: true + is_visible: true + - key: :benefit_market_catalog + is_enabled: true + item: + class_name: "BenefitMarkets::BenefitMarketCatalog" + scope: + name: :by_kind_and_application_date + arguments: + - :aca_shop + - <%= Date.new(2020,1,1) %> + # where: + # arguments: + # "application_period.min": + # "$lte": <%#= Date.new(2020,1,1) %> + meta: + label: 'Health' + content_type: :model_attributes + settings: + - key: :title + item: + attribute: :title + constraints: {max_length: 20} + meta: + label: Catalog Title + content_type: :text_field + description: Enter market catalog title + is_required: false + is_visible: true + - key: :description + item: + attribute: :description + constraints: {max_length: 200} + meta: + label: Catalog Description + content_type: :text_field + description: Enter market catalog description + is_required: false + is_visible: true \ No newline at end of file diff --git a/spec/resource_registry/operations/namespaces/form_spec.rb b/spec/resource_registry/operations/namespaces/form_spec.rb new file mode 100644 index 00000000..2f1bf60f --- /dev/null +++ b/spec/resource_registry/operations/namespaces/form_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ResourceRegistry::Operations::Namespaces::Form do + include RegistryDataSeed + + subject { described_class.new.call(namespace: [:shop, :"2021"], registry: registry) } + + context 'When a valid namespace passed' do + let(:registry) do + registry = ResourceRegistry::Registry.new + registry.register('configuration.load_path', features_folder_path) + registry + end + + before do + ResourceRegistry::Operations::Registries::Load.new.call(registry: registry) + end + + it "should return namespace entity" do + subject + + expect(subject).to be_a Dry::Monads::Result::Success + expect(subject.value!).to be_a ResourceRegistry::Namespace + end + end +end diff --git a/spec/resource_registry/operations/namespaces/update_features_spec.rb b/spec/resource_registry/operations/namespaces/update_features_spec.rb new file mode 100644 index 00000000..ffcc25c8 --- /dev/null +++ b/spec/resource_registry/operations/namespaces/update_features_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ResourceRegistry::Operations::Namespaces::UpdateFeatures do + include RegistryDataSeed + + subject { described_class.new.call(namespace: params, registry: registry) } + + context 'When valid feature hash passed' do + + let(:registry) do + registry = ResourceRegistry::Registry.new + registry.register('configuration.load_path', features_folder_path) + registry + end + + before do + ResourceRegistry::Operations::Registries::Load.new.call(registry: registry) + end + + let(:params) do + { + features: { + :"0"=>{key: 'health', is_enabled: 'true', namespace: 'shop.2021'}, + :"1"=>{key: 'dental', is_enabled: 'true', namespace: 'shop.2021'}, + :"2"=>{ + key: 'benefit_market_catalog', + namespace: 'shop.2021', + is_enabled: 'true', + settings: { + :"0"=>{title: "DC Health Link SHOP Benefit Catalog"}, + :"1"=>{description: "Test Description"} + } + } + } + } + end + + it "should return success with hash output" do + subject + end + end +end diff --git a/spec/resource_registry/validation/namespace_contract_spec.rb b/spec/resource_registry/validation/namespace_contract_spec.rb new file mode 100644 index 00000000..e69de29b From aa1d57423f270aa68cab8e9b1c30968a202dfb78 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Mon, 18 Jan 2021 19:26:09 -0500 Subject: [PATCH 14/40] Added spec fix --- .../validation/namespace_contract.rb | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/resource_registry/validation/namespace_contract.rb b/lib/resource_registry/validation/namespace_contract.rb index cd23e643..987ca25a 100644 --- a/lib/resource_registry/validation/namespace_contract.rb +++ b/lib/resource_registry/validation/namespace_contract.rb @@ -26,18 +26,19 @@ class NamespaceContract < ResourceRegistry::Validation::ApplicationContract rule(:features) do if key? && value - return if value.any?{|feature| feature.is_a?(ResourceRegistry::Feature)} - feature_results = value.inject([]) do |results, feature_hash| - result = ResourceRegistry::Validation::FeatureContract.new.call(feature_hash) + if value.none?{|feature| feature.is_a?(ResourceRegistry::Feature)} + feature_results = value.inject([]) do |results, feature_hash| + result = ResourceRegistry::Validation::FeatureContract.new.call(feature_hash) - if result.failure? - key.failure(text: "invalid feature", error: result.errors.to_h) if result && result.failure? - else - results << result.to_h + if result.failure? + key.failure(text: "invalid feature", error: result.errors.to_h) if result && result.failure? + else + results << result.to_h + end end - end - values.merge!(features: feature_results) + values.merge!(features: feature_results) + end end end From 773a4daf3aee1a206ff7208a53153fdf63981532 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Mon, 18 Jan 2021 23:47:53 -0500 Subject: [PATCH 15/40] Added namespace serializer and refactored registry create,load operations --- .../operations/registries/create.rb | 16 ++---- .../operations/registries/load.rb | 51 ++++++++----------- lib/resource_registry/serializers.rb | 1 + .../serializers/namespaces/serialize.rb | 45 ++++++++++++++++ .../operations/registries/create_spec.rb | 2 +- 5 files changed, 70 insertions(+), 45 deletions(-) create mode 100644 lib/resource_registry/serializers/namespaces/serialize.rb diff --git a/lib/resource_registry/operations/registries/create.rb b/lib/resource_registry/operations/registries/create.rb index 5f7f9942..dcbe928c 100644 --- a/lib/resource_registry/operations/registries/create.rb +++ b/lib/resource_registry/operations/registries/create.rb @@ -12,10 +12,9 @@ def call(path:, registry:) params = yield deserialize(file_io) feature_hashes = yield serialize(params) features = yield create(feature_hashes) - registry = yield save_and_register_feature(features, registry) - namespaces = yield build_namespaces(features) + registry = yield persist(features, registry) - Success(namespace_list: namespaces, registry: registry) + Success(features) end private @@ -60,7 +59,7 @@ def create(feature_hashes) }.to_result end - def save_and_register_feature(features, registry) + def persist(features, registry) features.each do |feature| ResourceRegistry::Stores.persist(feature) if defined? Rails registry.register_feature(feature) @@ -68,15 +67,6 @@ def save_and_register_feature(features, registry) Success(registry) end - - def build_namespaces(features) - Try { - features.collect do |feature| - namespace = ResourceRegistry::Operations::Namespaces::Build.new.call(feature) - namespace.success if namespace.success? - end.compact - }.to_result - end end end end diff --git a/lib/resource_registry/operations/registries/load.rb b/lib/resource_registry/operations/registries/load.rb index f3b65c40..461f5ae8 100644 --- a/lib/resource_registry/operations/registries/load.rb +++ b/lib/resource_registry/operations/registries/load.rb @@ -6,50 +6,43 @@ module Operations module Registries # Create a Feature class Load - send(:include, Dry::Monads[:result, :do]) + send(:include, Dry::Monads[:result, :do, :try]) def call(registry:) - paths = yield list_paths(load_path_for(registry)) - values = yield load_features(paths, registry) - entities = yield merge_namespaces(values[:namespace_list]) - registry = yield register_graph(entities, values[:registry]) + paths = yield list_paths(registry) + features = yield load_features(paths, registry) + namespaces = yield serialize_namespaces(features) + registry = yield register_graph(namespaces, registry) Success(registry) end private - def list_paths(load_path) + def list_paths(registry) + load_path = registry.resolve('configuration.load_path') paths = ResourceRegistry::Stores::File::ListPath.new.call(load_path) + Success(paths) end def load_features(paths, registry) - namespaces_list = [] - - paths.value!.each do |path| - result = ResourceRegistry::Operations::Registries::Create.new.call(path: path, registry: registry) - namespaces_list << result.success[:namespace_list] if result.success? - end - - Success(registry: registry, namespace_list: namespaces_list.flatten) + Try { + paths = paths.value! + paths.reduce([]) do |features, path| + result = ResourceRegistry::Operations::Registries::Create.new.call(path: path, registry: registry) + features << result.success if result.success? + features + end.flatten + }.to_result end - def merge_namespaces(namespace_list) - namespaces = namespace_list.reduce({}) do |namespaces, ns| - if namespaces[ns[:key]] - namespaces[ns[:key]][:feature_keys] += ns[:feature_keys] - else - namespaces[ns[:key]] = ns - end - namespaces - end - - Success(namespaces.values) + def serialize_namespaces(features) + ResourceRegistry::Serializers::Namespaces::Serialize.new.call(features: features) end - def register_graph(entities, registry) - graph = ResourceRegistry::Operations::Graphs::Create.new.call(entities, registry) + def register_graph(namespaces, registry) + graph = ResourceRegistry::Operations::Graphs::Create.new.call(namespaces, registry) if graph.success? registry.register_graph(graph.value!) @@ -59,10 +52,6 @@ def register_graph(entities, registry) Success(registry) end - - def load_path_for(registry) - registry.resolve('configuration.load_path') - end end end end diff --git a/lib/resource_registry/serializers.rb b/lib/resource_registry/serializers.rb index 46ef8ad3..1f34785a 100644 --- a/lib/resource_registry/serializers.rb +++ b/lib/resource_registry/serializers.rb @@ -3,6 +3,7 @@ require_relative 'serializers/yaml/deserialize' require_relative 'serializers/yaml/serialize' require_relative 'serializers/features/serialize' +require_relative 'serializers/namespaces/serialize' module ResourceRegistry module Serializers diff --git a/lib/resource_registry/serializers/namespaces/serialize.rb b/lib/resource_registry/serializers/namespaces/serialize.rb new file mode 100644 index 00000000..8f5ffd35 --- /dev/null +++ b/lib/resource_registry/serializers/namespaces/serialize.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module ResourceRegistry + module Serializers + module Namespaces + # Transform a Hash into YAML-formatted String + class Serialize + send(:include, Dry::Monads[:result, :do, :try]) + + # @param [Hash] params Hash to be transformed into YAML String + # @return [String] parsed values wrapped in Dry::Monads::Result object + def call(features:) + namespaces = yield build(features) + namespace_dict = yield merge(namespaces) + + Success(namespace_dict.values) + end + + private + + def build(features) + Try { + features.collect do |feature| + namespace = ResourceRegistry::Operations::Namespaces::Build.new.call(feature) + namespace.success if namespace.success? + end.compact + }.to_result + end + + def merge(namespaces) + Try { + namespaces.reduce({}) do |namespaces, ns| + if namespaces[ns[:key]] + namespaces[ns[:key]][:feature_keys] += ns[:feature_keys] + else + namespaces[ns[:key]] = ns + end + namespaces + end + }.to_result + end + end + end + end +end \ No newline at end of file diff --git a/spec/resource_registry/operations/registries/create_spec.rb b/spec/resource_registry/operations/registries/create_spec.rb index 744bc589..e40280b4 100644 --- a/spec/resource_registry/operations/registries/create_spec.rb +++ b/spec/resource_registry/operations/registries/create_spec.rb @@ -14,7 +14,7 @@ it "should return success with hash output" do subject expect(subject).to be_a Dry::Monads::Result::Success - expect(subject.value![:registry]).to be_a ResourceRegistry::Registry + expect(subject.value!).to include(a_kind_of ResourceRegistry::Feature) end end From 7d61bfb47444d9ebcdf8efc1a288fdc78d490225 Mon Sep 17 00:00:00 2001 From: Dan Thomas Date: Tue, 19 Jan 2021 08:35:49 -0500 Subject: [PATCH 16/40] Add template YAML for Enterprise. Update README and rubucop settings --- .rubocop.yml | 6 - README.md | 131 +++++---- .../config/templates/features/enterprise.yml | 273 ++++++++++++++++++ 3 files changed, 341 insertions(+), 69 deletions(-) create mode 100644 lib/system/config/templates/features/enterprise.yml diff --git a/.rubocop.yml b/.rubocop.yml index e095b678..f96b01db 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -22,9 +22,6 @@ Layout/ExtraSpacing: Layout/EmptyLinesAroundClassBody: Enabled: false -Layout/FirstArrayElementIndentation: - Enabled: false - # Re-enable once other problems are solved Layout/SpaceAfterComma: Enabled: false @@ -38,9 +35,6 @@ Layout/SpaceInsideHashLiteralBraces: Layout/SpaceInsideBlockBraces: Enabled: false -Layout/TrailingEmptyLines: - Enabled: false - Layout/IndentationWidth: Enabled: true diff --git a/README.md b/README.md index b38c06f5..87e253ce 100644 --- a/README.md +++ b/README.md @@ -3,68 +3,70 @@ # @title README.md # @author Dan Thomas --> - # ResourceRegistry - [![Build Status](https://travis-ci.com/ideacrew/resource_registry.svg?branch=master)](https://travis-ci.com/ideacrew/resource_registry) - ResourceRegistry is a library for system configuration, feature flipping and eventing. It offers an approach to custom configuration for a single codebase, supporting use cases such as: +# ResourceRegistry - * Customer-level preference profiles - * Multitenancy - * Access control based on privilidges and subscriptions +[![Build Status](https://travis-ci.com/ideacrew/resource_registry.svg?branch=master)](https://travis-ci.com/ideacrew/resource_registry) - ResourceRegistry is intended to address 'logic sprawl' that can occur with minimally- or un-structured key/value system settings schemes. It offers an - alternative to code obfuscation issues that often pops up when using Rails Concerns. +ResourceRegistry is a library for system configuration, feature flipping and eventing. It offers an approach to custom configuration for a single codebase, supporting use cases such as: + +- Customer-level preference profiles +- Multitenancy +- Access control based on privilidges and subscriptions + +ResourceRegistry is intended to address 'logic sprawl' that can occur with minimally- or un-structured key/value system settings schemes. It offers an +alternative to code obfuscation issues that often pops up when using Rails Concerns. ## Gem Features - * Group associated system code and configuration settings as a Feature - * Define a namespace taxonomy to associate and nest Features and dependencies - * Enable/disable individual Features - * Store metadata values for a Feature that support auto generation of a configuration UI - * Access Features and their attribute values using a thread-safe key/value store - * Use YAML files to seed Features and namespaces +- Group associated system code and configuration settings as a Feature +- Define a namespace taxonomy to associate and nest Features and dependencies +- Enable/disable individual Features +- Store metadata values for a Feature that support auto generation of a configuration UI +- Access Features and their attribute values using a thread-safe key/value store +- Use YAML files to seed Features and namespaces ## Compatibility - * Ruby 2.6 - * Rails 5.2.4 +- Ruby 2.6 +- Rails 5.2.4 ### Installing on Rails - Add this line to your project's Gemfile: +Add this line to your project's Gemfile: gem 'resource_registry' - And then execute: +And then execute: $ bundle - Or install it yourself as: +Or install it yourself as: $ gem install resource_registry - In your project build the directory tree to house configuration files: +In your project build the directory tree to house configuration files: $ mkdir -p ./system/boot && mkdir -p ./system/config - Then, create Resource Registry's initializer file: +Then, create Resource Registry's initializer file: $ touch ./config/initializers/resource_registry.rb ## Feature - ResourceRegistry uses a Feature to group related system functions and settings. Featurse are composed of the following high level attributes: +ResourceRegistry uses a Feature to group related system functions and settings. Featurse are composed of the following high level attributes: - * key [Symbol] 'key' of the Feature's key/value pair. This is the Feature's identifier and must be unique - * item [Any] 'value' of the Feature's key/value pair. May be a static value, proc, class instance and may include an options hash - * namespace [Array] an ordered list that supports optional taxonomy for relationships between Features - * is_enabled [Boolean] indicator whether the Feature is accessible in the current configuration - * settings [Array] a list of key/item pairs associated with the Feature - * meta [Hash] a set of attributes to store configuration values and drive their presentation in User Interface +- key [Symbol] 'key' of the Feature's key/value pair. This is the Feature's identifier and must be unique +- item [Any] 'value' of the Feature's key/value pair. May be a static value, proc, class instance and may include an options hash +- namespace [Array] an ordered list that supports optional taxonomy for relationships between Features +- is_enabled [Boolean] indicator whether the Feature is accessible in the current configuration +- settings [Array] a list of key/item pairs associated with the Feature +- meta [Hash] a set of attributes to store configuration values and drive their presentation in User Interface - Here is an example Feature definition in YAML format. Note the settings ```effective_period``` value is an expression: +Here is an example Feature definition in YAML format. Note the settings `effective_period` value is an expression: -``` ruby +```ruby - namespace: - :enroll_app - :aca_shop_market @@ -90,11 +92,11 @@ item: :initial_sponsor_jan_default ``` - ### Registering Features +### Registering Features - Features are most useful when they're loaded into a registry for runtime access. For example: +Features are most useful when they're loaded into a registry for runtime access. For example: -``` ruby +```ruby require 'resource_registry' # Initialize registry @@ -113,6 +115,7 @@ my_registry.resolve_feature('stringify') {:my_symbol} # => "my_symbol" ``` ### Detailed Example + ```ruby my_registry = ResourceRegistry::Registry.new @@ -126,17 +129,17 @@ end # Specify the code to invoke when the registry resolves the Feature key greeter_instance = Greeter.new -# Assign the Feature to a Taxonomy namespace +# Assign the Feature to a Taxonomy namespace ns = [:operations, :ai] # Associate a Setting key/value pair with the Feature scope_setting = {key: :scope, item: "online"} - + # Define a Feature with a namespace and settings -greeter = ResourceRegistry::Feature.new(key: :greeter, - item: greeter_instance, - namespace: ns, +greeter = ResourceRegistry::Feature.new(key: :greeter, + item: greeter_instance, + namespace: ns, settings: [scope_setting]) # Add Feature to the Registry @@ -150,71 +153,73 @@ my_registry[:greeter] {"Dolly"} # => "Hello Dolly" ### Namepace - Use the optional Feature#namespace attribute to organize Features. Namespaces support enable you to define a structure to group Features into a logical structure or taxonomy that can help with code clarity. For example: +Use the optional Feature#namespace attribute to organize Features. Namespaces support enable you to define a structure to group Features into a logical structure or taxonomy that can help with code clarity. For example: -``` ruby - my_species = ResourceRegistry::Feature.new( key: :species, +```ruby + my_species = ResourceRegistry::Feature.new( key: :species, item: :Operations::Species::Create.new, is_enabled: true, namespace: [:kingdom, :phylum, :class, :order, :family, :genus]) my_registry.register_feature(my_species) ``` -Namespaced Features respect their anscesters with regard to code access. For instance ```Feature#enabled?``` will check not only the referenced Feature, but traverse all ancestors in its namespace. If any of the referenced Feature's anscestors is disabled, then the referenced Feature is considered disabled -- regardless of whether ```is_enabled``` is set to ```true``` or ```false```. +Namespaced Features respect their anscesters with regard to code access. For instance `Feature#enabled?` will check not only the referenced Feature, but traverse all ancestors in its namespace. If any of the referenced Feature's anscestors is disabled, then the referenced Feature is considered disabled -- regardless of whether `is_enabled` is set to `true` or `false`. -For instance, extending the ```species``` Feature example above: +For instance, extending the `species` Feature example above: -``` ruby - my_phylum = ResourceRegistry::Feature.new(key: :phylum, +```ruby + my_phylum = ResourceRegistry::Feature.new(key: :phylum, item: :Operations::Phylum::Create.new, is_enabled: false, namespace: [:kingdom]) my_registry.register_feature(my_phylum) ``` -Here the ```my_registry[:my_phylum].is_enabled? == false```. As it's a namespace ancestor to the ```my_registry[:species]```, ```my_registry[:species].is_enabled? == false``` also. +Here the `my_registry[:my_phylum].is_enabled? == false`. As it's a namespace ancestor to the `my_registry[:species]`, `my_registry[:species].is_enabled? == false` also. -Namespaces serve another purpose: enabling auto-generation of Admin UI configuration settings. This is a future function that uses Namespace in combination with Meta attributes to build the UI forms. +Namespaces serve another purpose: enabling auto-generation of Admin UI configuration settings. This is a future function that uses Namespace in combination with Meta attributes to build the UI forms. ## Rails Integration - A registry is configured and loaded when your application starts. +A registry is configured and loaded when your application starts. ## Configuration - The initializer and configuration files manage the setup and loading process. +The initializer and configuration files manage the setup and loading process. + +Configuration files are located in your project's `system/config` directory. All Yaml files in and below this directory are autoloaded during the boot process. Configuration settings may be organized into directories and files in any manner. Values will properly load into the container hierarchy provided the file begins with a reference to an identifiable parent key. - Configuration files are located in your project's ```system/config``` directory. All Yaml files in and below this directory are autoloaded during the boot process. Configuration settings may be organized into directories and files in any manner. Values will properly load into the container hierarchy provided the file begins with a reference to an identifiable parent key. +An example of a simple configuration file: - An example of a simple configuration file: - ```ruby +```ruby # ./system/config/enterprise.yml - ``` +``` - ## Defining Configuration Settings +## Defining Configuration Settings - ### UI-ready configuration settings +### UI-ready configuration settings ## Credits - Based on [dry-system](https://dry-rb.org/gems/dry-system/) and [dry-validation](https://dry-rb.org/gems/dry-validation/1.0/) ``` + +Based on [dry-system](https://dry-rb.org/gems/dry-system/) and [dry-validation](https://dry-rb.org/gems/dry-validation/1.0/) ``` ## Future Features - * Taxonomy: support namespace structures and validations - * Subscription - * Bootable infrastructure components +- Taxonomy: support namespace structures and validations +- Subscription +- Bootable infrastructure components ## Development - After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. +After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. - To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). +To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). ## Contributing - Bug reports and pull requests are welcome on GitHub at https://github.com/ideacrew/resource_registry. +Bug reports and pull requests are welcome on GitHub at https://github.com/ideacrew/resource_registry. ## License - The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). +The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). diff --git a/lib/system/config/templates/features/enterprise.yml b/lib/system/config/templates/features/enterprise.yml new file mode 100644 index 00000000..01ccec64 --- /dev/null +++ b/lib/system/config/templates/features/enterprise.yml @@ -0,0 +1,273 @@ +--- +registry: + - namespace: + - :enterprise + meta: + label: Enterprise + description: Organizations that share hosting services under this Enterprise and their top level configurations + features: + - key: :enterprise_owner_accounts + item: [] + is_enabled: true + meta: + label: Enterprise Owner Accounts + description: A list of accounts with enterprise admininstrator priviliges + content_type: :array_of_accounts + is_required: true + is_visible: true + - key: :enterprise_profile + is_enabled: true + settings: + - key: :enterprise_organization_name + item: nil + meta: + label: Enterprise Organization Name + description: Name of organization hosting this system instance + content_type: :string + is_required: false + is_visible: true + settings: + - key: :technical_contact + item: + name: nil + email: nil + phone: nil + meta: + label: Technical Contact + content_type: :contact + description: Contact information for person who can assist with system technical issues + is_required: false + is_visible: true + - key: :operational_contact + item: + name: nil + email: nil + phone: nil + meta: + label: Operational Contact + content_type: :contact + description: Contact information for person who can assist with system operational issues + is_required: false + is_visible: true + - key: :is_multitenant + item: false + meta: + label: Multitenant? + description: Is this Enterprise instance hosting services shared between multiple customers? + content_type: :boolean + default: false + is_required: true + is_visible: true + - namespace: + - :enterprise + - :tenants + meta: + label: Tenants + description: Organizations that share hosting services under this Enterprise and their top level configurations + features: + - key: :dchbx_tenant + item: :dchbx + is_enabled: true + meta: + label: Tenant Key + description: A short, unique identifier for this tenant. Lower case letters only. May not start with a number. + content_type: :symbol + is_required: true + is_visible: true + settings: + - key: :start_at + item: nil + meta: + label: Start At + Description: Date when this organization's account first activated + content_type: :date_time + is_required: false + is_visible: true + - key: :expire_at + item: nil + - namespace: + - :enterprise + - :subscriptions + - :dchbx + meta: + label: Tenant Subscriptions + description: Software products authorized for each Tenant + features: + - key: :enroll_app + item: nil + is_enabled: true + settings: + - key: :validator_id + item: nil + - key: :subscribed_at + item: nil + - key: :unsubscribed_at + item: nil + - namespace: + - :dchbx + meta: + label: DC HealthLink Tenant + description: Tenants manage features under their own namespace key + features: + - key: :dchbx_owner_accounts + item: [] + is_enabled: true + meta: + label: Owner Accounts + description: A list of accounts with admininstrator priviliges + content_type: :array_of_accounts + is_required: true + is_visible: true + - key: :dchbx_profile + is_enabled: true + settings: + - key: :dchbx_organization_name + item: "DC HealthLink" + meta: + label: Organization Name + description: Name of organization hosting this system instance + content_type: :string + is_required: false + is_visible: true + settings: + - key: :technical_contact + item: + name: nil + email: nil + phone: nil + meta: + label: Technical Contact + content_type: :contact + description: Contact information for person who can assist with system technical issues + is_required: false + is_visible: true + - key: :operational_contact + item: + name: nil + email: nil + phone: nil + meta: + label: Operational Contact + content_type: :contact + description: Contact information for person who can assist with system operational issues + is_required: false + is_visible: true + - key: :dchbx_profile + is_enabled: true + settings: + - key: :dchbx_organization_name + item: "DC HealthLink" + meta: + label: Enterprise Organization Name + description: Name of organization hosting this site + content_type: :string + is_required: false + is_visible: true + - namespace: + - :dchbx + - :enroll_app + features: + - key: :aca_shop_market + is_enabled: true + - key: :aca_individual_market + is_enabled: true + - key: :congressional_market + is_enabled: true + - namespace: + - :dchbx + - :enroll_app + - :site + - :brand + features: + - key: logo + is_enabled: true + meta: + type: :base_64 + description: Organization logo image that will appear on the portal page. For best results, use .png or .jpg 160h x 160w pixel image. + - key: fav_icon + is_enabled: true + meta: + type: :base_64 + description: Favorite icon is a 16x16 pixel image that will appear on the web browser tab, bookmarks, history, etc.. For best results, use .png or .jpg image 160h x 160w + - key: heading_typeface + item: https://fonts.googleapis.com/css?family=Lato:400,700,400italic" + is_enabled: true + meta: + type: :url + default: https://fonts.googleapis.com/css?family=Lato:400,700,400italic" + description: Google offers an extensive catalog of open source fonts that may be used with the Tool. See https://fonts.google.com/ + - key: body_typeface + item: https://fonts.googleapis.com/css?family=Lato:400,700,400italic" + is_enabled: true + meta: + type: :url + default: https://fonts.googleapis.com/css?family=Lato:400,700,400italic" + description: Google offers an extensive catalog of open source fonts that may be used with the Tool. See https://fonts.google.com/ + - key: color_pallette + is_enabled: true + settings: + - key: :primary_color + meta: + type: :swatch + default: "#007bff" + - key: :secondary_color + meta: + type: :swatch + default: "#6c757d" + - key: :success_color + meta: + type: :swatch + default: "#28a745" + - key: :danger_color + meta: + type: :swatch + default: "#dc3545" + - key: :warning_color + meta: + type: :swatch + default: "#ffc107" + - key: :info_color + meta: + type: :swatch + default: "#17a2b8" + - namespace: + - :dchbx + - :enroll_app + - :site + - :urls + features: + - key: :policies_url + item: "https://dchealthlink.com/" + is_enabled: true + meta: + content_type: :url + is_required: false + is_visible: true + - key: :faqs_url + item: "https://www.dchealthlink.com/Frequently-Asked-Questions" + is_enabled: true + meta: + content_type: :url + is_required: false + is_visible: true + - key: :help_url + item: "https://www.dchealthlink.com/help" + is_enabled: true + meta: + content_type: :url + is_required: false + is_visible: true + - key: :nondiscrimination_notice_url + item: "https://www.dchealthlink.com/nondiscrimination" + is_enabled: true + meta: + content_type: :url + is_required: false + is_visible: true + - key: :business_resource_center_url + item: "https://dchealthlink.com/smallbusiness/" + is_enabled: true + meta: + content_type: :url + is_required: false + is_visible: true From 6fa3cc4f9303ad90d44514e0834a9bd32276826f Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Tue, 19 Jan 2021 09:15:20 -0500 Subject: [PATCH 17/40] Updated view helpers --- lib/resource_registry/helpers/view_controls.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/resource_registry/helpers/view_controls.rb b/lib/resource_registry/helpers/view_controls.rb index 70f2b79a..42635e91 100644 --- a/lib/resource_registry/helpers/view_controls.rb +++ b/lib/resource_registry/helpers/view_controls.rb @@ -273,7 +273,7 @@ def value_for(setting, form, options = {}) end value = if form.object.class.to_s.match(/^ResourceRegistry.*/).present? - form.object.settings.where(key: setting.key).first&.item + form.object.settings.detect{|s| s.key == setting.key}&.item else form.object.send(setting.key) end @@ -513,7 +513,7 @@ def feature_panel(feature_key, feature_registry, _options = {}) render_feature(feature, form) + tag.div(class: 'row mt-3') do tag.div(class: 'col-4') do - form.submit(class: 'btn btn-primary') + form.submit('Update', class: 'btn btn-primary') end + tag.div(class: 'col-6') do tag.div(class: 'flash-message', id: feature_key.to_s + '-alert') @@ -542,7 +542,7 @@ def namespace_panel(namespace, feature_registry, _options = {}) namespace_content += tag.div(class: 'row mt-3') do tag.div(class: 'col-4') do - form.submit(class: 'btn btn-primary') + form.submit('Update', class: 'btn btn-primary') end + tag.div(class: 'col-6') do tag.div(class: 'flash-message', id: namespace.path.map(&:to_s).join('-') + '-alert') From e56dfed8cbe061d414a7952a54efee5cf9b7ab2b Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Thu, 21 Jan 2021 13:51:16 -0500 Subject: [PATCH 18/40] Added view controls and create input controls helper --- .../helpers/input_controls.rb | 376 ++++++++++++ .../helpers/view_controls.rb | 547 ++++-------------- .../models/mongoid/namespace_path.rb | 4 - lib/resource_registry/navigation.rb | 18 +- .../operations/namespaces/form.rb | 3 +- 5 files changed, 500 insertions(+), 448 deletions(-) create mode 100644 lib/resource_registry/helpers/input_controls.rb diff --git a/lib/resource_registry/helpers/input_controls.rb b/lib/resource_registry/helpers/input_controls.rb new file mode 100644 index 00000000..c4ff5aed --- /dev/null +++ b/lib/resource_registry/helpers/input_controls.rb @@ -0,0 +1,376 @@ +module InputControls + + def build_option_field(option, form, attrs = {}) + type = option.meta.content_type&.to_sym + input_control = case type + when :swatch + input_swatch_control(option, form) + when :base_64 + input_file_control(option, form) + when :radio_select + input_radio_control(option, form) + when :checkbox_select + input_checkbox_control(option, form) + when :select + select_control(option, form) + when :number + input_number_control(option, form) + when :email + input_email_control(option, form) + when :date + input_date_control(option, form) + when :currency + input_currency_control(option, form) + when :feature_enabled + feature_enabled_control(option, form) + when :slider_switch + slider_switch_control(option, form) + else + input_text_control(option, form, attrs) + end + # else :text_field + # input_text_control(option, form) + # else + # # :dan_check_box + # # find dan_check_box_control helper + # # else + # # custom_helper #catch_all for custom types + # end + + if [:radio_select, :checkbox_select].include?(type) + custom_form_group(option, input_control) + else + return input_control if [:feature_enabled, :slider_switch].include?(type) + form_group(option, input_control) + end + end + + def feature_enabled_control(option, form) + tag.div(class: "form-group") do + content = option.key.to_s.titleize + content += tag.label(class: 'switch') do + tag.input(type: 'hidden', value: option.key, name: 'feature_key') + + tag.input(type: "checkbox", checked: option.is_enabled) + + tag.span(class: "slider") + end + + content += tag.div(class: "spinner-border d-none text-success", role: "status") do + tag.span(class: "sr-only") do + "Loading..." + end + end + + content.html_safe + end + end + + def slider_switch_control(option, form) + tag.div(class: "form-group") do + content = option.key.to_s.titleize + content += tag.label(class: 'switch') do + tag.input(type: 'hidden', value: option.key, name: 'feature_key') + + tag.input(type: "checkbox", checked: option.item) + + tag.span(class: "slider") + end + + content.html_safe + end + end + + def select_control(setting, form) + id = setting[:key].to_s + selected_option = "Choose..." + meta = setting[:meta] + + # aria_describedby = id + + value = value_for(setting, form) || setting.item || meta&.default + option_list = tag.option(selected_option, selected: (value.blank? ? true : false)) + meta.enum.each do |choice| + option_list += tag.option(choice.first[1], selected: (choice.first[0].to_s == value.to_s), value: choice.first[0]) + end + + tag.select(option_list, id: id, class: "form-control", name: input_name_for(setting, form)) + end + + def select_dropdown(input_id, list, show_default = false, selected = nil) + name = (input_id.to_s.scan(/supported_languages/).present? ? input_id : 'admin[' + input_id.to_s + ']') + + return unless list.is_a? Array + content_tag(:select, class: "form-control", id: input_id, name: name, required: true) do + concat(content_tag(:option, "Select", value: "")) unless show_default + list.each do |item| + if item.is_a? Array + is_selected = false + is_selected = true if selected.present? && selected == item[1] + concat(content_tag(:option, item[0], value: item[1], selected: is_selected)) + elsif item.is_a? Hash + concat(content_tag(:option, item.first[1], value: item.first[0])) + elsif input_id == 'state' + concat(content_tag(:option, item.to_s.titleize, value: item)) + elsif show_default + concat(content_tag(:option, item, value: item)) + else + concat(content_tag(:option, item.to_s.humanize, value: item)) + end + end + end + end + + def input_import_control(setting, _form) + id = setting[:key].to_s + aria_describedby = id + label = setting[:title] || id.titleize + + tag.div(class: "input-group-prepend") do + tag.span('Upload', class: "input-group-text", id: id) + end + + tag.div(class: "custom-file") do + tag.input(nil, type: "file", id: id, name: id + "[value]", class: "custom-file-input", aria: { describedby: aria_describedby }) + + tag.label('Choose File', for: id, value: label, class: "custom-file-label") + end + end + + def input_radio_control(setting, form) + meta = setting.meta + input_value = value_for(setting, form) || setting.item || meta&.default + aria_label = "Radio button for following text input" #setting[:aria_label] || "Radio button for following text input" + + if setting.is_a?(ResourceRegistry::Setting) + element_name = input_name_for(setting, form) + else + element_name = form&.object_name.to_s + "[is_enabled]" + input_value = form.object&.is_enabled + input_value = setting.is_enabled if input_value.blank? + end + + meta.enum.collect do |choice| + choice = send(choice) if choice.is_a?(String) + input_group do + tag.div(tag.div(tag.input(nil, type: "radio", name: element_name, value: choice.first[0], checked: input_value.to_s == choice.first[0].to_s, required: true), class: "input-group-text"), class: "input-group-prepend") + + tag.input(nil, type: "text", placeholder: choice.first[1], class: "form-control", aria: {label: aria_label }) + end + end.join('').html_safe + end + + def input_checkbox_control(setting, form) + meta = setting.meta + input_value = value_for(setting, form) || setting.item || meta&.default + aria_label = 'Checkbox button for following text input' + meta.enum.collect do |choice| + choice = send(choice) if choice.is_a?(String) + input_group do + tag.div(tag.div(tag.input(nil, type: 'checkbox', name: "#{input_name_for(setting, form)}[]", value: choice.first[0], checked: input_value.include?(choice.first[0].to_s), required: false), class: 'input-group-text'), class: 'input-group-prepend') + + tag.input(nil, type: 'text', placeholder: choice.first[1], class: 'form-control', aria: {label: aria_label }) + end + end.join('').html_safe + end + + def input_file_control(setting, form) + meta = setting.meta + id = setting.key.to_s + aria_describedby = id + label = meta.label + input_value = setting.item || meta.default + + preview = if input_value.present? + tag.img(class: 'w-100', src: "data:#{meta.type};base64,#{input_value}") + else + tag.span('No logo') + end + + control_inputs = + tag.div(class: "input-group-prepend") do + tag.span('Upload', class: "input-group-text", id: id) + end + + tag.div(class: "custom-file") do + tag.input(nil, type: "file", id: id, name: form&.object_name.to_s + "[#{setting.key}]", class: "custom-file-input", aria: { describedby: aria_describedby }) + + tag.label('Choose File', for: id, value: label, class: "custom-file-label") + end + + control = + tag.div(class: "col-2") do + preview + end + + tag.div(class: 'input-group') do + control_inputs + end + + control + end + + def input_text_control(setting, form, options = {}) + id = setting[:key].to_s + + meta = setting[:meta] + input_value = value_for(setting, form, options) || setting.item || meta&.default + + # aria_describedby = id + + is_required = meta[:is_required] == false ? meta[:is_required] : true + placeholder = "Enter #{meta[:label]}".gsub('*','') if meta[:description].blank? + # if meta[:attribute] + # tag.input(nil, type: "text", value: input_value, id: id, name: form&.object_name.to_s + "[#{id}]",class: "form-control", required: true) + # else + + tag.input(nil, type: "text", value: input_value, id: id, name: input_name_for(setting, form), placeholder: placeholder, class: "form-control", required: is_required) + # end + end + + def input_date_control(setting, form) + id = setting[:key].to_s + + date_value = value_for(setting, form) + date_value = date_value.to_date if date_value.is_a?(Time) + date_value = date_value.to_s(:db) if date_value.is_a?(Date) + + meta = setting[:meta] + input_value = date_value || setting.item || meta&.default + # aria_describedby = id + + is_required = meta&.is_required == false ? meta.is_required : true + + tag.input(nil, type: "date", value: input_value, id: id, name: input_name_for(setting, form), placeholder: "mm/dd/yyyy", class: "form-control", required: is_required) + end + + def input_number_control(setting, form) + id = setting[:key].to_s + meta = setting[:meta] + input_value = value_for(setting, form) || meta.value || meta.default + # input_value = setting[:value] || setting[:default] + # aria_describedby = id + placeholder = "Enter #{meta[:label]}".gsub('*','') if meta[:description].blank? + + # if setting[:attribute] + tag.input(nil, type: "number", step: "any", value: input_value, id: id, name: input_name_for(setting, form), placeholder: placeholder, class: "form-control", required: true, oninput: "check(this)") + # else + # tag.input(nil, type: "number", step:"any", value: input_value, id: id, name: form&.object_name.to_s + "[value]",class: "form-control", required: true, oninput: "check(this)") + # end + end + + def input_email_control(setting, form) + id = setting[:key].to_s + meta = setting[:meta] + input_value = meta.value || meta.default + # input_value = setting[:value] || setting[:default] + # aria_describedby = id + + # if setting[:attribute] + tag.input(nil, type: "email", step: "any", value: input_value, id: id, name: input_name_for(setting, form),class: "form-control", required: true, oninput: "check(this)") + # else + # tag.input(nil, type: "email", step:"any", value: input_value, id: id, name: form&.object_name.to_s + "[value]",class: "form-control", required: true, oninput: "check(this)") + # end + end + + def input_color_control(setting) + id = setting[:key].to_s + input_value = setting[:value] || setting[:default] + + tag.input(nil, type: "color", value: input_value, id: id) + end + + def input_swatch_control(setting, form) + # id = setting[:key].to_s + # color = setting[:value] || setting[:default] + id = setting[:key].to_s + meta = setting[:meta] + color = meta.value || meta.default + + tag.input(nil, type: "text", value: color, id: id, name: form&.object_name.to_s + "[value]",class: "js-color-swatch form-control") + + tag.div(tag.button(type: "button", id: id, class: "btn", value: "", style: "background-color: #{color}"), class: "input-group-append") + end + + def input_currency_control(setting, form) + id = setting[:key].to_s + meta = setting[:meta] + input_value = meta.value || meta.default + + # id = setting[:key].to_s + # input_value = setting[:value] || setting[:default] + aria_map = { label: "Amount (to the nearest dollar)"} + + tag.div(tag.span('$', class: "input-group-text"), class: "input-group-prepend") + + tag.input(nil, type: "text", value: input_value, id: id, name: input_name_for(setting, form), class: "form-control", aria: { map: aria_map }) + + tag.div(tag.span('.00', class: "input-group-text"), class: "input-group-append") + end + + def value_for(setting, form, options = {}) + if options[:record].present? + item = JSON.parse(setting.item) + options[:record].send(item['attribute']) + else + value = if form.object.class.to_s.match(/^ResourceRegistry.*/).present? + form.object.settings.detect{|s| s.key == setting.key}&.item + else + form.object.send(setting.key) + end + + value = value.to_s if value.is_a?(FalseClass) + value + end + end + + def input_name_for(setting, form) + if form.object.class.to_s.match(/^ResourceRegistry.*/).present? + if form.index.present? + form&.object_name.to_s + "[#{form.index}][#{setting.key}]" + else + form&.object_name.to_s + "[settings][#{setting.key}]" + end + else + form&.object_name.to_s + "[#{setting.key}]" + end + end + + # Wrap any input group in
tag + def input_group + tag.div(yield, class: "input-group") + end + + # Build a general-purpose form group wrapper around the supplied input control + def form_group(setting, control) + id = setting[:key].to_s + # label = setting[:title] || id.titleize + label = setting.meta.label || id.titleize + help_id = id + 'HelpBlock' + # help_text = setting[:description] + # aria_label = setting[:aria_label] || "Radio button for following text input" + help_text = setting.meta.description + aria_label = "Radio button for following text input" #setting[:aria_label] || "Radio button for following text input" + + tag.div(class: "form-group") do + tag.label(for: id, value: label, aria: { label: aria_label }) do + label + end + + input_group { control } + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) + end + end + + def custom_form_group(setting, control) + id = setting[:key].to_s + # label = setting[:title] || id.titleize + label = setting.meta.label || id.titleize + help_id = id + 'HelpBlock' + help_text = setting.meta.description + aria_label = "#{setting.meta.content_type.to_s.humanize} button for following text input" #setting[:aria_label] || "Radio button for following text input" + + tag.div(class: "form-group") do + tag.label(for: id, value: label, aria: { label: aria_label }) do + label + end + + control + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) + end + end + + def build_attribute_field(form, attribute) + setting = { + key: attribute, + default: form.object.send(attribute), + type: :string, + attribute: true + } + + input_control = input_text_control(setting, form) + form_group(setting, input_control) + end +end \ No newline at end of file diff --git a/lib/resource_registry/helpers/view_controls.rb b/lib/resource_registry/helpers/view_controls.rb index 42635e91..abaa5e82 100644 --- a/lib/resource_registry/helpers/view_controls.rb +++ b/lib/resource_registry/helpers/view_controls.rb @@ -1,442 +1,70 @@ # frozen_string_literal: true +require_relative 'input_controls' + module RegistryViewControls + include ::InputControls + + def render_feature(feature, form, registry = nil) + return render_model_feature(feature, form, registry) if [:model_filter, :model_attributes].include?(feature.meta.content_type) - def render_feature(feature, form = nil) feature = feature.feature if feature.is_a?(ResourceRegistry::FeatureDSL) - return render_attributes_feature(feature, form) if feature.meta.content_type == :model_attributes content = form.hidden_field(:is_enabled) - content += if ['legend'].include?(feature.meta.content_type.to_s) - form.hidden_field(:namespace, value: feature.namespace_path.dotted_path) - else - build_option_field(feature, form) - end - content += feature.settings.collect{|setting| build_option_field(setting, form).html_safe if setting.meta}.compact.join.html_safe - content.html_safe - end - - def render_attributes_feature(feature, form) - item = JSON.parse(feature.item) - model_klass = item['class_name'].constantize - record = if item['scope'].present? - model_klass.send(item['scope']['name'], *item['scope']['arguments']) - elsif item['where'].present? - criteria = item['where']['arguments'] - model_klass.where(criteria) - end - - content = form.hidden_field(:is_enabled) + - form.hidden_field(:namespace, value: feature.namespace_path.path.map(&:to_s).join('.')) - - - feature.settings.each_with_index do |setting, index| - next unless setting.meta - content += form.fields_for :settings, setting, {index: index} do |setting_form| - build_option_field(setting, setting_form, {record: record}) - end + if ['legend'].include?(feature.meta.content_type.to_s) + content += form.hidden_field(:namespace, value: feature.namespace_path.path.map(&:to_s).join('.')) + elsif feature.meta.content_type == :feature_enabled + content += build_option_field(feature, form) end + content += feature.settings.collect{|setting| build_option_field(setting, form).html_safe if setting.meta}.compact.join.html_safe content.html_safe end - def build_option_field(option, form, attrs = {}) - type = option.meta.content_type&.to_sym - input_control = case type - when :swatch - input_swatch_control(option, form) - when :base_64 - input_file_control(option, form) - when :radio_select - input_radio_control(option, form) - when :checkbox_select - input_checkbox_control(option, form) - when :select - select_control(option, form) - when :number - input_number_control(option, form) - when :email - input_email_control(option, form) - when :date - input_date_control(option, form) - when :currency - input_currency_control(option, form) - when :feature_enabled - feature_enabled_control(option, form) - when :slider_switch - slider_switch_control(option, form) - else - input_text_control(option, form, attrs) - end - # else :text_field - # input_text_control(option, form) - # else - # # :dan_check_box - # # find dan_check_box_control helper - # # else - # # custom_helper #catch_all for custom types - # end - - if [:radio_select, :checkbox_select].include?(type) - custom_form_group(option, input_control) - else - return input_control if [:feature_enabled, :slider_switch].include?(type) - form_group(option, input_control) - end - end - - def feature_enabled_control(option, form) - tag.div(class: "form-group") do - content = option.key.to_s.titleize - content += tag.label(class: 'switch') do - tag.input(type: 'hidden', value: option.key, name: 'feature_key') + - tag.input(type: "checkbox", checked: option.is_enabled) + - tag.span(class: "slider") - end - - content += tag.div(class: "spinner-border d-none text-success", role: "status") do - tag.span(class: "sr-only") do - "Loading..." - end - end - - content.html_safe - end - end - - def slider_switch_control(option, form) - tag.div(class: "form-group") do - content = option.key.to_s.titleize - content += tag.label(class: 'switch') do - tag.input(type: 'hidden', value: option.key, name: 'feature_key') + - tag.input(type: "checkbox", checked: option.item) + - tag.span(class: "slider") - end - - # content += tag.div(class: "spinner-border d-none text-success", role: "status") do - # tag.span(class: "sr-only") do - # "Loading..." - # end - # end - - content.html_safe - end - end - - # def slider_switch_control(option, form) - # meta = option.meta - # # input_value = value_for(option, form) || option.item || meta&.default - - # tag.div(class: "form-group") do - # tag.label(for: option.key.to_s, value: option.key.to_s.titleize, class: 'pr-2') do - # option.key.to_s.titleize - # end + - # tag.input(nil, id: 'settingToggle', type: "checkbox", name: input_name_for(option, form), checked: true, data: {toggle: 'toggle', style: 'ios'}) + - # tag.label(class: 'pl-2') { meta&.label } + - # tag.a(class: "btn btn-secondary ml-4", role: "button", href: '#') do - # "Configure #{meta.value_hash['key'].to_s.titleize}" - # end - # end - # end - - def select_control(setting, form) - id = setting[:key].to_s - selected_option = "Choose..." - meta = setting[:meta] + def render_model_feature(feature, form, registry) + query_setting = feature.settings.detect{|s| s.key == :model_query_params} + query_params = JSON.parse(query_setting.item) if query_setting - # aria_describedby = id + result = registry[feature.key]{ query_params || {}}.success + filter_enabled = feature.settings.detect{|s| s.key == :filter_enabled} - value = value_for(setting, form) || setting.item || meta&.default - option_list = tag.option(selected_option, selected: (value.blank? ? true : false)) - meta.enum.each do |choice| - option_list += tag.option(choice.first[1], selected: (choice.first[0].to_s == value.to_s), value: choice.first[0]) - end - - tag.select(option_list, id: id, class: "form-control", name: input_name_for(setting, form)) - end + filter_data = result[0] if filter_enabled + record = result.last - def select_dropdown(input_id, list, show_default = false, selected = nil) - name = (input_id.to_s.scan(/supported_languages/).present? ? input_id : 'admin[' + input_id.to_s + ']') - - return unless list.is_a? Array - content_tag(:select, class: "form-control", id: input_id, name: name, required: true) do - concat(content_tag(:option, "Select", value: "")) unless show_default - list.each do |item| - if item.is_a? Array - is_selected = false - is_selected = true if selected.present? && selected == item[1] - concat(content_tag(:option, item[0], value: item[1], selected: is_selected)) - elsif item.is_a? Hash - concat(content_tag(:option, item.first[1], value: item.first[0])) - elsif input_id == 'state' - concat(content_tag(:option, item.to_s.titleize, value: item)) - elsif show_default - concat(content_tag(:option, item, value: item)) - else - concat(content_tag(:option, item.to_s.humanize, value: item)) - end - end - end - end - - def input_import_control(setting, _form) - id = setting[:key].to_s - aria_describedby = id - label = setting[:title] || id.titleize - - tag.div(class: "input-group-prepend") do - tag.span('Upload', class: "input-group-text", id: id) - end + - tag.div(class: "custom-file") do - tag.input(nil, type: "file", id: id, name: id + "[value]", class: "custom-file-input", aria: { describedby: aria_describedby }) + - tag.label('Choose File', for: id, value: label, class: "custom-file-label") - end - end - - def input_radio_control(setting, form) - meta = setting.meta - input_value = value_for(setting, form) || setting.item || meta&.default - aria_label = "Radio button for following text input" #setting[:aria_label] || "Radio button for following text input" + content = form.hidden_field(:is_enabled) + + form.hidden_field(:namespace, value: feature.namespace_path.path.map(&:to_s).join('.')) - if setting.is_a?(ResourceRegistry::Setting) - element_name = input_name_for(setting, form) + if feature.meta.content_type == :model_filter + content += render_filter(form, feature, result[0]) else - element_name = form&.object_name.to_s + "[is_enabled]" - input_value = form.object&.is_enabled - input_value = setting.is_enabled if input_value.blank? - end - - meta.enum.collect do |choice| - choice = send(choice) if choice.is_a?(String) - input_group do - tag.div(tag.div(tag.input(nil, type: "radio", name: element_name, value: choice.first[0], checked: input_value.to_s == choice.first[0].to_s, required: true), class: "input-group-text"), class: "input-group-prepend") + - tag.input(nil, type: "text", placeholder: choice.first[1], class: "form-control", aria: {label: aria_label }) - end - end.join('').html_safe - end - - def input_checkbox_control(setting, form) - meta = setting.meta - input_value = value_for(setting, form) || setting.item || meta&.default - aria_label = 'Checkbox button for following text input' - meta.enum.collect do |choice| - choice = send(choice) if choice.is_a?(String) - input_group do - tag.div(tag.div(tag.input(nil, type: 'checkbox', name: "#{input_name_for(setting, form)}[]", value: choice.first[0], checked: input_value.include?(choice.first[0].to_s), required: false), class: 'input-group-text'), class: 'input-group-prepend') + - tag.input(nil, type: 'text', placeholder: choice.first[1], class: 'form-control', aria: {label: aria_label }) - end - end.join('').html_safe - end - - def input_file_control(setting, form) - meta = setting.meta - id = setting.key.to_s - aria_describedby = id - label = meta.label - input_value = setting.item || meta.default - - preview = if input_value.present? - tag.img(class: 'w-100', src: "data:#{meta.type};base64,#{input_value}") - else - tag.span('No logo') - end - - control_inputs = - tag.div(class: "input-group-prepend") do - tag.span('Upload', class: "input-group-text", id: id) - end + - tag.div(class: "custom-file") do - tag.input(nil, type: "file", id: id, name: form&.object_name.to_s + "[#{setting.key}]", class: "custom-file-input", aria: { describedby: aria_describedby }) + - tag.label('Choose File', for: id, value: label, class: "custom-file-label") - end - - control = - tag.div(class: "col-2") do - preview - end + - tag.div(class: 'input-group') do - control_inputs - end - - control - end + feature.settings.each_with_index do |setting, index| + next unless setting.meta + next if setting.key == :filter_enabled - # Wrap any input group in
tag - def input_group - tag.div(yield, class: "input-group") - end - - def value_for(setting, form, options = {}) - if options[:record].present? - item = JSON.parse(setting.item) - return options[:record].send(item['attribute']) - end - - value = if form.object.class.to_s.match(/^ResourceRegistry.*/).present? - form.object.settings.detect{|s| s.key == setting.key}&.item - else - form.object.send(setting.key) - end - - value = value.to_s if value.is_a?(FalseClass) - value - end - - def input_name_for(setting, form) - if form.object.class.to_s.match(/^ResourceRegistry.*/).present? - if form.index.present? - form&.object_name.to_s + "[#{form.index}][#{setting.key}]" - else - form&.object_name.to_s + "[settings][#{setting.key}]" + content += form.fields_for :settings, setting, {index: index} do |setting_form| + build_option_field(setting, setting_form, {record: record}) + end end - else - form&.object_name.to_s + "[#{setting.key}]" end - end - - def input_text_control(setting, form, options = {}) - id = setting[:key].to_s - - meta = setting[:meta] - input_value = value_for(setting, form, options) || setting.item || meta&.default - # aria_describedby = id - - is_required = meta[:is_required] == false ? meta[:is_required] : true - placeholder = "Enter #{meta[:label]}".gsub('*','') if meta[:description].blank? - # if meta[:attribute] - # tag.input(nil, type: "text", value: input_value, id: id, name: form&.object_name.to_s + "[#{id}]",class: "form-control", required: true) - # else - - tag.input(nil, type: "text", value: input_value, id: id, name: input_name_for(setting, form), placeholder: placeholder, class: "form-control", required: is_required) - # end - end - - def input_date_control(setting, form) - id = setting[:key].to_s - - date_value = value_for(setting, form) - date_value = date_value.to_date if date_value.is_a?(Time) - date_value = date_value.to_s(:db) if date_value.is_a?(Date) - - meta = setting[:meta] - input_value = date_value || setting.item || meta&.default - # aria_describedby = id - - is_required = meta&.is_required == false ? meta.is_required : true - - tag.input(nil, type: "date", value: input_value, id: id, name: input_name_for(setting, form), placeholder: "mm/dd/yyyy", class: "form-control", required: is_required) + content.html_safe end - def input_number_control(setting, form) - id = setting[:key].to_s - meta = setting[:meta] - input_value = value_for(setting, form) || meta.value || meta.default - # input_value = setting[:value] || setting[:default] - # aria_describedby = id - placeholder = "Enter #{meta[:label]}".gsub('*','') if meta[:description].blank? - - # if setting[:attribute] - tag.input(nil, type: "number", step: "any", value: input_value, id: id, name: input_name_for(setting, form), placeholder: placeholder, class: "form-control", required: true, oninput: "check(this)") - # else - # tag.input(nil, type: "number", step:"any", value: input_value, id: id, name: form&.object_name.to_s + "[value]",class: "form-control", required: true, oninput: "check(this)") - # end - end + def render_filter(form, feature, data) + id = feature.key.to_s + selected_option = feature.meta.label + # meta = setting[:meta] - def input_email_control(setting, form) - id = setting[:key].to_s - meta = setting[:meta] - input_value = meta.value || meta.default - # input_value = setting[:value] || setting[:default] # aria_describedby = id - # if setting[:attribute] - tag.input(nil, type: "email", step: "any", value: input_value, id: id, name: input_name_for(setting, form),class: "form-control", required: true, oninput: "check(this)") - # else - # tag.input(nil, type: "email", step:"any", value: input_value, id: id, name: form&.object_name.to_s + "[value]",class: "form-control", required: true, oninput: "check(this)") - # end - end - - def input_color_control(setting) - id = setting[:key].to_s - input_value = setting[:value] || setting[:default] - - tag.input(nil, type: "color", value: input_value, id: id) - end - - def input_swatch_control(setting, form) - # id = setting[:key].to_s - # color = setting[:value] || setting[:default] - id = setting[:key].to_s - meta = setting[:meta] - color = meta.value || meta.default - - tag.input(nil, type: "text", value: color, id: id, name: form&.object_name.to_s + "[value]",class: "js-color-swatch form-control") + - tag.div(tag.button(type: "button", id: id, class: "btn", value: "", style: "background-color: #{color}"), class: "input-group-append") - end - - def input_currency_control(setting, form) - id = setting[:key].to_s - meta = setting[:meta] - input_value = meta.value || meta.default - - # id = setting[:key].to_s - # input_value = setting[:value] || setting[:default] - aria_map = { label: "Amount (to the nearest dollar)"} - - tag.div(tag.span('$', class: "input-group-text"), class: "input-group-prepend") + - tag.input(nil, type: "text", value: input_value, id: id, name: input_name_for(setting, form), class: "form-control", aria: { map: aria_map }) + - tag.div(tag.span('.00', class: "input-group-text"), class: "input-group-append") - end - - def build_attribute_field(form, attribute) - setting = { - key: attribute, - default: form.object.send(attribute), - type: :string, - attribute: true - } - - input_control = input_text_control(setting, form) - form_group(setting, input_control) - end - - - ## FORM GROUPS - - # Build a general-purpose form group wrapper around the supplied input control - def form_group(setting, control) - id = setting[:key].to_s - # label = setting[:title] || id.titleize - label = setting.meta.label || id.titleize - help_id = id + 'HelpBlock' - # help_text = setting[:description] - # aria_label = setting[:aria_label] || "Radio button for following text input" - help_text = setting.meta.description - aria_label = "Radio button for following text input" #setting[:aria_label] || "Radio button for following text input" - - tag.div(class: "form-group") do - tag.label(for: id, value: label, aria: { label: aria_label }) do - label - end + - input_group { control } + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) + # value = value_for(setting, form) || setting.item || meta&.default + option_list = tag.option(selected_option, selected: true) + data.collect do |choice| + # option_list += tag.option(choice, selected: (choice.to_s == value.to_s), value: choice) + option_list += tag.option(choice, value: choice) end - end - def custom_form_group(setting, control) - id = setting[:key].to_s - # label = setting[:title] || id.titleize - label = setting.meta.label || id.titleize - help_id = id + 'HelpBlock' - help_text = setting.meta.description - aria_label = "#{setting.meta.content_type.to_s.humanize} button for following text input" #setting[:aria_label] || "Radio button for following text input" - - tag.div(class: "form-group") do - tag.label(for: id, value: label, aria: { label: aria_label }) do - label - end + - control + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) - end + tag.select(option_list, id: id, class: "form-control", name: feature.key.to_s) end def list_group_menu(nested_namespaces = nil, features = nil, options = {}) @@ -502,29 +130,84 @@ def list_tab_panels(features, feature_registry, _options = {}) end end - def feature_panel(feature_key, feature_registry, _options = {}) + def feature_panel(feature_key, registry, _options = {}) tag.div(class: 'card') do - content = tag.div(class: 'card-body') do - feature = defined?(Rails) ? find_feature(feature_key) : feature_registry[feature_key].feature - next if feature.blank? - tag.div(id: feature_key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature_key}-list") do - form_for(feature, as: 'feature', url: update_feature_exchanges_configuration_path(feature), method: :post, remote: true, authenticity_token: true) do |form| - form.hidden_field(:key) + - render_feature(feature, form) + - tag.div(class: 'row mt-3') do - tag.div(class: 'col-4') do - form.submit('Update', class: 'btn btn-primary') - end + - tag.div(class: 'col-6') do - tag.div(class: 'flash-message', id: feature_key.to_s + '-alert') - end - end + tag.div(class: 'card-body') do + feature = get_feature(feature_key, registry) + + if feature.present? + features = [feature] + if feature.item == 'features_display' + feature_group_display(feature) + else + if feature.item == 'feature_collection' + features_setting = feature.settings.detect{|setting| setting.meta&.content_type.to_s == 'feature_list_panel'} + feature_keys = features_setting.item + features = feature_keys.collect{|key| get_feature(key, registry)} + end + features.collect{|feature| feature_form_for(feature, registry)}.join.html_safe end - end.html_safe + end end end end + def feature_form_for(feature, registry) + tag.div(id: feature.key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature.key}-list", class: 'mt-5') do + form_for(feature, as: 'feature', url: update_feature_exchanges_configuration_path(feature), method: :post, remote: true, authenticity_token: true) do |form| + form.hidden_field(:key) + + render_feature(feature, form, registry) + + tag.div(class: 'row mt-3') do + tag.div(class: 'col-4') do + form.submit('Update', class: 'btn btn-primary') + end + + tag.div(class: 'col-6') do + tag.div(class: 'flash-message', id: feature.key.to_s + '-alert') + end + end + end + end.html_safe + end + + def feature_group_display(feature) + tag.div(id: feature.key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature.key}-list") do + feature.settings.collect do |setting| + feature_group_control(setting).html_safe if setting.meta && setting.meta.content_type.to_s == 'feature_group' + end.compact.join.html_safe + end.html_safe + end + + def feature_group_control(option) + features = option.item.collect{|key| find_feature(key)} + + features.collect do |feature| + feature.settings.collect do |setting| + section_name = setting.meta&.label || setting.key.to_s.titleize + tag.div(class: 'mt-5') do + tag.div(class: 'row') do + tag.div(class: 'col-md-8') do + tag.strong do + section_name + end + end + + tag.div(class: 'offset-md-6') do + tag.a(href: "/exchanges/configurations/#{feature.key}/edit", data: {remote: true}) do + tag.span do + "View #{section_name}" + end + end + end + + tag.div(class: 'col-md-8') do + tag.ul(class: 'list-unstyled ml-2') do + setting.item.collect{|value| tag.li{ value.to_s.titleize }}.join.html_safe + end + end + end + end + end + end.join.html_safe + end + def namespace_panel(namespace, feature_registry, _options = {}) tag.div(class: 'card') do tag.div(class: 'card-body') do @@ -533,9 +216,9 @@ def namespace_panel(namespace, feature_registry, _options = {}) namespace.features.each_with_index do |feature, index| namespace_content += form.fields_for :features, feature, {index: index} do |feature_form| - tag.div(id: feature.key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature.key}-list") do + tag.div(id: feature.key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature.key}-list", class: 'mt-2') do feature_form.hidden_field(:key) + - render_feature(feature, feature_form) + render_feature(feature, feature_form, feature_registry) end end end @@ -555,6 +238,10 @@ def namespace_panel(namespace, feature_registry, _options = {}) end end + def get_feature(feature_key, registry) + defined?(Rails) ? find_feature(feature_key) : registry[feature_key].feature + end + def find_feature(feature_key) feature_class = ResourceRegistry::Stores.feature_model return unless feature_class diff --git a/lib/resource_registry/models/mongoid/namespace_path.rb b/lib/resource_registry/models/mongoid/namespace_path.rb index 7f2afb34..04253dae 100644 --- a/lib/resource_registry/models/mongoid/namespace_path.rb +++ b/lib/resource_registry/models/mongoid/namespace_path.rb @@ -10,10 +10,6 @@ class NamespacePath embeds_one :meta, as: :metable, class_name: '::ResourceRegistry::Mongoid::Meta', cascade_callbacks: true embedded_in :feature, class_name: '::ResourceRegistry::Mongoid::Feature' - - def dotted_path - path.map(&:to_s).join('.') - end end end end diff --git a/lib/resource_registry/navigation.rb b/lib/resource_registry/navigation.rb index 38be3019..76f6ffec 100644 --- a/lib/resource_registry/navigation.rb +++ b/lib/resource_registry/navigation.rb @@ -100,25 +100,17 @@ def to_li(element) def content_to_expand(element) tag.div(class: 'collapse', id: "nav_#{element[:key]}", 'aria-expanded': 'false') do - (element[:features] + element[:namespaces]).reduce('') do |list, child_ele| + nav_features = element[:features].select{|feature| feature[:meta][:content_type] == :nav} + (nav_features + element[:namespaces]).reduce('') do |list, child_ele| list += to_ul(child_ele, true) end.html_safe end end def namespace_nav_link(element) - if element[:meta].blank? || element[:meta][:content_type] == :nav - tag.a(options[:tag_options][:a][:namespace_link][:options].merge(href: "#nav_#{element[:key]}", 'data-target': "#nav_#{element[:key]}")) do - tag.span do - element[:namespaces] ? element[:path].last.to_s.titleize : element[:meta][:label] - end - end - else - namespace_url = "/exchanges/configurations/#{element[:features].first[:key]}/namespace_edit" - tag.a(options[:tag_options][:a][:feature_link][:options].merge(href: namespace_url)) do - tag.span do - element[:namespaces] ? element[:path].last.to_s.titleize : element[:meta][:label] - end + tag.a(options[:tag_options][:a][:namespace_link][:options].merge(id: 'namespace-link', href: "#nav_#{element[:key]}", data: {target: "#nav_#{element[:key]}", feature: element[:features][0]&.[](:key)})) do + tag.span do + element[:namespaces] ? element[:path].last.to_s.titleize : element[:meta][:label] end end end diff --git a/lib/resource_registry/operations/namespaces/form.rb b/lib/resource_registry/operations/namespaces/form.rb index 7c4b5ce0..457b7b11 100644 --- a/lib/resource_registry/operations/namespaces/form.rb +++ b/lib/resource_registry/operations/namespaces/form.rb @@ -22,8 +22,9 @@ def call(namespace:, registry:) def find_features(namespace, registry) feature_keys = registry[:feature_graph].vertices.detect{|v| v.path == namespace}.feature_keys features = feature_keys.collect{|feature_key| find_feature(feature_key, registry)} + features_for_display = features.select{|feature| feature[:meta][:content_type] != :nav } - Success(features) + Success(features_for_display) end def construct_params(namespace, features) From 5160a8441f1f95c4f01e746c388199e3b6610488 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Thu, 21 Jan 2021 13:56:59 -0500 Subject: [PATCH 19/40] purge mongoid models --- lib/resource_registry/tasks/purge.rake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/resource_registry/tasks/purge.rake b/lib/resource_registry/tasks/purge.rake index 23b9197e..9eb7bb3e 100644 --- a/lib/resource_registry/tasks/purge.rake +++ b/lib/resource_registry/tasks/purge.rake @@ -5,7 +5,7 @@ namespace :resource_registry do task :purge => :environment do if defined? ::Mongoid::Document - + ResourceRegistry::Mongoid::Feature.delete_all else ResourceRegistry::ActiveRecord::Feature.delete_all end @@ -13,4 +13,4 @@ namespace :resource_registry do puts "::: Settings Delete Complete :::" puts "*"*80 unless Rails.env.test? end -end +end \ No newline at end of file From 0dd31f12a471bcaf2a681c80ac8d43e0d06e826a Mon Sep 17 00:00:00 2001 From: nisanth Date: Thu, 21 Jan 2021 16:06:27 -0500 Subject: [PATCH 20/40] setting default horizontal option on form group --- .../helpers/input_controls.rb | 23 +++++++++++++++---- .../helpers/view_controls.rb | 5 ++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/lib/resource_registry/helpers/input_controls.rb b/lib/resource_registry/helpers/input_controls.rb index c4ff5aed..b3e6bfe7 100644 --- a/lib/resource_registry/helpers/input_controls.rb +++ b/lib/resource_registry/helpers/input_controls.rb @@ -328,7 +328,7 @@ def input_group end # Build a general-purpose form group wrapper around the supplied input control - def form_group(setting, control) + def form_group(setting, control, options = {horizontal: true}) id = setting[:key].to_s # label = setting[:title] || id.titleize label = setting.meta.label || id.titleize @@ -339,10 +339,23 @@ def form_group(setting, control) aria_label = "Radio button for following text input" #setting[:aria_label] || "Radio button for following text input" tag.div(class: "form-group") do - tag.label(for: id, value: label, aria: { label: aria_label }) do - label - end + - input_group { control } + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) + if options[:horizontal] + tag.div(class: 'row') do + tag.div(class: 'col col-sm-12 col-md-4') do + tag.label(for: id, value: label, aria: { label: aria_label }) do + label + end + end + + tag.div(class: 'col col-sm-12 col-md-6') do + input_group { control } + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) + end + end + else + tag.label(for: id, value: label, aria: { label: aria_label }) do + label + end + + input_group { control } + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) + end end end diff --git a/lib/resource_registry/helpers/view_controls.rb b/lib/resource_registry/helpers/view_controls.rb index abaa5e82..fa9761fd 100644 --- a/lib/resource_registry/helpers/view_controls.rb +++ b/lib/resource_registry/helpers/view_controls.rb @@ -134,13 +134,12 @@ def feature_panel(feature_key, registry, _options = {}) tag.div(class: 'card') do tag.div(class: 'card-body') do feature = get_feature(feature_key, registry) - if feature.present? features = [feature] - if feature.item == 'features_display' + if feature[:item] == 'features_display' feature_group_display(feature) else - if feature.item == 'feature_collection' + if feature[:item] == 'feature_collection' features_setting = feature.settings.detect{|setting| setting.meta&.content_type.to_s == 'feature_list_panel'} feature_keys = features_setting.item features = feature_keys.collect{|key| get_feature(key, registry)} From f25fcfd270f4ca7b98a430118860e11394e0f583 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Thu, 21 Jan 2021 16:25:54 -0500 Subject: [PATCH 21/40] Added headers for display panel --- .../helpers/view_controls.rb | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/lib/resource_registry/helpers/view_controls.rb b/lib/resource_registry/helpers/view_controls.rb index abaa5e82..348734dd 100644 --- a/lib/resource_registry/helpers/view_controls.rb +++ b/lib/resource_registry/helpers/view_controls.rb @@ -181,29 +181,34 @@ def feature_group_control(option) features = option.item.collect{|key| find_feature(key)} features.collect do |feature| - feature.settings.collect do |setting| - section_name = setting.meta&.label || setting.key.to_s.titleize - tag.div(class: 'mt-5') do - tag.div(class: 'row') do - tag.div(class: 'col-md-8') do - tag.strong do - section_name - end - end + - tag.div(class: 'offset-md-6') do - tag.a(href: "/exchanges/configurations/#{feature.key}/edit", data: {remote: true}) do - tag.span do - "View #{section_name}" - end - end - end + - tag.div(class: 'col-md-8') do - tag.ul(class: 'list-unstyled ml-2') do - setting.item.collect{|value| tag.li{ value.to_s.titleize }}.join.html_safe + tag.div(class: 'mt-3') do + tag.h4 do + feature.meta.label + end + + feature.settings.collect do |setting| + section_name = setting.meta&.label || setting.key.to_s.titleize + tag.div(class: 'mt-3') do + tag.div(class: 'row') do + tag.div(class: 'col-md-4') do + tag.strong do + section_name + end + end + + tag.div(class: 'col-md-4') do + tag.a(href: "/exchanges/configurations/#{feature.key}/edit", data: {remote: true}) do + tag.span do + "View #{section_name}" + end + end + end + + tag.div(class: 'col-md-6') do + tag.ul(class: 'list-group list-group-flush ml-2') do + setting.item.collect{|value| tag.li(class: 'list-group-item'){ value.to_s.titleize }}.join.html_safe + end end end end - end + end.join.html_safe end end.join.html_safe end From a3fb1f044884500809e6bc12bdf8da9471b73329 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Thu, 21 Jan 2021 16:33:56 -0500 Subject: [PATCH 22/40] Added exception handling for model item json parse --- lib/resource_registry/helpers/view_controls.rb | 4 ++-- lib/resource_registry/models/mongoid/feature.rb | 2 ++ lib/resource_registry/models/mongoid/setting.rb | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/resource_registry/helpers/view_controls.rb b/lib/resource_registry/helpers/view_controls.rb index 8525d04a..01f47fd1 100644 --- a/lib/resource_registry/helpers/view_controls.rb +++ b/lib/resource_registry/helpers/view_controls.rb @@ -136,10 +136,10 @@ def feature_panel(feature_key, registry, _options = {}) feature = get_feature(feature_key, registry) if feature.present? features = [feature] - if feature[:item] == 'features_display' + if feature.item == 'features_display' feature_group_display(feature) else - if feature[:item] == 'feature_collection' + if feature.item == 'feature_collection' features_setting = feature.settings.detect{|setting| setting.meta&.content_type.to_s == 'feature_list_panel'} feature_keys = features_setting.item features = feature_keys.collect{|key| get_feature(key, registry)} diff --git a/lib/resource_registry/models/mongoid/feature.rb b/lib/resource_registry/models/mongoid/feature.rb index f1b6f287..f320ea79 100644 --- a/lib/resource_registry/models/mongoid/feature.rb +++ b/lib/resource_registry/models/mongoid/feature.rb @@ -20,6 +20,8 @@ def setting(key) def item JSON.parse(super) if super.present? + rescue JSON::ParserError + super end def item=(value) diff --git a/lib/resource_registry/models/mongoid/setting.rb b/lib/resource_registry/models/mongoid/setting.rb index b782bb5b..2ee43a6a 100644 --- a/lib/resource_registry/models/mongoid/setting.rb +++ b/lib/resource_registry/models/mongoid/setting.rb @@ -14,6 +14,8 @@ class Setting def item JSON.parse(super) if super.present? + rescue JSON::ParserError + super end def item=(value) From 1931b38ef187b33934e08f2ab96e0ec490df0c13 Mon Sep 17 00:00:00 2001 From: nisanth Date: Thu, 21 Jan 2021 16:58:46 -0500 Subject: [PATCH 23/40] adding info tag --- lib/resource_registry/helpers/input_controls.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/resource_registry/helpers/input_controls.rb b/lib/resource_registry/helpers/input_controls.rb index b3e6bfe7..5802fcf4 100644 --- a/lib/resource_registry/helpers/input_controls.rb +++ b/lib/resource_registry/helpers/input_controls.rb @@ -327,8 +327,13 @@ def input_group tag.div(yield, class: "input-group") end + def info_tooltip(title) + tag.i(class: 'fas fa-info-circle', rel: 'tooltip', title: title) do + end + end + # Build a general-purpose form group wrapper around the supplied input control - def form_group(setting, control, options = {horizontal: true}) + def form_group(setting, control, options = {horizontal: true, tooltip: true, title: 'test description'}) id = setting[:key].to_s # label = setting[:title] || id.titleize label = setting.meta.label || id.titleize @@ -346,7 +351,10 @@ def form_group(setting, control, options = {horizontal: true}) label end end + - tag.div(class: 'col col-sm-12 col-md-6') do + tag.div(class: 'col col-sm-12 col-md-1') do + info_tooltip(options[:title]) if options[:tooltip] + end + + tag.div(class: 'col col-sm-12 col-md-5') do input_group { control } + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) end end From bf1a7c78d3d0ff90fa32ea31783da184974d3ebf Mon Sep 17 00:00:00 2001 From: nisanth Date: Thu, 21 Jan 2021 17:05:10 -0500 Subject: [PATCH 24/40] commenting placeholder --- lib/resource_registry/helpers/input_controls.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/resource_registry/helpers/input_controls.rb b/lib/resource_registry/helpers/input_controls.rb index 5802fcf4..b3bee460 100644 --- a/lib/resource_registry/helpers/input_controls.rb +++ b/lib/resource_registry/helpers/input_controls.rb @@ -208,7 +208,7 @@ def input_text_control(setting, form, options = {}) # aria_describedby = id is_required = meta[:is_required] == false ? meta[:is_required] : true - placeholder = "Enter #{meta[:label]}".gsub('*','') if meta[:description].blank? + # placeholder = "Enter #{meta[:label]}".gsub('*','') if meta[:description].blank? # if meta[:attribute] # tag.input(nil, type: "text", value: input_value, id: id, name: form&.object_name.to_s + "[#{id}]",class: "form-control", required: true) # else From 63f75d09a54312ab894d58b0434ada2677a094f5 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Mon, 25 Jan 2021 11:33:04 -0500 Subject: [PATCH 25/40] updated view controls for filters and updates for feature update --- .../helpers/input_controls.rb | 21 +- .../helpers/view_controls.rb | 267 +++++++++--------- .../operations/features/update.rb | 9 +- .../operations/registries/create.rb | 2 +- lib/resource_registry/stores.rb | 4 +- .../stores/mongoid/persist.rb | 52 ++-- 6 files changed, 191 insertions(+), 164 deletions(-) diff --git a/lib/resource_registry/helpers/input_controls.rb b/lib/resource_registry/helpers/input_controls.rb index b3bee460..4e1259f4 100644 --- a/lib/resource_registry/helpers/input_controls.rb +++ b/lib/resource_registry/helpers/input_controls.rb @@ -208,7 +208,7 @@ def input_text_control(setting, form, options = {}) # aria_describedby = id is_required = meta[:is_required] == false ? meta[:is_required] : true - # placeholder = "Enter #{meta[:label]}".gsub('*','') if meta[:description].blank? + placeholder = "Enter #{meta[:label]}".gsub('*','') if meta[:description].blank? # if meta[:attribute] # tag.input(nil, type: "text", value: input_value, id: id, name: form&.object_name.to_s + "[#{id}]",class: "form-control", required: true) # else @@ -296,7 +296,7 @@ def input_currency_control(setting, form) def value_for(setting, form, options = {}) if options[:record].present? - item = JSON.parse(setting.item) + item = setting_value(setting) options[:record].send(item['attribute']) else value = if form.object.class.to_s.match(/^ResourceRegistry.*/).present? @@ -327,13 +327,8 @@ def input_group tag.div(yield, class: "input-group") end - def info_tooltip(title) - tag.i(class: 'fas fa-info-circle', rel: 'tooltip', title: title) do - end - end - # Build a general-purpose form group wrapper around the supplied input control - def form_group(setting, control, options = {horizontal: true, tooltip: true, title: 'test description'}) + def form_group(setting, control, options = {horizontal: true, tooltip: true}) id = setting[:key].to_s # label = setting[:title] || id.titleize label = setting.meta.label || id.titleize @@ -352,7 +347,7 @@ def form_group(setting, control, options = {horizontal: true, tooltip: true, tit end end + tag.div(class: 'col col-sm-12 col-md-1') do - info_tooltip(options[:title]) if options[:tooltip] + tag.i(class: 'fas fa-info-circle', rel: 'tooltip', title: setting.meta.description) if options[:tooltip] end + tag.div(class: 'col col-sm-12 col-md-5') do input_group { control } + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) @@ -394,4 +389,12 @@ def build_attribute_field(form, attribute) input_control = input_text_control(setting, form) form_group(setting, input_control) end + + def setting_value(setting) + if setting && setting.is_a?(ResourceRegistry::Setting) + JSON.parse(setting.item) + else + setting&.item + end + end end \ No newline at end of file diff --git a/lib/resource_registry/helpers/view_controls.rb b/lib/resource_registry/helpers/view_controls.rb index 01f47fd1..5b0d984c 100644 --- a/lib/resource_registry/helpers/view_controls.rb +++ b/lib/resource_registry/helpers/view_controls.rb @@ -6,7 +6,7 @@ module RegistryViewControls include ::InputControls def render_feature(feature, form, registry = nil) - return render_model_feature(feature, form, registry) if [:model_filter, :model_attributes].include?(feature.meta.content_type) + return render_model_feature(feature, form, registry) if feature.meta.content_type == :model_attributes feature = feature.feature if feature.is_a?(ResourceRegistry::FeatureDSL) content = form.hidden_field(:is_enabled) @@ -22,115 +22,151 @@ def render_feature(feature, form, registry = nil) end def render_model_feature(feature, form, registry) - query_setting = feature.settings.detect{|s| s.key == :model_query_params} - query_params = JSON.parse(query_setting.item) if query_setting + query_setting = feature.settings.detect{|setting| setting.key == :model_query_params} + query_params = setting_value(query_setting) - result = registry[feature.key]{ query_params || {}}.success - filter_enabled = feature.settings.detect{|s| s.key == :filter_enabled} + result = @filter_result + result = registry[feature.key]{ query_params || {}}.success unless result + filter_setting = feature.settings.detect{|s| s.key == :filter_params} - filter_data = result[0] if filter_enabled - record = result.last - - content = form.hidden_field(:is_enabled) + - form.hidden_field(:namespace, value: feature.namespace_path.path.map(&:to_s).join('.')) - - if feature.meta.content_type == :model_filter - content += render_filter(form, feature, result[0]) - else - feature.settings.each_with_index do |setting, index| - next unless setting.meta - next if setting.key == :filter_enabled + content = '' + content = render_filter(form, feature, result).html_safe if filter_setting + content += form.hidden_field(:is_enabled) + content += form.hidden_field(:namespace, value: feature.namespace_path.path.map(&:to_s).join('.')) - content += form.fields_for :settings, setting, {index: index} do |setting_form| - build_option_field(setting, setting_form, {record: record}) - end - end + feature.settings.each do |setting| + next if setting.meta.blank? || setting.key == :filter_params + content += build_option_field(setting, form, {record: result[:record]}) end content.html_safe end def render_filter(form, feature, data) - id = feature.key.to_s - selected_option = feature.meta.label - # meta = setting[:meta] - - # aria_describedby = id + filter_setting = feature.settings.detect{|s| s.key == :filter_params} + filter_params = setting_value(filter_setting) - # value = value_for(setting, form) || setting.item || meta&.default - option_list = tag.option(selected_option, selected: true) - data.collect do |choice| - # option_list += tag.option(choice, selected: (choice.to_s == value.to_s), value: choice) - option_list += tag.option(choice, value: choice) + option_list = tag.option(filter_setting.meta.label) + data[:calender_years].collect do |choice| + option_list += tag.option(choice, selected: (choice == data[:catalog_year]), value: choice) end - tag.select(option_list, id: id, class: "form-control", name: feature.key.to_s) + tag.div(class: 'row mb-4') do + tag.div(class: 'col-4') do + content = filter_params['criteria'].reduce([]) do |strs, (attr, value)| + strs << tag.input(type: 'hidden', id: "filter_#{attr}", name: "filter[#{attr}]", value: value) + end.join + content += tag.input(type: 'hidden', id: :target_feature, name: :target_feature, value: feature.key.to_s) + content += tag.select(option_list, id: filter_params['name'], class: "form-control feature-filter", name: "filter[#{filter_params['name']}]") + content.html_safe + end + end end - def list_group_menu(nested_namespaces = nil, features = nil, options = {}) - content = '' - tag.div({class: "list-group", id: "list-tab", role: "tablist"}.merge(options)) do - - if features - features.each do |feature| - feature_rec = ResourceRegistry::ActiveRecord::Feature.where(key: feature).first + # def list_group_menu(nested_namespaces = nil, features = nil, options = {}) + # content = '' + # tag.div({class: "list-group", id: "list-tab", role: "tablist"}.merge(options)) do + + # if features + # features.each do |feature| + # feature_rec = ResourceRegistry::ActiveRecord::Feature.where(key: feature).first + + # content += tag.a(href: "##{feature}", class: "list-group-item list-group-item-action border-0", 'data-toggle': 'list', role: 'tab', id: "list-#{feature}-list", 'aria-controls': feature.to_s) do + # feature_rec&.setting(:label)&.item || feature.to_s.titleize + # end.html_safe + # end + # end + + # if nested_namespaces + # nested_namespaces.each do |namespace, children| + + # content += tag.a(href: "##{namespace}-group", class: "list-group-item list-group-item-action border-0", 'data-toggle': 'collapse', role: 'tab', id: "list-#{namespace}-list", 'aria-controls': namespace.to_s) do + # "+ #{namespace.to_s.titleize}" + # end + + # content += tag.span('data-toggle': 'list') do + # list_group_menu(children[:namespaces], children[:features], {class: "list-group collapse ml-4", id: "#{namespace}-group"}) + # end + # end + # end + + # content.html_safe + # end + # end + + # def list_tab_panels(features, feature_registry, _options = {}) + # tag.div(class: 'card') do + # # content = tag.div(class: 'card-header') do + # # tag.h4(feature.setting(:label)&.item || feature.key.to_s.titleize) + # # end + + # content = tag.div(class: 'card-body') do + # feature_content = '' + # features.each do |feature_key| + # feature = defined?(Rails) ? find_feature(feature_key) : feature_registry[feature_key].feature + # next if feature.blank? + + # feature_content += tag.div(id: feature_key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature_key}-list") do + # form_for(feature, as: 'feature', url: exchanges_configuration_path(feature), method: :patch, remote: true, authenticity_token: true) do |form| + # form.hidden_field(:key) + + # render_feature(feature, form) + + # tag.div(class: 'row mt-3') do + # tag.div(class: 'col-4') do + # form.submit(class: 'btn btn-primary') + # end + + # tag.div(class: 'col-6') do + # tag.div(class: 'flash-message', id: feature_key.to_s + '-alert') + # end + # end + # end + # end + # end + # feature_content.html_safe + # end.html_safe + # end + # end - content += tag.a(href: "##{feature}", class: "list-group-item list-group-item-action border-0", 'data-toggle': 'list', role: 'tab', id: "list-#{feature}-list", 'aria-controls': feature.to_s) do - feature_rec&.setting(:label)&.item || feature.to_s.titleize - end.html_safe + def namespace_panel(namespace, feature_registry, _options = {}) + tag.div(class: 'card') do + tag.div(class: 'card-body') do + if namespace.features.any?{|f| f.meta.content_type == :model_attributes} + namespace.features.collect{|feature| construct_feature_form(feature, feature_registry)}.join(tag.hr(class: 'mt-2 mb-3')).html_safe + else + construct_namespace_form(namespace, feature_registry) end - end - - if nested_namespaces - nested_namespaces.each do |namespace, children| + end.html_safe + end + end - content += tag.a(href: "##{namespace}-group", class: "list-group-item list-group-item-action border-0", 'data-toggle': 'collapse', role: 'tab', id: "list-#{namespace}-list", 'aria-controls': namespace.to_s) do - "+ #{namespace.to_s.titleize}" - end + def construct_namespace_form(namespace, registry) + form_for(namespace, as: 'namespace', url: update_namespace_exchanges_configurations_path, method: :post, remote: true, authenticity_token: true) do |form| + namespace_content = form.hidden_field(:path, value: namespace.path.map(&:to_s).join('.')) - content += tag.span('data-toggle': 'list') do - list_group_menu(children[:namespaces], children[:features], {class: "list-group collapse ml-4", id: "#{namespace}-group"}) + namespace.features.each_with_index do |feature, index| + namespace_content += form.fields_for :features, feature, {index: index} do |feature_form| + tag.div(id: feature.key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature.key}-list", class: 'mt-2') do + feature_form.hidden_field(:key) + + render_feature(feature, feature_form, feature_registry) end end end - content.html_safe - end - end - - def list_tab_panels(features, feature_registry, _options = {}) - tag.div(class: 'card') do - # content = tag.div(class: 'card-header') do - # tag.h4(feature.setting(:label)&.item || feature.key.to_s.titleize) - # end - - content = tag.div(class: 'card-body') do - feature_content = '' - features.each do |feature_key| - feature = defined?(Rails) ? find_feature(feature_key) : feature_registry[feature_key].feature - next if feature.blank? - - feature_content += tag.div(id: feature_key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature_key}-list") do - form_for(feature, as: 'feature', url: exchanges_configuration_path(feature), method: :patch, remote: true, authenticity_token: true) do |form| - form.hidden_field(:key) + - render_feature(feature, form) + - tag.div(class: 'row mt-3') do - tag.div(class: 'col-4') do - form.submit(class: 'btn btn-primary') - end + - tag.div(class: 'col-6') do - tag.div(class: 'flash-message', id: feature_key.to_s + '-alert') - end - end - end + namespace_content += tag.div(class: 'row mt-3') do + tag.div(class: 'col-4') do + form.submit('Update', class: 'btn btn-primary') + end + + tag.div(class: 'col-6') do + tag.div(class: 'flash-message', id: namespace.path.map(&:to_s).join('-') + '-alert') end end - feature_content.html_safe - end.html_safe + + namespace_content.html_safe end end + + def feature_panel(feature_key, registry, options = {}) + @filter_result = options[:filter_result] - def feature_panel(feature_key, registry, _options = {}) tag.div(class: 'card') do tag.div(class: 'card-body') do feature = get_feature(feature_key, registry) @@ -144,28 +180,33 @@ def feature_panel(feature_key, registry, _options = {}) feature_keys = features_setting.item features = feature_keys.collect{|key| get_feature(key, registry)} end - features.collect{|feature| feature_form_for(feature, registry)}.join.html_safe + features.collect{|feature| construct_feature_form(feature, registry)}.join(tag.hr(class: 'mt-2 mb-3')).html_safe end end end end end - def feature_form_for(feature, registry) - tag.div(id: feature.key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature.key}-list", class: 'mt-5') do - form_for(feature, as: 'feature', url: update_feature_exchanges_configuration_path(feature), method: :post, remote: true, authenticity_token: true) do |form| - form.hidden_field(:key) + - render_feature(feature, form, registry) + - tag.div(class: 'row mt-3') do - tag.div(class: 'col-4') do - form.submit('Update', class: 'btn btn-primary') - end + - tag.div(class: 'col-6') do - tag.div(class: 'flash-message', id: feature.key.to_s + '-alert') - end - end + def construct_feature_form(feature, registry) + tag.div(id: feature.key.to_s, 'aria-labelledby': "list-#{feature.key}-list", class: 'card border-0') do + tag.div(class: 'card-body') do + tag.div(class: 'card-title h6 font-weight-bold mb-4') do + feature.meta.label + end + + form_for(feature, as: 'feature', url: update_feature_exchanges_configuration_path(feature.key), method: :post, remote: true, authenticity_token: true) do |form| + form.hidden_field(:key) + + render_feature(feature, form, registry) + + tag.div(class: 'row mt-3') do + tag.div(class: 'col-4') do + form.submit('Update', class: 'btn btn-primary') + end + + tag.div(class: 'col-6') do + tag.div(class: 'flash-message', id: feature.key.to_s + '-alert') + end + end + end end - end.html_safe + end end def feature_group_display(feature) @@ -212,36 +253,6 @@ def feature_group_control(option) end.join.html_safe end - def namespace_panel(namespace, feature_registry, _options = {}) - tag.div(class: 'card') do - tag.div(class: 'card-body') do - form_for(namespace, as: 'namespace', url: update_namespace_exchanges_configurations_path, method: :post, remote: true, authenticity_token: true) do |form| - namespace_content = form.hidden_field(:path, value: namespace.path.map(&:to_s).join('.')) - - namespace.features.each_with_index do |feature, index| - namespace_content += form.fields_for :features, feature, {index: index} do |feature_form| - tag.div(id: feature.key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature.key}-list", class: 'mt-2') do - feature_form.hidden_field(:key) + - render_feature(feature, feature_form, feature_registry) - end - end - end - - namespace_content += tag.div(class: 'row mt-3') do - tag.div(class: 'col-4') do - form.submit('Update', class: 'btn btn-primary') - end + - tag.div(class: 'col-6') do - tag.div(class: 'flash-message', id: namespace.path.map(&:to_s).join('-') + '-alert') - end - end - - namespace_content.html_safe - end - end.html_safe - end - end - def get_feature(feature_key, registry) defined?(Rails) ? find_feature(feature_key) : registry[feature_key].feature end diff --git a/lib/resource_registry/operations/features/update.rb b/lib/resource_registry/operations/features/update.rb index 139dcf27..12072455 100644 --- a/lib/resource_registry/operations/features/update.rb +++ b/lib/resource_registry/operations/features/update.rb @@ -10,10 +10,10 @@ class Update def call(params) feature_params = yield build_params(params[:feature].to_h, params[:registry]) entity = yield create_entity(feature_params) - feature = yield save_record(entity) if defined? Rails + feature = yield save_record(entity, params[:registry], params[:filter]) if defined? Rails updated_feature = yield update_registry(entity, params[:registry]) - Success(feature || entity) + Success(entity) end private @@ -47,11 +47,12 @@ def create_entity(params) ResourceRegistry::Operations::Features::Create.new.call(params) end - def save_record(entity) - ResourceRegistry::Stores.persist(entity) || Success(entity) + def save_record(entity, registry, filter_params = nil) + ResourceRegistry::Stores.persist(entity, registry, {filter: filter_params}) || Success(entity) end def update_registry(entity, registry) + return Success(entity) if registry[entity.key].meta&.content_type == :model_attributes ResourceRegistry::Stores::Container::Update.new.call(entity, registry) end end diff --git a/lib/resource_registry/operations/registries/create.rb b/lib/resource_registry/operations/registries/create.rb index dcbe928c..06ee5476 100644 --- a/lib/resource_registry/operations/registries/create.rb +++ b/lib/resource_registry/operations/registries/create.rb @@ -61,7 +61,7 @@ def create(feature_hashes) def persist(features, registry) features.each do |feature| - ResourceRegistry::Stores.persist(feature) if defined? Rails + ResourceRegistry::Stores.persist(feature, registry) if defined? Rails registry.register_feature(feature) end diff --git a/lib/resource_registry/stores.rb b/lib/resource_registry/stores.rb index b0ee9d83..81d4110c 100644 --- a/lib/resource_registry/stores.rb +++ b/lib/resource_registry/stores.rb @@ -13,10 +13,10 @@ module ResourceRegistry module Stores class << self - def persist(entity) + def persist(entity, registry, params = {}) return unless store_namespace - "#{store_namespace}::Persist".constantize.new.call(entity) + "#{store_namespace}::Persist".constantize.new.call(entity, registry, params) end def find(feature_key) diff --git a/lib/resource_registry/stores/mongoid/persist.rb b/lib/resource_registry/stores/mongoid/persist.rb index 19d7119c..665ad46a 100644 --- a/lib/resource_registry/stores/mongoid/persist.rb +++ b/lib/resource_registry/stores/mongoid/persist.rb @@ -10,26 +10,30 @@ class Persist # @param [ResourceRegistry::Entities::Registry] container the container instance to which the constant will be assigned # @param [String] constant_name the name to assign to the container and its associated dependency injector # @return [Dry::Container] A non-finalized Dry::Container with associated configuration values wrapped in Dry::Monads::Result - def call(entity) - feature = yield find(entity) - feature = yield persist(entity, feature) + def call(entity, registry, params = {}) + record = yield find(entity, registry, params[:filter]) + record = yield persist(entity, record) - Success(feature) + Success(record) end private - def find(entity) - feature = ResourceRegistry::Mongoid::Feature.where(key: entity.key).first - - Success(feature) + def find(entity, registry, filter_params = nil) + record = if filter_params + registry[entity.key]{ filter_params }.success[:record] + else + ResourceRegistry::Mongoid::Feature.where(key: entity.key).first + end + + Success(record) end - def persist(entity, feature) - if feature.blank? + def persist(entity, record) + if record.blank? create(entity) else - update(entity, feature) + update(entity, record) end end @@ -39,18 +43,26 @@ def create(entity) }.to_result end - def update(entity, feature) + def update(entity, record) Try { - feature.is_enabled = entity.is_enabled - entity.settings.each do |setting_entity| - if setting = feature.settings.detect{|setting| setting.key == setting_entity.key} - setting.item = setting_entity.item - else - feature.settings.build(setting_entity.to_h) + if record.class.to_s.match?(/ResourceRegistry/) + record.is_enabled = entity.is_enabled + entity.settings.each do |setting_entity| + if setting = record.settings.detect{|setting| setting.key == setting_entity.key} + setting.item = setting_entity.item + else + record.settings.build(setting_entity.to_h) + end + end + else + attributes = entity.settings.reduce({}) do |attrs, setting| + attrs[setting.key] = setting.item + attrs end + record.assign_attributes(attributes) end - feature.save - feature + record.save(validate: false) + record }.to_result end end From 4c4eee43e225a81255c9d4d708bbdee8e10c908e Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Mon, 25 Jan 2021 20:29:57 -0500 Subject: [PATCH 26/40] Fixed persist logic for date ranges --- .../helpers/input_controls.rb | 30 +++++++++++++++++-- .../helpers/view_controls.rb | 6 ++-- .../stores/container/update.rb | 8 ++--- .../validation/setting_contract.rb | 3 ++ 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/lib/resource_registry/helpers/input_controls.rb b/lib/resource_registry/helpers/input_controls.rb index 4e1259f4..1a7f6383 100644 --- a/lib/resource_registry/helpers/input_controls.rb +++ b/lib/resource_registry/helpers/input_controls.rb @@ -19,6 +19,8 @@ def build_option_field(option, form, attrs = {}) input_email_control(option, form) when :date input_date_control(option, form) + when :date_range + input_date_range_control(option, form) when :currency input_currency_control(option, form) when :feature_enabled @@ -228,11 +230,35 @@ def input_date_control(setting, form) input_value = date_value || setting.item || meta&.default # aria_describedby = id - is_required = meta&.is_required == false ? meta.is_required : true + is_required = meta[:is_required] == false ? meta[:is_required] : true tag.input(nil, type: "date", value: input_value, id: id, name: input_name_for(setting, form), placeholder: "mm/dd/yyyy", class: "form-control", required: is_required) end + + def input_date_range_control(setting, form) + meta = setting[:meta] + + date_bounds = setting.item.split('..').collect do |date_str| + if date_str.match?(/\d{4}-\d{2}-\d{2}/) + date_str + else + date = Date.strptime(date_str, "%m/%d/%Y") + date.to_s(:db) + end + end + + is_required = meta[:is_required] == false ? meta[:is_required] : true + + from_input_name = form&.object_name.to_s + "[settings][#{setting.key}][begin]" + to_input_name = form&.object_name.to_s + "[settings][#{setting.key}][end]" + + tag.input(nil, type: "date", value: date_bounds[0], id: from_input_name, name: from_input_name, placeholder: "mm/dd/yyyy", class: "form-control", required: is_required) + + tag.div(class: 'input-group-addon') { 'to' } + + tag.input(nil, type: "date", value: date_bounds[1], id: to_input_name, name: to_input_name, placeholder: "mm/dd/yyyy", class: "form-control", required: is_required) + end + + def input_number_control(setting, form) id = setting[:key].to_s meta = setting[:meta] @@ -349,7 +375,7 @@ def form_group(setting, control, options = {horizontal: true, tooltip: true}) tag.div(class: 'col col-sm-12 col-md-1') do tag.i(class: 'fas fa-info-circle', rel: 'tooltip', title: setting.meta.description) if options[:tooltip] end + - tag.div(class: 'col col-sm-12 col-md-5') do + tag.div(class: 'col col-sm-12 col-md-7') do input_group { control } + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) end end diff --git a/lib/resource_registry/helpers/view_controls.rb b/lib/resource_registry/helpers/view_controls.rb index 5b0d984c..14f694d9 100644 --- a/lib/resource_registry/helpers/view_controls.rb +++ b/lib/resource_registry/helpers/view_controls.rb @@ -153,7 +153,7 @@ def construct_namespace_form(namespace, registry) namespace_content += tag.div(class: 'row mt-3') do tag.div(class: 'col-4') do - form.submit('Update', class: 'btn btn-primary') + form.submit('Save', class: 'btn btn-primary') end + tag.div(class: 'col-6') do tag.div(class: 'flash-message', id: namespace.path.map(&:to_s).join('-') + '-alert') @@ -178,7 +178,7 @@ def feature_panel(feature_key, registry, options = {}) if feature.item == 'feature_collection' features_setting = feature.settings.detect{|setting| setting.meta&.content_type.to_s == 'feature_list_panel'} feature_keys = features_setting.item - features = feature_keys.collect{|key| get_feature(key, registry)} + features = feature_keys.collect{|key| get_feature(key, registry)}.compact end features.collect{|feature| construct_feature_form(feature, registry)}.join(tag.hr(class: 'mt-2 mb-3')).html_safe end @@ -198,7 +198,7 @@ def construct_feature_form(feature, registry) render_feature(feature, form, registry) + tag.div(class: 'row mt-3') do tag.div(class: 'col-4') do - form.submit('Update', class: 'btn btn-primary') + form.submit('Save', class: 'btn btn-primary') end + tag.div(class: 'col-6') do tag.div(class: 'flash-message', id: feature.key.to_s + '-alert') diff --git a/lib/resource_registry/stores/container/update.rb b/lib/resource_registry/stores/container/update.rb index 2eda55d3..3e53ead7 100644 --- a/lib/resource_registry/stores/container/update.rb +++ b/lib/resource_registry/stores/container/update.rb @@ -22,10 +22,10 @@ def update(new_feature, container) registered_feature_hash = container[new_feature.key].feature.to_h registered_feature_hash[:is_enabled] = new_feature.is_enabled - new_feature.settings.each do |setting| - registered_feature_hash[:settings].each do |setting_hash| - setting_hash[:item] = setting.item if setting.key == setting_hash[:key] - end + registered_feature_hash[:settings] = registered_feature_hash[:settings].collect do |setting_hash| + new_setting = new_feature.settings.detect{|setting| setting.key == setting_hash[:key]} + setting_hash[:item] = new_setting.item if new_setting + setting_hash end updated_feature = ResourceRegistry::Operations::Features::Create.new.call(registered_feature_hash).value! diff --git a/lib/resource_registry/validation/setting_contract.rb b/lib/resource_registry/validation/setting_contract.rb index 2558d52f..d65d2d61 100644 --- a/lib/resource_registry/validation/setting_contract.rb +++ b/lib/resource_registry/validation/setting_contract.rb @@ -28,6 +28,9 @@ class SettingContract < ResourceRegistry::Validation::ApplicationContract dates = dates.collect{|str| Date.strptime(str, "%Y-%m-%d") } Range.new(*dates) end + elsif setting[:item].is_a?(Hash) && setting[:item][:begin] && setting[:item][:end] + dates = [setting[:item][:begin], setting[:item][:end]].collect{|str| Date.strptime(str, "%Y-%m-%d") } + Range.new(*dates) end setting.to_h.merge(item: item) if item From 51c499d7444a646b0a9d1e95bd62828fd7d4ea82 Mon Sep 17 00:00:00 2001 From: Dan Thomas Date: Tue, 26 Jan 2021 08:17:24 -0500 Subject: [PATCH 27/40] Gem cleanup - change to dev dependency, list in alpha order --- Gemfile | 5 +++++ Gemfile.lock | 21 ++++++++++--------- resource_registry.gemspec | 44 ++++++++++++++++++--------------------- 3 files changed, 36 insertions(+), 34 deletions(-) diff --git a/Gemfile b/Gemfile index 3b10a930..08b5d5ea 100644 --- a/Gemfile +++ b/Gemfile @@ -5,3 +5,8 @@ source "https://rubygems.org" # Specify your gem's dependencies in resource_registry.gemspec gemspec +group :development, :test do + gem "pry", platform: :mri + gem "pry-byebug", platform: :mri +end + diff --git a/Gemfile.lock b/Gemfile.lock index 29ec4d2f..ec77ab42 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,7 +2,6 @@ PATH remote: . specs: resource_registry (0.9.0) - bootsnap (~> 1.0) deep_merge (>= 1.0.0) dry-configurable (= 0.9) dry-container (~> 0.7) @@ -16,7 +15,6 @@ PATH mime-types nokogiri (>= 1.9.1) ox (~> 2.0) - pry-byebug rack (>= 1.6.13) rgl @@ -62,12 +60,12 @@ GEM dry-container (0.7.2) concurrent-ruby (~> 1.0) dry-configurable (~> 0.1, >= 0.1.3) - dry-core (0.4.9) + dry-core (0.4.10) concurrent-ruby (~> 1.0) dry-equalizer (0.3.0) dry-inflector (0.2.0) - dry-initializer (3.0.3) - dry-logic (1.0.6) + dry-initializer (3.0.4) + dry-logic (1.0.8) concurrent-ruby (~> 1.0) dry-core (~> 0.2) dry-equalizer (~> 0.2) @@ -77,7 +75,7 @@ GEM concurrent-ruby (~> 1.0) dry-core (~> 0.4, >= 0.4.4) dry-equalizer - dry-schema (1.5.0) + dry-schema (1.5.6) concurrent-ruby (~> 1.0) dry-configurable (~> 0.8, >= 0.8.3) dry-core (~> 0.4) @@ -97,13 +95,13 @@ GEM dry-equalizer (~> 0.3) dry-inflector (~> 0.1, >= 0.1.2) dry-logic (~> 1.0, >= 1.0.2) - dry-validation (1.5.0) + dry-validation (1.6.0) concurrent-ruby (~> 1.0) dry-container (~> 0.7, >= 0.7.1) dry-core (~> 0.4) dry-equalizer (~> 0.2) dry-initializer (~> 3.0) - dry-schema (~> 1.5) + dry-schema (~> 1.5, >= 1.5.2) erubi (1.9.0) i18n (1.8.2) concurrent-ruby (~> 1.0) @@ -116,7 +114,7 @@ GEM method_source (0.9.2) mime-types (3.3.1) mime-types-data (~> 3.2015) - mime-types-data (3.2020.0425) + mime-types-data (3.2020.1104) mini_portile2 (2.4.0) minitest (5.14.0) mongo (2.12.1) @@ -127,7 +125,7 @@ GEM msgpack (1.3.3) nokogiri (1.10.9) mini_portile2 (~> 2.4.0) - ox (2.13.2) + ox (2.13.4) parallel (1.19.1) parser (2.7.1.1) ast (~> 2.4.0) @@ -204,9 +202,12 @@ PLATFORMS DEPENDENCIES actionview (>= 5.2.4.2) activesupport (~> 5.2.4) + bootsnap (~> 1.0) bundler (~> 2.0) database_cleaner (~> 1.7) mongoid (~> 6.0) + pry + pry-byebug rake (~> 12.0) resource_registry! rspec (~> 3.9) diff --git a/resource_registry.gemspec b/resource_registry.gemspec index bd8452fe..aa114225 100644 --- a/resource_registry.gemspec +++ b/resource_registry.gemspec @@ -32,41 +32,37 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 2.5.1' - spec.add_dependency 'dry-validation', '~> 1.2' - spec.add_dependency 'dry-struct', '~> 1.0' - spec.add_dependency 'dry-types', '~> 1.0' - spec.add_dependency 'dry-configurable', '0.9' - - spec.add_dependency 'dry-container', '~> 0.7' spec.add_dependency 'deep_merge', '>= 1.0.0' - # Dependency gems added for security purposes - spec.add_dependency 'nokogiri', ">= 1.9.1" - spec.add_dependency "rack", ">= 1.6.13" - spec.add_dependency 'dry-monads', '~> 1.2' + spec.add_dependency 'dry-configurable', '0.9' + spec.add_dependency 'dry-container', '~> 0.7' spec.add_dependency 'dry-matcher', '~> 0.7' - - spec.add_dependency "loofah", ">= 2.3.1" - spec.add_development_dependency "actionview", ">= 5.2.4.2" - # end of dependency gem security updates + spec.add_dependency 'dry-monads', '~> 1.2' + spec.add_dependency 'dry-struct', '~> 1.0' + spec.add_dependency 'dry-types', '~> 1.0' + spec.add_dependency 'dry-validation', '~> 1.2' spec.add_dependency 'i18n', '>= 0.7.0' spec.add_dependency 'ox', '~> 2.0' - spec.add_dependency 'bootsnap', '~> 1.0' + spec.add_dependency 'loofah', '>= 2.3.1' + spec.add_dependency 'nokogiri', '>= 1.9.1' + spec.add_dependency 'rack', '>= 1.6.13' + spec.add_dependency 'mime-types' - spec.add_dependency 'pry-byebug' spec.add_dependency 'rgl'#, '~> 0.5.6' - spec.add_development_dependency "bundler", "~> 2.0" + spec.add_development_dependency "actionview", '>= 5.2.4.2' + spec.add_development_dependency 'activesupport', '~> 5.2.4' + spec.add_development_dependency 'bootsnap', '~> 1.0' + spec.add_development_dependency 'bundler', '~> 2.0' + spec.add_development_dependency 'database_cleaner', '~> 1.7' + spec.add_development_dependency 'mongoid', '~> 6.0' spec.add_development_dependency 'rake', '~> 12.0' spec.add_development_dependency 'rspec', '~> 3.9' spec.add_development_dependency 'rspec-rails', '~> 3.9' - spec.add_development_dependency 'mongoid', '~> 6.0' - spec.add_development_dependency 'activesupport', '~> 5.2.4' - spec.add_development_dependency "simplecov" #, '~> 1.0' - spec.add_development_dependency "database_cleaner", '~> 1.7' - spec.add_development_dependency "timecop", '~> 0.9' - spec.add_development_dependency "rubocop", '~> 0.74.0' - spec.add_development_dependency "yard", "~> 0.9" + spec.add_development_dependency 'rubocop', '~> 0.74.0' + spec.add_development_dependency 'simplecov' #, '~> 1.0' + spec.add_development_dependency 'timecop', '~> 0.9' + spec.add_development_dependency 'yard', '~> 0.9' end From f250485c4bbbdadb6a6b7e1d8a91ceed61b6ec47 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Thu, 28 Jan 2021 11:30:16 -0500 Subject: [PATCH 28/40] Added features clone operation --- lib/resource_registry/feature.rb | 1 + .../operations/features/clone.rb | 99 ++++++ .../aca_shop_market/feature_group.yml | 322 ++++++++++++++++++ .../operations/features/clone_spec.rb | 30 ++ 4 files changed, 452 insertions(+) create mode 100644 lib/resource_registry/operations/features/clone.rb create mode 100644 spec/rails_app/system/config/templates/features/aca_shop_market/feature_group.yml create mode 100644 spec/resource_registry/operations/features/clone_spec.rb diff --git a/lib/resource_registry/feature.rb b/lib/resource_registry/feature.rb index 39c139b0..2d87d5bb 100644 --- a/lib/resource_registry/feature.rb +++ b/lib/resource_registry/feature.rb @@ -3,6 +3,7 @@ require_relative 'validation/feature_contract' require_relative 'operations/features/create' +require_relative 'operations/features/clone' require_relative 'operations/features/configure' require_relative 'operations/features/disable' require_relative 'operations/features/enable' diff --git a/lib/resource_registry/operations/features/clone.rb b/lib/resource_registry/operations/features/clone.rb new file mode 100644 index 00000000..af659220 --- /dev/null +++ b/lib/resource_registry/operations/features/clone.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +module ResourceRegistry + module Operations + module Features + # Create a Feature + class Clone + send(:include, Dry::Monads[:result, :do, :try]) + + attr_reader :original_year, :new_calender_year + + def call(params:, registry:) + options = yield extract_options(params, registry) + params = yield construct_params(options) + features = yield create(params) + registry = yield persist(features, registry) + + Success(features) + end + + private + + def extract_options(params, registry) + feature_for_clone = registry[params[:target_feature]] + related_features = feature_for_clone.feature.settings.collect do |setting| + setting.item if setting.meta && setting.meta.content_type == :feature_list_panel + end.compact.flatten + + features = [] + features << feature_for_clone.feature + features += related_features.collect{|key| registry[key].feature} + + options = { + features: features, + calender_year: params[:settings][:calender_year] + } + + Success(options) + end + + def construct_params(options) + @original_year = options[:features][0].key.to_s.scan(/\d{4}/)[0] + @new_calender_year = options[:calender_year] + features_params = options[:features].collect do |feature| + serialize_hash(feature.to_h) + end + Success(features_params) + end + + def create(feature_hashes) + Try { + feature_hashes.collect do |feature_hash| + result = ResourceRegistry::Operations::Features::Create.new.call(feature_hash) + return result if result.failure? + result.value! + end + }.to_result + end + + def persist(features, registry) + features.each do |feature| + ResourceRegistry::Stores.persist(feature, registry) if defined? Rails + registry.register_feature(feature) + end + + Success(registry) + end + + def serialize_hash(attributes) + attributes.reduce({}) do |values, (key, value)| + values[key] = if value.is_a?(Hash) + serialize_hash(value) + elsif value.is_a?(Array) + value.collect do |element| + element.is_a?(Hash) ? serialize_hash(element) : serialize_text(element) + end + else + serialize_text(value) + end + + values + end + end + + def serialize_text(value) + return value if value.blank? + + if value.is_a?(Symbol) + value.to_s.gsub(original_year, new_calender_year).to_sym + elsif value.is_a?(String) + value.gsub(original_year, new_calender_year) + else + value + end + end + end + end + end +end \ No newline at end of file diff --git a/spec/rails_app/system/config/templates/features/aca_shop_market/feature_group.yml b/spec/rails_app/system/config/templates/features/aca_shop_market/feature_group.yml new file mode 100644 index 00000000..0c9279a6 --- /dev/null +++ b/spec/rails_app/system/config/templates/features/aca_shop_market/feature_group.yml @@ -0,0 +1,322 @@ +--- +registry: + - namespace: + path: + - :dchbx + - :enroll_app + - :aca_shop_market + meta: + label: ACA Shop + content_type: :feature_list + description: 'ACA Shop Market Settings' + is_required: true + is_visible: true + features: + - key: :health_packages + is_enabled: true + item: :features_display + meta: + label: Health Packages + content_type: :nav + default: :'' + description: 'Configurations for Health Product Packages' + is_required: true + is_visible: true + settings: + - key: :product_packages + item: + - :health_product_package_2021 + - :health_product_package_2020 + meta: + label: Product Packages + content_type: :feature_group + description: product packages + is_required: false + is_visible: true + - namespace: + - :dchbx + - :enroll_app + - :aca_shop_market + - :health_product_packages + features: + - key: :shop_product_package_clone + item: ResourceRegistry::Operations::Features::Clone.new + is_enabled: true + meta: + label: Shop Product Package Clone + content_type: :legend + default: nil + description: Clone shop product packages + is_required: false + is_visible: true + settings: + - key: :calender_year + item: nil + meta: + label: Calender Year + content_type: :select + description: Choose calender year for new product package + enum: <%= [Date.today.year + 1, Date.today.year + 2].reduce([]) {|values, date| values << [date, date]} %> + is_required: false + is_visible: true + - key: :health_product_package_2021 + is_enabled: true + item: :feature_collection + meta: + label: Health Product Package 2021 + content_type: :legend + default: true + description: Health Product Pacakge for 2021 + is_required: true + is_visible: true + settings: + - key: :contribution_models + item: + - :initial_sponsor_jan_default_2021 + - :initial_sponsor_default_2021 + - :renewal_sponsor_jan_default_2021 + - :renewal_sponsor_default_2021 + meta: + label: Contribution Models + content_type: :feature_list_panel + description: Contribution Models + is_required: false + is_visible: true + - key: :clone + item: "/exchanges/configurations/shop_product_package_clone/edit" + meta: + label: Clone Product Package + content_type: :feature_action + description: Clone Product Packages for the given calender year + is_required: false + is_visible: true + - key: :health_product_package_2020 + is_enabled: true + item: :feature_collection + meta: + label: Health Product Package 2020 + content_type: :legend + default: true + description: Health Product Pacakge for 2020 + is_required: true + is_visible: true + settings: + - key: :contribution_models + item: + - :initial_sponsor_jan_default_2020 + - :initial_sponsor_default_2020 + - :renewal_sponsor_jan_default_2020 + - :renewal_sponsor_default_2020 + meta: + label: Contribution Models + content_type: :feature_list_panel + description: Contribution Models + is_required: false + is_visible: true + - namespace: + - :enroll_app + - :aca_shop_market + - :benefit_market_catalog + - :catalog_2021 + - :contribution_model_criteria + features: + - key: :initial_sponsor_jan_default_2021 + item: :contribution_model_criterion + is_enabled: true + meta: + label: 2021 Initial Sponsor January Default + content_type: :legend + default: true + description: Contribution Criteria for January 2021 Initial Sponsors + is_required: true + is_visible: true + settings: + - key: :contribution_model_key + item: :zero_percent_sponsor_fixed_percent_contribution_model + meta: + label: Contribution Model Key + content_type: :select + description: Enter contribution model key + enum: [{zero_percent_sponsor_fixed_percent_contribution_model: "Zero Percent Sponsor Fixed Percent Contribution Model"}, {fifty_percent_sponsor_fixed_percent_contribution_model: "Fifty Percent Sponsor Fixed Percent Contribution Model"}] + is_required: true + is_visible: true + - key: :benefit_application_kind + item: :initial + meta: + label: Benefit Application Kind + content_type: :select + enum: [{initial: "Initial"}, {renewal: "Renewal"}] + default: '' + description: Enter benefit application kind + is_required: true + is_visible: true + - key: :effective_period + item: <%= Date.new(2021,1,1)..Date.new(2021,1,31) %> + meta: + label: Effective Period + content_type: :date_range + description: Please enter effective period for the contribution model + is_required: true + is_visible: true + - key: :order + item: 1 + - key: :default + item: false + - key: :renewal_criterion_key + item: :initial_sponsor_jan_default + meta: + label: Renewal Criterion Key + content_type: :text_field + description: Enter renewal criterion key + is_required: false + is_visible: true + - key: :initial_sponsor_default_2021 + item: :contribution_model_criterion + is_enabled: true + meta: + label: 2021 Initial Sponsor Default + content_type: :legend + default: true + description: Contribution Criteria for 2021 Initial Sponsors + is_required: true + is_visible: true + settings: + - key: :contribution_model_key + item: :zero_percent_sponsor_fixed_percent_contribution_model + meta: + label: Contribution Model Key + content_type: :select + description: Enter contribution model key + enum: [{zero_percent_sponsor_fixed_percent_contribution_model: "Zero Percent Sponsor Fixed Percent Contribution Model"}, {fifty_percent_sponsor_fixed_percent_contribution_model: "Fifty Percent Sponsor Fixed Percent Contribution Model"}] + is_required: true + is_visible: true + - key: :benefit_application_kind + item: :initial + meta: + label: Benefit Application Kind + content_type: :select + enum: [{initial: "Initial"}, {renewal: "Renewal"}] + default: '' + description: Enter benefit application kind + is_required: true + is_visible: true + - key: :effective_period + item: <%= Date.new(2021,2,1)..Date.new(2021,12,31) %> + meta: + label: Effective Period + content_type: :date_range + description: Please enter effective period for the contribution model + is_required: true + is_visible: true + - key: :order + item: 3 + - key: :default + item: true + - key: :renewal_criterion_key + item: :initial_sponsor_default + meta: + label: Renewal Criterion Key + content_type: :text_field + description: Enter renewal criterion key + is_required: false + is_visible: true + - key: :renewal_sponsor_jan_default_2021 + item: :contribution_model_criterion + is_enabled: true + meta: + label: 2021 Renewal January Sponsor Default + content_type: :legend + default: true + description: Contribution Criteria for January 2021 Renewal Sponsors + is_required: true + is_visible: true + settings: + - key: :contribution_model_key + item: :zero_percent_sponsor_fixed_percent_contribution_model + meta: + label: Contribution Model Key + content_type: :select + description: Enter contribution model key + enum: [{zero_percent_sponsor_fixed_percent_contribution_model: "Zero Percent Sponsor Fixed Percent Contribution Model"}, {fifty_percent_sponsor_fixed_percent_contribution_model: "Fifty Percent Sponsor Fixed Percent Contribution Model"}] + is_required: true + is_visible: true + - key: :benefit_application_kind + item: :renewal + meta: + label: Benefit Application Kind + content_type: :select + enum: [{initial: "Initial"}, {renewal: "Renewal"}] + default: '' + description: Enter benefit application kind + is_required: true + is_visible: true + - key: :effective_period + item: <%= Date.new(2021,1,1)..Date.new(2021,1,31) %> + meta: + label: Effective Period + content_type: :date_range + description: Please enter effective period for the contribution model + is_required: true + is_visible: true + - key: :order + item: 4 + - key: :default + item: false + - key: :renewal_criterion_key + item: :renewal_sponsor_jan_default + meta: + label: Renewal Criterion Key + content_type: :text_field + description: Enter renewal criterion key + is_required: false + is_visible: true + - key: :renewal_sponsor_default_2021 + item: :contribution_model_criterion + is_enabled: true + meta: + label: 2021 Renewal Sponsor Default + content_type: :legend + default: true + description: Contribution Criteria for 2021 Renewal Sponsors + is_required: true + is_visible: true + settings: + - key: :contribution_model_key + item: :zero_percent_sponsor_fixed_percent_contribution_model + meta: + label: Contribution Model Key + content_type: :select + description: Enter contribution model key + enum: [{zero_percent_sponsor_fixed_percent_contribution_model: "Zero Percent Sponsor Fixed Percent Contribution Model"}, {fifty_percent_sponsor_fixed_percent_contribution_model: "Fifty Percent Sponsor Fixed Percent Contribution Model"}] + is_required: true + is_visible: true + - key: :benefit_application_kind + item: :renewal + meta: + label: Benefit Application Kind + content_type: :select + enum: [{initial: "Initial"}, {renewal: "Renewal"}] + default: '' + description: Enter benefit application kind + is_required: true + is_visible: true + - key: :effective_period + item: <%= Date.new(2021,2,1)..Date.new(2021,12,31) %> + meta: + label: Effective Period + content_type: :date_range + description: Please enter effective period for the contribution model + is_required: true + is_visible: true + - key: :order + item: 2 + - key: :default + item: true + - key: :renewal_criterion_key + item: :renewal_sponsor_default + meta: + label: Renewal Criterion Key + content_type: :text_field + description: Enter renewal criterion key + is_required: false + is_visible: true \ No newline at end of file diff --git a/spec/resource_registry/operations/features/clone_spec.rb b/spec/resource_registry/operations/features/clone_spec.rb new file mode 100644 index 00000000..1b6856c8 --- /dev/null +++ b/spec/resource_registry/operations/features/clone_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ResourceRegistry::Operations::Features::Clone do + include RegistryDataSeed + + subject { described_class.new.call(params) } + + context 'When valid feature hash passed' do + + let(:registry) { ResourceRegistry::Registry.new } + let!(:register) { ResourceRegistry::Operations::Registries::Create.new.call(path: feature_group_template_path, registry: registry) } + + let(:params) { + { + params: { + target_feature: 'health_product_package_2021', + settings: {calender_year: "2022"} + }, + registry: registry + } + } + + it "should return success with hash output" do + expect(subject).to be_a Dry::Monads::Result::Success + expect(registry[:health_product_package_2022]).to be_truthy + end + end +end \ No newline at end of file From 98d15c605310f0b8a9f8f8332c10349907d24b1c Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Thu, 28 Jan 2021 11:32:43 -0500 Subject: [PATCH 29/40] updated view helpers --- .../helpers/input_controls.rb | 9 ++-- .../helpers/view_controls.rb | 46 +++++++++++++++---- lib/resource_registry/navigation.rb | 14 +++++- spec/support/registry_data_seed.rb | 4 ++ 4 files changed, 58 insertions(+), 15 deletions(-) diff --git a/lib/resource_registry/helpers/input_controls.rb b/lib/resource_registry/helpers/input_controls.rb index 1a7f6383..2d0a58a2 100644 --- a/lib/resource_registry/helpers/input_controls.rb +++ b/lib/resource_registry/helpers/input_controls.rb @@ -80,16 +80,17 @@ def slider_switch_control(option, form) end def select_control(setting, form) - id = setting[:key].to_s + id = setting.key.to_s selected_option = "Choose..." - meta = setting[:meta] + meta = setting.meta # aria_describedby = id value = value_for(setting, form) || setting.item || meta&.default option_list = tag.option(selected_option, selected: (value.blank? ? true : false)) meta.enum.each do |choice| - option_list += tag.option(choice.first[1], selected: (choice.first[0].to_s == value.to_s), value: choice.first[0]) + choice = choice.first if choice.is_a?(Hash) + option_list += tag.option(choice[1].to_s, selected: (choice[0].to_s == value.to_s), value: choice[0].to_s) end tag.select(option_list, id: id, class: "form-control", name: input_name_for(setting, form)) @@ -376,7 +377,7 @@ def form_group(setting, control, options = {horizontal: true, tooltip: true}) tag.i(class: 'fas fa-info-circle', rel: 'tooltip', title: setting.meta.description) if options[:tooltip] end + tag.div(class: 'col col-sm-12 col-md-7') do - input_group { control } + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) + input_group { control }# + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) end end else diff --git a/lib/resource_registry/helpers/view_controls.rb b/lib/resource_registry/helpers/view_controls.rb index 14f694d9..c97419c6 100644 --- a/lib/resource_registry/helpers/view_controls.rb +++ b/lib/resource_registry/helpers/view_controls.rb @@ -180,21 +180,32 @@ def feature_panel(feature_key, registry, options = {}) feature_keys = features_setting.item features = feature_keys.collect{|key| get_feature(key, registry)}.compact end - features.collect{|feature| construct_feature_form(feature, registry)}.join(tag.hr(class: 'mt-2 mb-3')).html_safe + features.collect{|feature| construct_feature_form(feature, registry, options)}.join(tag.hr(class: 'mt-2 mb-3')).html_safe end end end end end - def construct_feature_form(feature, registry) + def construct_feature_form(feature, registry, options = {}) + if options[:action_params] + clone_action = options[:action_params][:action].to_s == 'clone' + end + + submit_path = if clone_action + clone_feature_exchanges_configuration_path(feature.key) + else + update_feature_exchanges_configuration_path(feature.key) + end + tag.div(id: feature.key.to_s, 'aria-labelledby': "list-#{feature.key}-list", class: 'card border-0') do tag.div(class: 'card-body') do tag.div(class: 'card-title h6 font-weight-bold mb-4') do feature.meta.label end + - form_for(feature, as: 'feature', url: update_feature_exchanges_configuration_path(feature.key), method: :post, remote: true, authenticity_token: true) do |form| + form_for(feature, as: 'feature', url: submit_path, method: :post, remote: true, authenticity_token: true) do |form| form.hidden_field(:key) + + (clone_action ? hidden_field_tag('feature[target_feature]', options[:action_params][:key]) : '') + render_feature(feature, form, registry) + tag.div(class: 'row mt-3') do tag.div(class: 'col-4') do @@ -212,20 +223,37 @@ def construct_feature_form(feature, registry) def feature_group_display(feature) tag.div(id: feature.key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature.key}-list") do feature.settings.collect do |setting| - feature_group_control(setting).html_safe if setting.meta && setting.meta.content_type.to_s == 'feature_group' + feature_group_control(feature, setting).html_safe if setting.meta && setting.meta.content_type.to_s == 'feature_group' end.compact.join.html_safe end.html_safe end - def feature_group_control(option) + def feature_group_control(source_feature, option) features = option.item.collect{|key| find_feature(key)} - features.collect do |feature| tag.div(class: 'mt-3') do - tag.h4 do - feature.meta.label + tag.div(class: 'row') do + tag.div(class: 'col-md-6') do + tag.h4 do + feature.meta.label + end + end + + tag.div(class: 'col-md-6') do + action_setting = feature.settings.detect{|setting| setting.meta.content_type.to_s == 'feature_action'} + if action_setting + form_with(model: feature, url: action_setting.item, method: :get, remote: true, local: false) do |f| + hidden_field_tag('feature[action]', 'clone') + + hidden_field_tag('feature[key]', feature.key) + + f.submit('Clone', class: 'btn btn-link') + end.html_safe + # tag.a(href: action_setting.item + "?", data: {remote: true}) do + # tag.span { "Clone" } + # end + end + end end + feature.settings.collect do |setting| + next if setting.meta.content_type.to_s == 'feature_action' section_name = setting.meta&.label || setting.key.to_s.titleize tag.div(class: 'mt-3') do tag.div(class: 'row') do @@ -248,7 +276,7 @@ def feature_group_control(option) end end end - end.join.html_safe + end.compact.join.html_safe end end.join.html_safe end diff --git a/lib/resource_registry/navigation.rb b/lib/resource_registry/navigation.rb index 76f6ffec..66652827 100644 --- a/lib/resource_registry/navigation.rb +++ b/lib/resource_registry/navigation.rb @@ -110,7 +110,8 @@ def content_to_expand(element) def namespace_nav_link(element) tag.a(options[:tag_options][:a][:namespace_link][:options].merge(id: 'namespace-link', href: "#nav_#{element[:key]}", data: {target: "#nav_#{element[:key]}", feature: element[:features][0]&.[](:key)})) do tag.span do - element[:namespaces] ? element[:path].last.to_s.titleize : element[:meta][:label] + link_title(element) + # element[:namespaces] ? element[:path].last.to_s.titleize : element[:meta][:label] end end end @@ -120,9 +121,18 @@ def feature_nav_link(element) feature_url ||= ('/exchanges/configurations/' + element[:key].to_s + '/edit') tag.a(options[:tag_options][:a][:feature_link][:options].merge(href: feature_url)) do tag.span do - element[:namespaces] ? element[:path].last.to_s.titleize : element[:meta][:label] + link_title(element) + # element[:namespaces] ? element[:path].last.to_s.titleize : element[:meta][:label] end end end + + def link_title(element) + if element[:meta] + element[:meta][:label] + else + element[:path].last.to_s.titleize + end + end end end diff --git a/spec/support/registry_data_seed.rb b/spec/support/registry_data_seed.rb index 44eb278c..70eb2797 100644 --- a/spec/support/registry_data_seed.rb +++ b/spec/support/registry_data_seed.rb @@ -35,6 +35,10 @@ def feature_template_path Pathname.pwd.join('spec', 'rails_app', 'system', 'config', 'templates', 'features','aca_shop_market', 'aca_shop_market.yml') end + def feature_group_template_path + Pathname.pwd.join('spec', 'rails_app', 'system', 'config', 'templates', 'features','aca_shop_market', 'feature_group.yml') + end + def option_files_dir Pathname.pwd.join('spec', 'db', 'seedfiles', 'client') end From f6f3485a9abed03b35816f22a2b68b66d4e0c07b Mon Sep 17 00:00:00 2001 From: Dan Thomas Date: Mon, 1 Feb 2021 09:06:07 -0500 Subject: [PATCH 30/40] Bump rubocop version --- Gemfile.lock | 29 +++++++++++++++++------------ resource_registry.gemspec | 2 +- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ec77ab42..7f9fb4ac 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -41,7 +41,7 @@ GEM i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - ast (2.4.0) + ast (2.4.2) bootsnap (1.4.6) msgpack (~> 1.0) bson (4.8.2) @@ -106,7 +106,6 @@ GEM i18n (1.8.2) concurrent-ruby (~> 1.0) ice_nine (0.11.2) - jaro_winkler (1.5.4) lazy_priority_queue (0.1.1) loofah (2.5.0) crass (~> 1.0.2) @@ -126,9 +125,9 @@ GEM nokogiri (1.10.9) mini_portile2 (~> 2.4.0) ox (2.13.4) - parallel (1.19.1) - parser (2.7.1.1) - ast (~> 2.4.0) + parallel (1.20.1) + parser (3.0.0.0) + ast (~> 2.4.1) pry (0.12.2) coderay (~> 1.1.0) method_source (~> 0.9.0) @@ -151,6 +150,8 @@ GEM thor (>= 0.19.0, < 2.0) rainbow (3.0.0) rake (12.3.3) + regexp_parser (2.0.3) + rexml (3.2.4) rgl (0.5.2) lazy_priority_queue (~> 0.1.0) stream (~> 0.5.0) @@ -175,14 +176,18 @@ GEM rspec-mocks (~> 3.9.0) rspec-support (~> 3.9.0) rspec-support (3.9.2) - rubocop (0.74.0) - jaro_winkler (~> 1.5.1) + rubocop (1.9.1) parallel (~> 1.10) - parser (>= 2.6) + parser (>= 3.0.0.0) rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml + rubocop-ast (>= 1.2.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 1.7) - ruby-progressbar (1.10.1) + unicode-display_width (>= 1.4.0, < 3.0) + rubocop-ast (1.4.1) + parser (>= 2.7.1.5) + ruby-progressbar (1.11.0) simplecov (0.18.5) docile (~> 1.1) simplecov-html (~> 0.11) @@ -193,7 +198,7 @@ GEM timecop (0.9.1) tzinfo (1.2.7) thread_safe (~> 0.1) - unicode-display_width (1.6.1) + unicode-display_width (2.0.0) yard (0.9.24) PLATFORMS @@ -212,7 +217,7 @@ DEPENDENCIES resource_registry! rspec (~> 3.9) rspec-rails (~> 3.9) - rubocop (~> 0.74.0) + rubocop (~> 1.9.0) simplecov timecop (~> 0.9) yard (~> 0.9) diff --git a/resource_registry.gemspec b/resource_registry.gemspec index aa114225..f84a6a8e 100644 --- a/resource_registry.gemspec +++ b/resource_registry.gemspec @@ -60,7 +60,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'rake', '~> 12.0' spec.add_development_dependency 'rspec', '~> 3.9' spec.add_development_dependency 'rspec-rails', '~> 3.9' - spec.add_development_dependency 'rubocop', '~> 0.74.0' + spec.add_development_dependency 'rubocop', '~> 1.9.0' spec.add_development_dependency 'simplecov' #, '~> 1.0' spec.add_development_dependency 'timecop', '~> 0.9' spec.add_development_dependency 'yard', '~> 0.9' From 85e0b3b704ec62fd8a402c3061bffdf6a5e28fc6 Mon Sep 17 00:00:00 2001 From: Dan Thomas Date: Mon, 1 Feb 2021 09:14:00 -0500 Subject: [PATCH 31/40] Bump nokogiri, actionview, rack, actionpack and activesupport gem versions --- Gemfile.lock | 138 +++++++++++++++++++------------------- resource_registry.gemspec | 8 +-- 2 files changed, 74 insertions(+), 72 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 7f9fb4ac..8cc2018c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,86 +13,82 @@ PATH i18n (>= 0.7.0) loofah (>= 2.3.1) mime-types - nokogiri (>= 1.9.1) + nokogiri (>= 1.11.1) ox (~> 2.0) - rack (>= 1.6.13) + rack (>= 2.2.3) rgl GEM remote: https://rubygems.org/ specs: - actionpack (5.2.4.2) - actionview (= 5.2.4.2) - activesupport (= 5.2.4.2) + actionpack (5.2.4.4) + actionview (= 5.2.4.4) + activesupport (= 5.2.4.4) rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.4.2) - activesupport (= 5.2.4.2) + actionview (5.2.4.4) + activesupport (= 5.2.4.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - activemodel (5.2.4.2) - activesupport (= 5.2.4.2) - activesupport (5.2.4.2) + activemodel (5.2.4.4) + activesupport (= 5.2.4.4) + activesupport (5.2.4.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) ast (2.4.2) - bootsnap (1.4.6) + bootsnap (1.7.0) msgpack (~> 1.0) - bson (4.8.2) + bson (4.11.1) builder (3.2.4) byebug (11.1.3) - coderay (1.1.2) - concurrent-ruby (1.1.6) + coderay (1.1.3) + concurrent-ruby (1.1.8) crass (1.0.6) - database_cleaner (1.8.4) + database_cleaner (1.99.0) deep_merge (1.2.1) - diff-lcs (1.3) - docile (1.3.2) + diff-lcs (1.4.4) + docile (1.3.5) dry-configurable (0.9.0) concurrent-ruby (~> 1.0) dry-core (~> 0.4, >= 0.4.7) dry-container (0.7.2) concurrent-ruby (~> 1.0) dry-configurable (~> 0.1, >= 0.1.3) - dry-core (0.4.10) + dry-core (0.5.0) concurrent-ruby (~> 1.0) dry-equalizer (0.3.0) dry-inflector (0.2.0) dry-initializer (3.0.4) - dry-logic (1.0.8) + dry-logic (1.1.0) concurrent-ruby (~> 1.0) - dry-core (~> 0.2) - dry-equalizer (~> 0.2) + dry-core (~> 0.5, >= 0.5) dry-matcher (0.8.3) dry-core (>= 0.4.8) dry-monads (1.3.5) concurrent-ruby (~> 1.0) dry-core (~> 0.4, >= 0.4.4) dry-equalizer - dry-schema (1.5.6) + dry-schema (1.6.0) concurrent-ruby (~> 1.0) dry-configurable (~> 0.8, >= 0.8.3) - dry-core (~> 0.4) - dry-equalizer (~> 0.2) + dry-core (~> 0.5, >= 0.5) dry-initializer (~> 3.0) dry-logic (~> 1.0) - dry-types (~> 1.4) - dry-struct (1.3.0) - dry-core (~> 0.4, >= 0.4.4) - dry-equalizer (~> 0.3) - dry-types (~> 1.3) + dry-types (~> 1.5) + dry-struct (1.4.0) + dry-core (~> 0.5, >= 0.5) + dry-types (~> 1.5) ice_nine (~> 0.11) - dry-types (1.4.0) + dry-types (1.5.0) concurrent-ruby (~> 1.0) dry-container (~> 0.3) - dry-core (~> 0.4, >= 0.4.4) - dry-equalizer (~> 0.3) + dry-core (~> 0.5, >= 0.5) dry-inflector (~> 0.1, >= 0.1.2) dry-logic (~> 1.0, >= 1.0.2) dry-validation (1.6.0) @@ -102,39 +98,42 @@ GEM dry-equalizer (~> 0.2) dry-initializer (~> 3.0) dry-schema (~> 1.5, >= 1.5.2) - erubi (1.9.0) - i18n (1.8.2) + erubi (1.10.0) + generator (0.0.1) + i18n (1.8.7) concurrent-ruby (~> 1.0) ice_nine (0.11.2) lazy_priority_queue (0.1.1) - loofah (2.5.0) + loofah (2.9.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) - method_source (0.9.2) + method_source (1.0.0) mime-types (3.3.1) mime-types-data (~> 3.2015) mime-types-data (3.2020.1104) - mini_portile2 (2.4.0) - minitest (5.14.0) - mongo (2.12.1) + mini_portile2 (2.5.0) + minitest (5.14.3) + mongo (2.14.0) bson (>= 4.8.2, < 5.0.0) - mongoid (6.4.4) + mongoid (6.4.8) activemodel (>= 5.1, < 6.0.0) mongo (>= 2.5.1, < 3.0.0) - msgpack (1.3.3) - nokogiri (1.10.9) - mini_portile2 (~> 2.4.0) - ox (2.13.4) + msgpack (1.4.2) + nokogiri (1.11.1) + mini_portile2 (~> 2.5.0) + racc (~> 1.4) + ox (2.14.1) parallel (1.20.1) parser (3.0.0.0) ast (~> 2.4.1) - pry (0.12.2) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - pry-byebug (3.8.0) + pry (0.13.1) + coderay (~> 1.1) + method_source (~> 1.0) + pry-byebug (3.9.0) byebug (~> 11.0) - pry (~> 0.10) - rack (2.2.2) + pry (~> 0.13.0) + racc (1.5.2) + rack (2.2.3) rack-test (1.1.0) rack (>= 1.0, < 3) rails-dom-testing (2.0.3) @@ -142,9 +141,9 @@ GEM nokogiri (>= 1.6) rails-html-sanitizer (1.3.0) loofah (~> 2.3) - railties (5.2.4.2) - actionpack (= 5.2.4.2) - activesupport (= 5.2.4.2) + railties (5.2.4.4) + actionpack (= 5.2.4.4) + activesupport (= 5.2.4.4) method_source rake (>= 0.8.7) thor (>= 0.19.0, < 2.0) @@ -152,16 +151,16 @@ GEM rake (12.3.3) regexp_parser (2.0.3) rexml (3.2.4) - rgl (0.5.2) + rgl (0.5.7) lazy_priority_queue (~> 0.1.0) - stream (~> 0.5.0) + stream (~> 0.5.3) rspec (3.9.0) rspec-core (~> 3.9.0) rspec-expectations (~> 3.9.0) rspec-mocks (~> 3.9.0) - rspec-core (3.9.1) - rspec-support (~> 3.9.1) - rspec-expectations (3.9.1) + rspec-core (3.9.3) + rspec-support (~> 3.9.3) + rspec-expectations (3.9.4) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.9.0) rspec-mocks (3.9.1) @@ -175,7 +174,7 @@ GEM rspec-expectations (~> 3.9.0) rspec-mocks (~> 3.9.0) rspec-support (~> 3.9.0) - rspec-support (3.9.2) + rspec-support (3.9.4) rubocop (1.9.1) parallel (~> 1.10) parser (>= 3.0.0.0) @@ -188,25 +187,28 @@ GEM rubocop-ast (1.4.1) parser (>= 2.7.1.5) ruby-progressbar (1.11.0) - simplecov (0.18.5) + simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) - simplecov-html (0.12.2) - stream (0.5) - thor (1.0.1) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.12.3) + simplecov_json_formatter (0.1.2) + stream (0.5.3) + generator + thor (1.1.0) thread_safe (0.3.6) - timecop (0.9.1) - tzinfo (1.2.7) + timecop (0.9.2) + tzinfo (1.2.9) thread_safe (~> 0.1) unicode-display_width (2.0.0) - yard (0.9.24) + yard (0.9.26) PLATFORMS ruby DEPENDENCIES - actionview (>= 5.2.4.2) - activesupport (~> 5.2.4) + actionview (>= 5.2.4.3) + activesupport (~> 5.2.4.3) bootsnap (~> 1.0) bundler (~> 2.0) database_cleaner (~> 1.7) diff --git a/resource_registry.gemspec b/resource_registry.gemspec index f84a6a8e..6d91bbae 100644 --- a/resource_registry.gemspec +++ b/resource_registry.gemspec @@ -45,14 +45,14 @@ Gem::Specification.new do |spec| spec.add_dependency 'i18n', '>= 0.7.0' spec.add_dependency 'ox', '~> 2.0' spec.add_dependency 'loofah', '>= 2.3.1' - spec.add_dependency 'nokogiri', '>= 1.9.1' - spec.add_dependency 'rack', '>= 1.6.13' + spec.add_dependency 'nokogiri', '>= 1.11.1' + spec.add_dependency 'rack', '>= 2.2.3' spec.add_dependency 'mime-types' spec.add_dependency 'rgl'#, '~> 0.5.6' - spec.add_development_dependency "actionview", '>= 5.2.4.2' - spec.add_development_dependency 'activesupport', '~> 5.2.4' + spec.add_development_dependency "actionview", '>= 5.2.4.3' + spec.add_development_dependency 'activesupport', '~> 5.2.4.3' spec.add_development_dependency 'bootsnap', '~> 1.0' spec.add_development_dependency 'bundler', '~> 2.0' spec.add_development_dependency 'database_cleaner', '~> 1.7' From af84a1fa3b6b896f043affa245817fcf3d94562c Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Tue, 2 Feb 2021 10:57:51 -0500 Subject: [PATCH 32/40] Updated RR helpers, operations with following changes 1)Updated view controls for resource registry to support horizontal/vertical forms 2)Updated meta -> enum to support constants 3)Renamed clone to renew --- lib/resource_registry/feature.rb | 2 +- .../helpers/input_controls.rb | 39 +++- .../helpers/view_controls.rb | 180 ++++++------------ lib/resource_registry/meta.rb | 2 +- lib/resource_registry/models/mongoid/meta.rb | 14 +- lib/resource_registry/namespace.rb | 1 + .../features/{clone.rb => renew.rb} | 6 +- .../operations/namespaces/list_features.rb | 42 ++++ .../operations/namespaces/update_features.rb | 25 +-- lib/resource_registry/registry.rb | 7 +- .../validation/meta_contract.rb | 2 +- .../features/{clone_spec.rb => renew_spec.rb} | 2 +- .../namespaces/list_features_spec.rb | 31 +++ 13 files changed, 189 insertions(+), 164 deletions(-) rename lib/resource_registry/operations/features/{clone.rb => renew.rb} (93%) create mode 100644 lib/resource_registry/operations/namespaces/list_features.rb rename spec/resource_registry/operations/features/{clone_spec.rb => renew_spec.rb} (92%) create mode 100644 spec/resource_registry/operations/namespaces/list_features_spec.rb diff --git a/lib/resource_registry/feature.rb b/lib/resource_registry/feature.rb index 2d87d5bb..72d846fa 100644 --- a/lib/resource_registry/feature.rb +++ b/lib/resource_registry/feature.rb @@ -3,7 +3,7 @@ require_relative 'validation/feature_contract' require_relative 'operations/features/create' -require_relative 'operations/features/clone' +require_relative 'operations/features/renew' require_relative 'operations/features/configure' require_relative 'operations/features/disable' require_relative 'operations/features/enable' diff --git a/lib/resource_registry/helpers/input_controls.rb b/lib/resource_registry/helpers/input_controls.rb index 2d0a58a2..00d1f064 100644 --- a/lib/resource_registry/helpers/input_controls.rb +++ b/lib/resource_registry/helpers/input_controls.rb @@ -43,7 +43,28 @@ def build_option_field(option, form, attrs = {}) custom_form_group(option, input_control) else return input_control if [:feature_enabled, :slider_switch].include?(type) - form_group(option, input_control) + form_group(option, input_control, attrs) + end + end + + def input_filter_control(form, feature, data) + filter_setting = feature.settings.detect{|s| s.key == :filter_params} + filter_params = setting_value(filter_setting) + + option_list = tag.option(filter_setting.meta.label) + data[:calender_years].collect do |choice| + option_list += tag.option(choice, selected: (choice == data[:catalog_year]), value: choice) + end + + tag.div(class: 'row mb-4') do + tag.div(class: 'col-4') do + content = filter_params['criteria'].reduce([]) do |strs, (attr, value)| + strs << tag.input(type: 'hidden', id: "filter_#{attr}", name: "filter[#{attr}]", value: value) + end.join + content += tag.input(type: 'hidden', id: :target_feature, name: :target_feature, value: feature.key.to_s) + content += tag.select(option_list, id: filter_params['name'], class: "form-control feature-filter", name: "filter[#{filter_params['name']}]") + content.html_safe + end end end @@ -88,9 +109,13 @@ def select_control(setting, form) value = value_for(setting, form) || setting.item || meta&.default option_list = tag.option(selected_option, selected: (value.blank? ? true : false)) - meta.enum.each do |choice| - choice = choice.first if choice.is_a?(Hash) - option_list += tag.option(choice[1].to_s, selected: (choice[0].to_s == value.to_s), value: choice[0].to_s) + + choices = meta.enum + choices = meta.enum.constantize if choices.is_a?(String) + + choices.each do |choice| + choice = choice.is_a?(Hash) ? choice.first : [choice.to_s, choice.to_s.humanize] + option_list += tag.option(choice[1], selected: (choice[0]== value.to_s), value: choice[0]) end tag.select(option_list, id: id, class: "form-control", name: input_name_for(setting, form)) @@ -355,7 +380,7 @@ def input_group end # Build a general-purpose form group wrapper around the supplied input control - def form_group(setting, control, options = {horizontal: true, tooltip: true}) + def form_group(setting, control, options = {}) id = setting[:key].to_s # label = setting[:title] || id.titleize label = setting.meta.label || id.titleize @@ -374,7 +399,7 @@ def form_group(setting, control, options = {horizontal: true, tooltip: true}) end end + tag.div(class: 'col col-sm-12 col-md-1') do - tag.i(class: 'fas fa-info-circle', rel: 'tooltip', title: setting.meta.description) if options[:tooltip] + tag.i(class: 'fas fa-info-circle', rel: 'tooltip', title: setting.meta.description) end + tag.div(class: 'col col-sm-12 col-md-7') do input_group { control }# + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) @@ -418,7 +443,7 @@ def build_attribute_field(form, attribute) end def setting_value(setting) - if setting && setting.is_a?(ResourceRegistry::Setting) + if setting.is_a?(ResourceRegistry::Setting) && setting.item&.is_a?(String) JSON.parse(setting.item) else setting&.item diff --git a/lib/resource_registry/helpers/view_controls.rb b/lib/resource_registry/helpers/view_controls.rb index c97419c6..97b26b3c 100644 --- a/lib/resource_registry/helpers/view_controls.rb +++ b/lib/resource_registry/helpers/view_controls.rb @@ -5,8 +5,8 @@ module RegistryViewControls include ::InputControls - def render_feature(feature, form, registry = nil) - return render_model_feature(feature, form, registry) if feature.meta.content_type == :model_attributes + def render_settings(feature, form, registry, options) + return render_model_settings(feature, form, registry, options) if feature.meta.content_type == :model_attributes feature = feature.feature if feature.is_a?(ResourceRegistry::FeatureDSL) content = form.hidden_field(:is_enabled) @@ -14,14 +14,14 @@ def render_feature(feature, form, registry = nil) if ['legend'].include?(feature.meta.content_type.to_s) content += form.hidden_field(:namespace, value: feature.namespace_path.path.map(&:to_s).join('.')) elsif feature.meta.content_type == :feature_enabled - content += build_option_field(feature, form) + content += build_option_field(feature, form, options) end - content += feature.settings.collect{|setting| build_option_field(setting, form).html_safe if setting.meta}.compact.join.html_safe + content += feature.settings.collect{|setting| build_option_field(setting, form, options).html_safe if setting.meta}.compact.join.html_safe content.html_safe end - def render_model_feature(feature, form, registry) + def render_model_settings(feature, form, registry, options) query_setting = feature.settings.detect{|setting| setting.key == :model_query_params} query_params = setting_value(query_setting) @@ -30,115 +30,33 @@ def render_model_feature(feature, form, registry) filter_setting = feature.settings.detect{|s| s.key == :filter_params} content = '' - content = render_filter(form, feature, result).html_safe if filter_setting + content = input_filter_control(form, feature, result).html_safe if filter_setting content += form.hidden_field(:is_enabled) content += form.hidden_field(:namespace, value: feature.namespace_path.path.map(&:to_s).join('.')) - feature.settings.each do |setting| - next if setting.meta.blank? || setting.key == :filter_params - content += build_option_field(setting, form, {record: result[:record]}) + if result[:record] + feature.settings.each do |setting| + next if setting.meta.blank? || setting.key == :filter_params + content += build_option_field(setting, form, options.merge(record: result[:record])) + end end content.html_safe end - def render_filter(form, feature, data) - filter_setting = feature.settings.detect{|s| s.key == :filter_params} - filter_params = setting_value(filter_setting) - - option_list = tag.option(filter_setting.meta.label) - data[:calender_years].collect do |choice| - option_list += tag.option(choice, selected: (choice == data[:catalog_year]), value: choice) - end - - tag.div(class: 'row mb-4') do - tag.div(class: 'col-4') do - content = filter_params['criteria'].reduce([]) do |strs, (attr, value)| - strs << tag.input(type: 'hidden', id: "filter_#{attr}", name: "filter[#{attr}]", value: value) - end.join - content += tag.input(type: 'hidden', id: :target_feature, name: :target_feature, value: feature.key.to_s) - content += tag.select(option_list, id: filter_params['name'], class: "form-control feature-filter", name: "filter[#{filter_params['name']}]") - content.html_safe - end - end - end - - # def list_group_menu(nested_namespaces = nil, features = nil, options = {}) - # content = '' - # tag.div({class: "list-group", id: "list-tab", role: "tablist"}.merge(options)) do - - # if features - # features.each do |feature| - # feature_rec = ResourceRegistry::ActiveRecord::Feature.where(key: feature).first - - # content += tag.a(href: "##{feature}", class: "list-group-item list-group-item-action border-0", 'data-toggle': 'list', role: 'tab', id: "list-#{feature}-list", 'aria-controls': feature.to_s) do - # feature_rec&.setting(:label)&.item || feature.to_s.titleize - # end.html_safe - # end - # end - - # if nested_namespaces - # nested_namespaces.each do |namespace, children| - - # content += tag.a(href: "##{namespace}-group", class: "list-group-item list-group-item-action border-0", 'data-toggle': 'collapse', role: 'tab', id: "list-#{namespace}-list", 'aria-controls': namespace.to_s) do - # "+ #{namespace.to_s.titleize}" - # end - - # content += tag.span('data-toggle': 'list') do - # list_group_menu(children[:namespaces], children[:features], {class: "list-group collapse ml-4", id: "#{namespace}-group"}) - # end - # end - # end - - # content.html_safe - # end - # end - - # def list_tab_panels(features, feature_registry, _options = {}) - # tag.div(class: 'card') do - # # content = tag.div(class: 'card-header') do - # # tag.h4(feature.setting(:label)&.item || feature.key.to_s.titleize) - # # end - - # content = tag.div(class: 'card-body') do - # feature_content = '' - # features.each do |feature_key| - # feature = defined?(Rails) ? find_feature(feature_key) : feature_registry[feature_key].feature - # next if feature.blank? - - # feature_content += tag.div(id: feature_key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature_key}-list") do - # form_for(feature, as: 'feature', url: exchanges_configuration_path(feature), method: :patch, remote: true, authenticity_token: true) do |form| - # form.hidden_field(:key) + - # render_feature(feature, form) + - # tag.div(class: 'row mt-3') do - # tag.div(class: 'col-4') do - # form.submit(class: 'btn btn-primary') - # end + - # tag.div(class: 'col-6') do - # tag.div(class: 'flash-message', id: feature_key.to_s + '-alert') - # end - # end - # end - # end - # end - # feature_content.html_safe - # end.html_safe - # end - # end - - def namespace_panel(namespace, feature_registry, _options = {}) + def namespace_panel(namespace, feature_registry, options = {}) tag.div(class: 'card') do tag.div(class: 'card-body') do if namespace.features.any?{|f| f.meta.content_type == :model_attributes} - namespace.features.collect{|feature| construct_feature_form(feature, feature_registry)}.join(tag.hr(class: 'mt-2 mb-3')).html_safe + namespace.features.collect{|feature| construct_feature_form(feature, feature_registry, options)}.join(tag.hr(class: 'mt-2 mb-3')).html_safe else - construct_namespace_form(namespace, feature_registry) + construct_namespace_form(namespace, feature_registry, options) end end.html_safe end end - def construct_namespace_form(namespace, registry) + def construct_namespace_form(namespace, registry, options) form_for(namespace, as: 'namespace', url: update_namespace_exchanges_configurations_path, method: :post, remote: true, authenticity_token: true) do |form| namespace_content = form.hidden_field(:path, value: namespace.path.map(&:to_s).join('.')) @@ -146,7 +64,7 @@ def construct_namespace_form(namespace, registry) namespace_content += form.fields_for :features, feature, {index: index} do |feature_form| tag.div(id: feature.key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature.key}-list", class: 'mt-2') do feature_form.hidden_field(:key) + - render_feature(feature, feature_form, feature_registry) + render_settings(feature, feature_form, feature_registry, options) end end end @@ -166,19 +84,27 @@ def construct_namespace_form(namespace, registry) def feature_panel(feature_key, registry, options = {}) @filter_result = options[:filter_result] + @horizontal = true if options[:horizontal] tag.div(class: 'card') do tag.div(class: 'card-body') do - feature = get_feature(feature_key, registry) + feature = get_feature(feature_key, registry) if feature.present? features = [feature] if feature.item == 'features_display' - feature_group_display(feature) + feature_group_display(feature, registry) else if feature.item == 'feature_collection' - features_setting = feature.settings.detect{|setting| setting.meta&.content_type.to_s == 'feature_list_panel'} - feature_keys = features_setting.item - features = feature_keys.collect{|key| get_feature(key, registry)}.compact + setting = feature.settings.detect{|setting| setting.meta&.content_type.to_s == 'feature_list_panel'} + + feature_list = if setting.item.is_a?(Hash) && setting.item['operation'] + elements = setting.item['operation'].split(/\./) + elements[0].constantize.send(elements[1]).call(setting.item['params'].symbolize_keys).success + else + setting.item + end + + features = feature_list.collect{|key| get_feature(key, registry)}.compact end features.collect{|feature| construct_feature_form(feature, registry, options)}.join(tag.hr(class: 'mt-2 mb-3')).html_safe end @@ -187,13 +113,13 @@ def feature_panel(feature_key, registry, options = {}) end end - def construct_feature_form(feature, registry, options = {}) + def construct_feature_form(feature, registry, options) if options[:action_params] - clone_action = options[:action_params][:action].to_s == 'clone' + renew_action = options[:action_params][:action].to_s == 'renew' end - submit_path = if clone_action - clone_feature_exchanges_configuration_path(feature.key) + submit_path = if renew_action + renew_feature_exchanges_configuration_path(feature.key) else update_feature_exchanges_configuration_path(feature.key) end @@ -201,12 +127,12 @@ def construct_feature_form(feature, registry, options = {}) tag.div(id: feature.key.to_s, 'aria-labelledby': "list-#{feature.key}-list", class: 'card border-0') do tag.div(class: 'card-body') do tag.div(class: 'card-title h6 font-weight-bold mb-4') do - feature.meta.label + feature.meta&.label || feature.key.to_s.titleize end + form_for(feature, as: 'feature', url: submit_path, method: :post, remote: true, authenticity_token: true) do |form| form.hidden_field(:key) + - (clone_action ? hidden_field_tag('feature[target_feature]', options[:action_params][:key]) : '') + - render_feature(feature, form, registry) + + (renew_action ? hidden_field_tag('feature[target_feature]', options[:action_params][:key]) : '') + + render_settings(feature, form, registry, options) + tag.div(class: 'row mt-3') do tag.div(class: 'col-4') do form.submit('Save', class: 'btn btn-primary') @@ -220,35 +146,36 @@ def construct_feature_form(feature, registry, options = {}) end end - def feature_group_display(feature) + def feature_group_display(feature, registry) tag.div(id: feature.key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature.key}-list") do feature.settings.collect do |setting| - feature_group_control(feature, setting).html_safe if setting.meta && setting.meta.content_type.to_s == 'feature_group' + if setting.meta&.content_type.to_s == 'feature_group' + features = registry.features_by_namespace(setting.item).collect{|key| find_feature(key)} + feature_group_control(features, registry).html_safe + end end.compact.join.html_safe - end.html_safe + end end - def feature_group_control(source_feature, option) - features = option.item.collect{|key| find_feature(key)} + def feature_group_control(features, registry) + features = features.select{|feature| feature.meta.present? && feature.meta.content_type.to_s != 'feature_action' } + features.collect do |feature| tag.div(class: 'mt-3') do tag.div(class: 'row') do tag.div(class: 'col-md-6') do tag.h4 do - feature.meta.label + feature.meta&.label || feature.key.to_s.titleize end end + tag.div(class: 'col-md-6') do action_setting = feature.settings.detect{|setting| setting.meta.content_type.to_s == 'feature_action'} if action_setting form_with(model: feature, url: action_setting.item, method: :get, remote: true, local: false) do |f| - hidden_field_tag('feature[action]', 'clone') + + hidden_field_tag('feature[action]', 'renew') + hidden_field_tag('feature[key]', feature.key) + - f.submit('Clone', class: 'btn btn-link') + f.submit(action_setting.key.to_s.titleize, class: 'btn btn-link') end.html_safe - # tag.a(href: action_setting.item + "?", data: {remote: true}) do - # tag.span { "Clone" } - # end end end end + @@ -271,14 +198,21 @@ def feature_group_control(source_feature, option) end + tag.div(class: 'col-md-6') do tag.ul(class: 'list-group list-group-flush ml-2') do - setting.item.collect{|value| tag.li(class: 'list-group-item'){ value.to_s.titleize }}.join.html_safe + feature_list = if setting.item.is_a?(Hash) && setting.item['operation'] + elements = setting.item['operation'].split(/\./) + elements[0].constantize.send(elements[1]).call(setting.item['params'].symbolize_keys).success + else + setting.item + end + + feature_list.collect{|value| tag.li(class: 'list-group-item'){ value.to_s.titleize }}.join.html_safe end end end end end.compact.join.html_safe end - end.join.html_safe + end.join end def get_feature(feature_key, registry) diff --git a/lib/resource_registry/meta.rb b/lib/resource_registry/meta.rb index 297bcf60..dd6da2ae 100644 --- a/lib/resource_registry/meta.rb +++ b/lib/resource_registry/meta.rb @@ -37,7 +37,7 @@ class Meta < Dry::Struct # @!attribute [r] enum # List of vaalid domain values when configuration values are constrained to an enumerated set # @return [Array] - attribute :enum, Types::Array.of(Types::Any).optional.meta(omittable: true) + attribute :enum, Types::Any.optional.meta(omittable: true) # @!attribute [r] is_required # Internal indicator whether the configuration setting value must be set in the UI diff --git a/lib/resource_registry/models/mongoid/meta.rb b/lib/resource_registry/models/mongoid/meta.rb index d6f87b83..5d92b16d 100644 --- a/lib/resource_registry/models/mongoid/meta.rb +++ b/lib/resource_registry/models/mongoid/meta.rb @@ -11,7 +11,7 @@ class Meta field :default, type: String field :value, type: String field :description, type: String - field :enum, type: Array + field :enum, type: String field :is_required, type: Boolean field :is_visible, type: Boolean @@ -24,6 +24,16 @@ def value_hash def value=(val) super(val.to_json) end + + def enum=(val) + super(val.to_json) + end + + def enum + JSON.parse(super) if super.present? + rescue JSON::ParserError + super + end end end -end +end \ No newline at end of file diff --git a/lib/resource_registry/namespace.rb b/lib/resource_registry/namespace.rb index dfe27e1c..a4daa4a2 100644 --- a/lib/resource_registry/namespace.rb +++ b/lib/resource_registry/namespace.rb @@ -5,6 +5,7 @@ require_relative 'operations/namespaces/create' require_relative 'operations/namespaces/form' require_relative 'operations/namespaces/update_features' +require_relative 'operations/namespaces/list_features' require_relative 'operations/graphs/create' module ResourceRegistry diff --git a/lib/resource_registry/operations/features/clone.rb b/lib/resource_registry/operations/features/renew.rb similarity index 93% rename from lib/resource_registry/operations/features/clone.rb rename to lib/resource_registry/operations/features/renew.rb index af659220..cadb8e06 100644 --- a/lib/resource_registry/operations/features/clone.rb +++ b/lib/resource_registry/operations/features/renew.rb @@ -4,7 +4,7 @@ module ResourceRegistry module Operations module Features # Create a Feature - class Clone + class Renew send(:include, Dry::Monads[:result, :do, :try]) attr_reader :original_year, :new_calender_year @@ -87,6 +87,10 @@ def serialize_text(value) if value.is_a?(Symbol) value.to_s.gsub(original_year, new_calender_year).to_sym + elsif value.is_a?(Range) && value.min.is_a?(Date) + Range.new(value.min.next_year, value.max.next_year) + elsif value.is_a?(Date) + value.next_year elsif value.is_a?(String) value.gsub(original_year, new_calender_year) else diff --git a/lib/resource_registry/operations/namespaces/list_features.rb b/lib/resource_registry/operations/namespaces/list_features.rb new file mode 100644 index 00000000..156c02c1 --- /dev/null +++ b/lib/resource_registry/operations/namespaces/list_features.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# params: namespace +# returns array of feature dsl objects + +module ResourceRegistry + module Operations + module Namespaces + class ListFeatures + send(:include, Dry::Monads[:result, :do, :try]) + + def call(params) + values = yield validate(params) + features = yield list_features(values) + + Success(features) + end + + private + + def validate(params) + return Failure("Namespace parameter missing.") unless params[:namespace] + + registry = params[:registry] + registry = registry.constantize if registry.is_a?(String) + return Failure("Unable to find namespace #{params[:namespace]} under #{params[:registry]}.") unless registry.namespaces.include?(params[:namespace]) + + Success({ + namespace: params[:namespace], + registry: registry + }) + end + + def list_features(values) + features = values[:registry].features_by_namespace(values[:namespace]) + + Success(features) + end + end + end + end +end \ No newline at end of file diff --git a/lib/resource_registry/operations/namespaces/update_features.rb b/lib/resource_registry/operations/namespaces/update_features.rb index 057cce73..78cd5d0d 100644 --- a/lib/resource_registry/operations/namespaces/update_features.rb +++ b/lib/resource_registry/operations/namespaces/update_features.rb @@ -23,34 +23,11 @@ def extract(params) def update(feature_params, registry) feature_params.each do |params| - feature = registry[params[:key]] - update_model_attributes(params, registry) if feature.meta&.content_type == :model_attributes - ResourceRegistry::Operations::Features::Update.new.call(feature: feature_params.first, registry: registry) + ResourceRegistry::Operations::Features::Update.new.call(feature: params, registry: registry) end Success(registry) end - - def update_model_attributes(params, registry) - feature = registry[params[:key]] - item = feature.item - - model_klass = item['class_name'].constantize - - record = if item['scope'].present? - model_klass.send(item['scope']['name'], *item['scope']['arguments']) - elsif item['where'].present? - criteria = item['where']['arguments'] - model_klass.where(criteria) - end - - if record - record.assign_attributes(params[:settings].values.reduce({}, :merge)) - record.save(validate: false) - end - rescue NameError => e - Failure("Exception #{e} occured while updating feature #{params[:key]} ") - end end end end diff --git a/lib/resource_registry/registry.rb b/lib/resource_registry/registry.rb index 3fb8a06b..4595ebb7 100644 --- a/lib/resource_registry/registry.rb +++ b/lib/resource_registry/registry.rb @@ -36,8 +36,9 @@ def navigation(options = {}) end def swap_feature(feature) + feature = dsl_for(feature) self._container.delete("feature_index.#{feature.key}") - self._container.delete(namespaced(feature.key, feature.namespace_path.path)) + self._container.delete(namespaced(feature.key, feature.namespace)) register_feature(feature) @features_stale = false end @@ -50,7 +51,7 @@ def swap_feature(feature) def register_feature(feature) raise ArgumentError, "#{feature} must be a ResourceRegistry::Feature or ResourceRegistry::FeatureDSL" if !feature.is_a?(ResourceRegistry::Feature) && !feature.is_a?(ResourceRegistry::FeatureDSL) - feature = dsl_for(feature) + feature = dsl_for(feature) unless feature.is_a?(ResourceRegistry::FeatureDSL) raise ResourceRegistry::Error::DuplicateFeatureError, "feature already registered #{feature.key.inspect}" if feature?(feature.key) @features_stale = true @@ -114,7 +115,7 @@ def features def namespaces return @namespaces if defined? @namespaces - @namespaces = features.collect{|feature_key| self[feature_key].feature.namespace}.uniq + @namespaces = features.collect{|feature_key| self[feature_key].namespace}.uniq end def namespace_features_hash diff --git a/lib/resource_registry/validation/meta_contract.rb b/lib/resource_registry/validation/meta_contract.rb index 2d7e35ee..cedec31d 100644 --- a/lib/resource_registry/validation/meta_contract.rb +++ b/lib/resource_registry/validation/meta_contract.rb @@ -23,7 +23,7 @@ class MetaContract < ResourceRegistry::Validation::ApplicationContract optional(:default).maybe(:any) optional(:value).maybe(:any) optional(:description).maybe(:string) - optional(:enum).maybe(:array) + optional(:enum).maybe(:any) optional(:is_required).maybe(:bool) optional(:is_visible).maybe(:bool) end diff --git a/spec/resource_registry/operations/features/clone_spec.rb b/spec/resource_registry/operations/features/renew_spec.rb similarity index 92% rename from spec/resource_registry/operations/features/clone_spec.rb rename to spec/resource_registry/operations/features/renew_spec.rb index 1b6856c8..025ccbbc 100644 --- a/spec/resource_registry/operations/features/clone_spec.rb +++ b/spec/resource_registry/operations/features/renew_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ResourceRegistry::Operations::Features::Clone do +RSpec.describe ResourceRegistry::Operations::Features::Renew do include RegistryDataSeed subject { described_class.new.call(params) } diff --git a/spec/resource_registry/operations/namespaces/list_features_spec.rb b/spec/resource_registry/operations/namespaces/list_features_spec.rb new file mode 100644 index 00000000..fceabf2f --- /dev/null +++ b/spec/resource_registry/operations/namespaces/list_features_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ResourceRegistry::Operations::Namespaces::ListFeatures do + include RegistryDataSeed + + subject { described_class.new.call(params) } + + context 'When valid feature hash passed' do + + let!(:registry) { ResourceRegistry::Registry.new } + let(:register) { ResourceRegistry::Operations::Registries::Create.new.call(path: feature_group_template_path, registry: registry) } + + let!(:test_registry) do + register + ::ResourceRegistry::TestRegistry = registry + end + + let(:namespace) {"enroll_app.aca_shop_market.benefit_market_catalog.catalog_2021.contribution_model_criteria"} + + let(:params) { + {namespace: namespace, registry: 'ResourceRegistry::TestRegistry'} + } + + it "should return success with hash output" do + expect(subject).to be_a Dry::Monads::Result::Success + expect(subject.success).to eq registry.features_by_namespace(namespace) + end + end +end \ No newline at end of file From 193128ec9529a8511a1f7773c506c557d45349b0 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Thu, 4 Feb 2021 11:04:35 -0500 Subject: [PATCH 33/40] Added date validations --- .../operations/features/renew.rb | 8 +++++- .../operations/features/update.rb | 27 ++++++++++++++++--- lib/resource_registry/registry.rb | 1 - 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/lib/resource_registry/operations/features/renew.rb b/lib/resource_registry/operations/features/renew.rb index cadb8e06..1b360b14 100644 --- a/lib/resource_registry/operations/features/renew.rb +++ b/lib/resource_registry/operations/features/renew.rb @@ -23,7 +23,7 @@ def call(params:, registry:) def extract_options(params, registry) feature_for_clone = registry[params[:target_feature]] related_features = feature_for_clone.feature.settings.collect do |setting| - setting.item if setting.meta && setting.meta.content_type == :feature_list_panel + get_features(setting.item) if setting.meta && setting.meta.content_type == :feature_list_panel end.compact.flatten features = [] @@ -38,6 +38,12 @@ def extract_options(params, registry) Success(options) end + def get_features(item) + return item unless item['operation'].present? + elements = item['operation'].split(/\./) + elements[0].constantize.send(elements[1]).call(item['params'].symbolize_keys).success + end + def construct_params(options) @original_year = options[:features][0].key.to_s.scan(/\d{4}/)[0] @new_calender_year = options[:calender_year] diff --git a/lib/resource_registry/operations/features/update.rb b/lib/resource_registry/operations/features/update.rb index 12072455..568b23c0 100644 --- a/lib/resource_registry/operations/features/update.rb +++ b/lib/resource_registry/operations/features/update.rb @@ -9,7 +9,8 @@ class Update def call(params) feature_params = yield build_params(params[:feature].to_h, params[:registry]) - entity = yield create_entity(feature_params) + feature_values = yield validate(feature_params, params[:registry]) + entity = yield create_entity(feature_values) feature = yield save_record(entity, params[:registry], params[:filter]) if defined? Rails updated_feature = yield update_registry(entity, params[:registry]) @@ -43,8 +44,28 @@ def feature_toggle_params(params, registry) Success(feature_params) end - def create_entity(params) - ResourceRegistry::Operations::Features::Create.new.call(params) + def validate(feature_params, registry) + date_range_settings = registry[feature_params[:key]].settings.select{|s| s.meta && s.meta.content_type == :date_range} + + if date_range_settings.present? + feature_params[:settings].each do |setting_hash| + if date_range_setting = date_range_settings.detect{|s| s.key == setting_hash[:key].to_sym} + expected_year = date_range_setting.item.min.year + + date_begin = Date.strptime(setting_hash[:item][:begin], "%Y-%m-%d") + date_end = Date.strptime(setting_hash[:item][:end], "%Y-%m-%d") + + return Failure("#{setting_hash[:key].to_s.humanize} should be with in calender year.") unless (date_begin.year == expected_year && date_end.year == expected_year) + return Failure("#{setting_hash[:key].to_s.humanize} invalid date range selected.") unless date_end > date_begin + end + end + end + + Success(feature_params) + end + + def create_entity(feature_values) + ResourceRegistry::Operations::Features::Create.new.call(feature_values) end def save_record(entity, registry, filter_params = nil) diff --git a/lib/resource_registry/registry.rb b/lib/resource_registry/registry.rb index 4595ebb7..95691ead 100644 --- a/lib/resource_registry/registry.rb +++ b/lib/resource_registry/registry.rb @@ -114,7 +114,6 @@ def features end def namespaces - return @namespaces if defined? @namespaces @namespaces = features.collect{|feature_key| self[feature_key].namespace}.uniq end From f52ec1907eb3e49aff0ad4d41e100499a0f7a8bc Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Tue, 9 Feb 2021 14:58:04 -0500 Subject: [PATCH 34/40] Fixed renew, list_features operations --- .../helpers/view_controls.rb | 44 ++++++++++--------- .../operations/features/renew.rb | 20 +++++---- .../operations/namespaces/list_features.rb | 28 +++++++++--- 3 files changed, 58 insertions(+), 34 deletions(-) diff --git a/lib/resource_registry/helpers/view_controls.rb b/lib/resource_registry/helpers/view_controls.rb index 97b26b3c..d5e90176 100644 --- a/lib/resource_registry/helpers/view_controls.rb +++ b/lib/resource_registry/helpers/view_controls.rb @@ -24,7 +24,6 @@ def render_settings(feature, form, registry, options) def render_model_settings(feature, form, registry, options) query_setting = feature.settings.detect{|setting| setting.key == :model_query_params} query_params = setting_value(query_setting) - result = @filter_result result = registry[feature.key]{ query_params || {}}.success unless result filter_setting = feature.settings.detect{|s| s.key == :filter_params} @@ -96,15 +95,7 @@ def feature_panel(feature_key, registry, options = {}) else if feature.item == 'feature_collection' setting = feature.settings.detect{|setting| setting.meta&.content_type.to_s == 'feature_list_panel'} - - feature_list = if setting.item.is_a?(Hash) && setting.item['operation'] - elements = setting.item['operation'].split(/\./) - elements[0].constantize.send(elements[1]).call(setting.item['params'].symbolize_keys).success - else - setting.item - end - - features = feature_list.collect{|key| get_feature(key, registry)}.compact + features = setting_value(setting) end features.collect{|feature| construct_feature_form(feature, registry, options)}.join(tag.hr(class: 'mt-2 mb-3')).html_safe end @@ -150,7 +141,7 @@ def feature_group_display(feature, registry) tag.div(id: feature.key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature.key}-list") do feature.settings.collect do |setting| if setting.meta&.content_type.to_s == 'feature_group' - features = registry.features_by_namespace(setting.item).collect{|key| find_feature(key)} + features = setting_value(setting) feature_group_control(features, registry).html_safe end end.compact.join.html_safe @@ -161,6 +152,8 @@ def feature_group_control(features, registry) features = features.select{|feature| feature.meta.present? && feature.meta.content_type.to_s != 'feature_action' } features.collect do |feature| + settings_with_meta = feature.settings.select{|s| s.meta.present?} + tag.div(class: 'mt-3') do tag.div(class: 'row') do tag.div(class: 'col-md-6') do @@ -169,7 +162,7 @@ def feature_group_control(features, registry) end end + tag.div(class: 'col-md-6') do - action_setting = feature.settings.detect{|setting| setting.meta.content_type.to_s == 'feature_action'} + action_setting = settings_with_meta.detect{|setting| setting.meta.content_type.to_s == 'feature_action'} if action_setting form_with(model: feature, url: action_setting.item, method: :get, remote: true, local: false) do |f| hidden_field_tag('feature[action]', 'renew') + @@ -179,7 +172,7 @@ def feature_group_control(features, registry) end end end + - feature.settings.collect do |setting| + settings_with_meta.collect do |setting| next if setting.meta.content_type.to_s == 'feature_action' section_name = setting.meta&.label || setting.key.to_s.titleize tag.div(class: 'mt-3') do @@ -198,14 +191,8 @@ def feature_group_control(features, registry) end + tag.div(class: 'col-md-6') do tag.ul(class: 'list-group list-group-flush ml-2') do - feature_list = if setting.item.is_a?(Hash) && setting.item['operation'] - elements = setting.item['operation'].split(/\./) - elements[0].constantize.send(elements[1]).call(setting.item['params'].symbolize_keys).success - else - setting.item - end - - feature_list.collect{|value| tag.li(class: 'list-group-item'){ value.to_s.titleize }}.join.html_safe + feature_list = setting_value(setting) + feature_list.collect{|feature| tag.li(class: 'list-group-item'){ feature.key.to_s.titleize }}.join.html_safe end end end @@ -224,4 +211,19 @@ def find_feature(feature_key) return unless feature_class feature_class.where(key: feature_key).first end + + def setting_value(setting) + value = if setting.is_a?(ResourceRegistry::Setting) + JSON.parse(setting.item) + else + setting.item + end + + if value.is_a?(Hash) && value['operation'] + elements = value['operation'].split(/\./) + elements[0].constantize.send(elements[1]).call(value['params'].symbolize_keys).success + else + value + end + end end \ No newline at end of file diff --git a/lib/resource_registry/operations/features/renew.rb b/lib/resource_registry/operations/features/renew.rb index 1b360b14..0e5ee621 100644 --- a/lib/resource_registry/operations/features/renew.rb +++ b/lib/resource_registry/operations/features/renew.rb @@ -21,14 +21,14 @@ def call(params:, registry:) private def extract_options(params, registry) - feature_for_clone = registry[params[:target_feature]] - related_features = feature_for_clone.feature.settings.collect do |setting| + features = [] + features << registry[params[:target_feature]].feature + + related_features = features[0].settings.collect do |setting| get_features(setting.item) if setting.meta && setting.meta.content_type == :feature_list_panel end.compact.flatten - features = [] - features << feature_for_clone.feature - features += related_features.collect{|key| registry[key].feature} + features += related_features.collect{|feature| registry[feature.key].feature} options = { features: features, @@ -94,15 +94,19 @@ def serialize_text(value) if value.is_a?(Symbol) value.to_s.gsub(original_year, new_calender_year).to_sym elsif value.is_a?(Range) && value.min.is_a?(Date) - Range.new(value.min.next_year, value.max.next_year) + Range.new(new_date(value.begin), new_date(value.end)) elsif value.is_a?(Date) value.next_year - elsif value.is_a?(String) - value.gsub(original_year, new_calender_year) + elsif value.is_a?(String) || value.to_s.match(/^\d+$/) + value.to_s.gsub(original_year, new_calender_year) else value end end + + def new_date(ref_date) + Date.new(new_calender_year.to_i, ref_date.month, ref_date.day) + end end end end diff --git a/lib/resource_registry/operations/namespaces/list_features.rb b/lib/resource_registry/operations/namespaces/list_features.rb index 156c02c1..c465154e 100644 --- a/lib/resource_registry/operations/namespaces/list_features.rb +++ b/lib/resource_registry/operations/namespaces/list_features.rb @@ -10,8 +10,9 @@ class ListFeatures send(:include, Dry::Monads[:result, :do, :try]) def call(params) - values = yield validate(params) - features = yield list_features(values) + values = yield validate(params) + feature_keys = yield list_features(values) + features = yield find_features(feature_keys, values) Success(features) end @@ -26,16 +27,33 @@ def validate(params) return Failure("Unable to find namespace #{params[:namespace]} under #{params[:registry]}.") unless registry.namespaces.include?(params[:namespace]) Success({ - namespace: params[:namespace], - registry: registry + namespace: params[:namespace], + registry: registry, + order: params[:order] }) end def list_features(values) - features = values[:registry].features_by_namespace(values[:namespace]) + feature_keys = values[:registry].features_by_namespace(values[:namespace]) + + Success(feature_keys) + end + + def find_features(feature_keys, values) + features = feature_keys.collect {|key| ResourceRegistry::Stores.feature_model.where(key: key).first }.compact + + if values[:order] + features_for_sort = features.select{|f| f.settings.any?{|s| s.key == values[:order].to_sym && s.item}} + features = features_for_sort.sort_by{|f| sort_by_value(f, values[:order])}.reverse + (features - features_for_sort) + end Success(features) end + + def sort_by_value(feature, setting_key) + setting = feature.settings.detect{|s| s.key == setting_key.to_sym} + setting.item.to_i + end end end end From add6a9baaa605bd57ebe4e9a7719f37eccd23982 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Thu, 11 Feb 2021 16:58:35 -0500 Subject: [PATCH 35/40] Fixed spec failures --- .../operations/features/renew.rb | 17 ++++++----- .../operations/namespaces/list_features.rb | 3 +- .../aca_shop_market/feature_group.yml | 28 +++++++++++-------- .../operations/features/renew_spec.rb | 2 ++ .../namespaces/list_features_spec.rb | 21 ++++++-------- 5 files changed, 36 insertions(+), 35 deletions(-) diff --git a/lib/resource_registry/operations/features/renew.rb b/lib/resource_registry/operations/features/renew.rb index 0e5ee621..39f20ff4 100644 --- a/lib/resource_registry/operations/features/renew.rb +++ b/lib/resource_registry/operations/features/renew.rb @@ -38,18 +38,11 @@ def extract_options(params, registry) Success(options) end - def get_features(item) - return item unless item['operation'].present? - elements = item['operation'].split(/\./) - elements[0].constantize.send(elements[1]).call(item['params'].symbolize_keys).success - end - def construct_params(options) @original_year = options[:features][0].key.to_s.scan(/\d{4}/)[0] @new_calender_year = options[:calender_year] - features_params = options[:features].collect do |feature| - serialize_hash(feature.to_h) - end + features_params = options[:features].collect{|feature| serialize_hash(feature.to_h)} + Success(features_params) end @@ -72,6 +65,12 @@ def persist(features, registry) Success(registry) end + def get_features(item) + return item unless (item.is_a?(Hash) && item['operation'].present?) + elements = item['operation'].split(/\./) + elements[0].constantize.send(elements[1]).call(item['params'].symbolize_keys).success + end + def serialize_hash(attributes) attributes.reduce({}) do |values, (key, value)| values[key] = if value.is_a?(Hash) diff --git a/lib/resource_registry/operations/namespaces/list_features.rb b/lib/resource_registry/operations/namespaces/list_features.rb index c465154e..1722ebc9 100644 --- a/lib/resource_registry/operations/namespaces/list_features.rb +++ b/lib/resource_registry/operations/namespaces/list_features.rb @@ -40,7 +40,8 @@ def list_features(values) end def find_features(feature_keys, values) - features = feature_keys.collect {|key| ResourceRegistry::Stores.feature_model.where(key: key).first }.compact + feature_model = ResourceRegistry::Stores.feature_model + features = feature_keys.collect{|key| feature_model.present? ? feature_model.where(key: key).first : values[:registry][key].feature }.compact if values[:order] features_for_sort = features.select{|f| f.settings.any?{|s| s.key == values[:order].to_sym && s.item}} diff --git a/spec/rails_app/system/config/templates/features/aca_shop_market/feature_group.yml b/spec/rails_app/system/config/templates/features/aca_shop_market/feature_group.yml index 0c9279a6..c4e7a063 100644 --- a/spec/rails_app/system/config/templates/features/aca_shop_market/feature_group.yml +++ b/spec/rails_app/system/config/templates/features/aca_shop_market/feature_group.yml @@ -70,24 +70,26 @@ registry: is_required: true is_visible: true settings: + - key: :calender_year + item: 2021 - key: :contribution_models item: - - :initial_sponsor_jan_default_2021 - - :initial_sponsor_default_2021 - - :renewal_sponsor_jan_default_2021 - - :renewal_sponsor_default_2021 + operation: ResourceRegistry::Operations::Namespaces::ListFeatures.new + params: + namespace: "enroll_app.aca_shop_market.benefit_market_catalog.catalog_2021.contribution_model_criteria" + registry: EnrollRegistry meta: label: Contribution Models content_type: :feature_list_panel description: Contribution Models is_required: false is_visible: true - - key: :clone - item: "/exchanges/configurations/shop_product_package_clone/edit" + - key: :renew + item: "/exchanges/configurations/shop_product_package_renew/edit" meta: - label: Clone Product Package + label: Renew Product Package content_type: :feature_action - description: Clone Product Packages for the given calender year + description: Renew Product Packages for the given calender year is_required: false is_visible: true - key: :health_product_package_2020 @@ -101,12 +103,14 @@ registry: is_required: true is_visible: true settings: + - key: :calender_year + item: 2020 - key: :contribution_models item: - - :initial_sponsor_jan_default_2020 - - :initial_sponsor_default_2020 - - :renewal_sponsor_jan_default_2020 - - :renewal_sponsor_default_2020 + operation: ResourceRegistry::Operations::Namespaces::ListFeatures.new + params: + namespace: "enroll_app.aca_shop_market.benefit_market_catalog.catalog_2020.contribution_model_criteria" + registry: EnrollRegistry meta: label: Contribution Models content_type: :feature_list_panel diff --git a/spec/resource_registry/operations/features/renew_spec.rb b/spec/resource_registry/operations/features/renew_spec.rb index 025ccbbc..99223636 100644 --- a/spec/resource_registry/operations/features/renew_spec.rb +++ b/spec/resource_registry/operations/features/renew_spec.rb @@ -23,6 +23,8 @@ } it "should return success with hash output" do + stub_const("EnrollRegistry", registry) + expect(subject).to be_a Dry::Monads::Result::Success expect(registry[:health_product_package_2022]).to be_truthy end diff --git a/spec/resource_registry/operations/namespaces/list_features_spec.rb b/spec/resource_registry/operations/namespaces/list_features_spec.rb index fceabf2f..539d9be4 100644 --- a/spec/resource_registry/operations/namespaces/list_features_spec.rb +++ b/spec/resource_registry/operations/namespaces/list_features_spec.rb @@ -9,23 +9,18 @@ context 'When valid feature hash passed' do - let!(:registry) { ResourceRegistry::Registry.new } - let(:register) { ResourceRegistry::Operations::Registries::Create.new.call(path: feature_group_template_path, registry: registry) } - - let!(:test_registry) do - register - ::ResourceRegistry::TestRegistry = registry - end - + let(:registry) { ResourceRegistry::Registry.new } + let(:register) { ResourceRegistry::Operations::Registries::Create.new.call(path: feature_group_template_path, registry: registry) } let(:namespace) {"enroll_app.aca_shop_market.benefit_market_catalog.catalog_2021.contribution_model_criteria"} + let(:params) { {namespace: namespace, registry: 'EnrollRegistry'} } + + before { register } - let(:params) { - {namespace: namespace, registry: 'ResourceRegistry::TestRegistry'} - } + it "should return success with features" do + stub_const("EnrollRegistry", registry) - it "should return success with hash output" do expect(subject).to be_a Dry::Monads::Result::Success - expect(subject.success).to eq registry.features_by_namespace(namespace) + expect(subject.success.map(&:key)).to eq registry.features_by_namespace(namespace) end end end \ No newline at end of file From 0a8be8c3ab9a778148bc9e027426e6b175a32ab4 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Fri, 12 Feb 2021 08:35:04 -0500 Subject: [PATCH 36/40] commit specs --- .../operations/features/renew.rb | 2 +- .../operations/graphs/create.rb | 2 + .../operations/namespaces/build.rb | 36 +++--- .../operations/namespaces/list_features.rb | 4 +- .../operations/registries/load.rb | 2 +- .../serializers/namespaces/serialize.rb | 10 +- .../operations/graphs/create_spec.rb | 108 ++++++++++++++++++ .../operations/namespaces/build_spec.rb | 101 ++++++++++++++++ .../operations/namespaces/create_spec.rb | 65 +++++++++++ .../serializers/namespaces/serialize_spec.rb | 49 ++++++++ 10 files changed, 346 insertions(+), 33 deletions(-) create mode 100644 spec/resource_registry/operations/graphs/create_spec.rb create mode 100644 spec/resource_registry/operations/namespaces/build_spec.rb create mode 100644 spec/resource_registry/operations/namespaces/create_spec.rb create mode 100644 spec/resource_registry/serializers/namespaces/serialize_spec.rb diff --git a/lib/resource_registry/operations/features/renew.rb b/lib/resource_registry/operations/features/renew.rb index 39f20ff4..fb097c63 100644 --- a/lib/resource_registry/operations/features/renew.rb +++ b/lib/resource_registry/operations/features/renew.rb @@ -3,7 +3,7 @@ module ResourceRegistry module Operations module Features - # Create a Feature + # Renew the given feature along with all associated features class Renew send(:include, Dry::Monads[:result, :do, :try]) diff --git a/lib/resource_registry/operations/graphs/create.rb b/lib/resource_registry/operations/graphs/create.rb index 833b448c..e23bb4f3 100644 --- a/lib/resource_registry/operations/graphs/create.rb +++ b/lib/resource_registry/operations/graphs/create.rb @@ -7,12 +7,14 @@ module ResourceRegistry module Operations module Graphs + # Creates Directed Acyclic Graph with the given namespaces class Create send(:include, Dry::Monads[:result, :do]) def call(namespaces, registry) graph = yield create(namespaces, registry) result = yield validate(graph) + Success(result) end diff --git a/lib/resource_registry/operations/namespaces/build.rb b/lib/resource_registry/operations/namespaces/build.rb index bc805c43..178b3558 100644 --- a/lib/resource_registry/operations/namespaces/build.rb +++ b/lib/resource_registry/operations/namespaces/build.rb @@ -7,10 +7,8 @@ module Namespaces class Build send(:include, Dry::Monads[:result, :do]) - NAVIGATION_TYPES = %w[feature_list nav] - - def call(feature) - feature = yield validate(feature) + def call(feature, content_types = []) + feature = yield validate(feature, content_types) values = yield build(feature) Success(values) @@ -18,11 +16,17 @@ def call(feature) private - def validate(feature) - return Failure("feature meta can't be empty") if feature.meta.to_h.empty? - return Failure("feature namespace path can't be empty") if feature.namespace_path.to_h.empty? - return Failure("namesapce content type should be #{NAVIGATION_TYPES}") unless NAVIGATION_TYPES.include?(feature.namespace_path.meta&.content_type&.to_s) - Success(feature) + def validate(feature, content_types) + errors = [] + errors << "feature meta can't be empty" if feature.meta.to_h.empty? + errors << "feature namespace path can't be empty" if feature.namespace_path.to_h.empty? + errors << "namesapce content type should be #{content_types}" if content_types.present? && content_types.exclude?(feature.namespace_path.meta&.content_type&.to_s) + + if errors.empty? + Success(feature) + else + Failure(errors) + end end def build(feature) @@ -35,20 +39,6 @@ def build(feature) meta: namespace_path.meta.to_h }) end - - # def construct(namespaces, feature) - # namespace_identifier = feature.namespace.map(&:to_s).join('.') - # if namespaces[namespace_identifier] - # namespaces[namespace_identifier][:features] << feature.key - # else - # namespaces[namespace_identifier] = { - # key: feature.namespace[-1], - # path: feature.namespace, - # features: [feature.key] - # } - # end - # Success(namespaces) - # end end end end diff --git a/lib/resource_registry/operations/namespaces/list_features.rb b/lib/resource_registry/operations/namespaces/list_features.rb index 1722ebc9..fca708fb 100644 --- a/lib/resource_registry/operations/namespaces/list_features.rb +++ b/lib/resource_registry/operations/namespaces/list_features.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true -# params: namespace -# returns array of feature dsl objects - module ResourceRegistry module Operations module Namespaces + # List features under a given namespace. Features are also sorted in the order requested in the params. class ListFeatures send(:include, Dry::Monads[:result, :do, :try]) diff --git a/lib/resource_registry/operations/registries/load.rb b/lib/resource_registry/operations/registries/load.rb index 461f5ae8..054f8694 100644 --- a/lib/resource_registry/operations/registries/load.rb +++ b/lib/resource_registry/operations/registries/load.rb @@ -38,7 +38,7 @@ def load_features(paths, registry) end def serialize_namespaces(features) - ResourceRegistry::Serializers::Namespaces::Serialize.new.call(features: features) + ResourceRegistry::Serializers::Namespaces::Serialize.new.call(features: features, namespace_types: %w[feature_list nav]) end def register_graph(namespaces, registry) diff --git a/lib/resource_registry/serializers/namespaces/serialize.rb b/lib/resource_registry/serializers/namespaces/serialize.rb index 8f5ffd35..c1e76145 100644 --- a/lib/resource_registry/serializers/namespaces/serialize.rb +++ b/lib/resource_registry/serializers/namespaces/serialize.rb @@ -3,14 +3,14 @@ module ResourceRegistry module Serializers module Namespaces - # Transform a Hash into YAML-formatted String + # Transform feature collection into Namespace hash class Serialize send(:include, Dry::Monads[:result, :do, :try]) # @param [Hash] params Hash to be transformed into YAML String # @return [String] parsed values wrapped in Dry::Monads::Result object - def call(features:) - namespaces = yield build(features) + def call(features:, namespace_types:) + namespaces = yield build(features, namespace_types) namespace_dict = yield merge(namespaces) Success(namespace_dict.values) @@ -18,10 +18,10 @@ def call(features:) private - def build(features) + def build(features, namespace_types) Try { features.collect do |feature| - namespace = ResourceRegistry::Operations::Namespaces::Build.new.call(feature) + namespace = ResourceRegistry::Operations::Namespaces::Build.new.call(feature, namespace_types) namespace.success if namespace.success? end.compact }.to_result diff --git a/spec/resource_registry/operations/graphs/create_spec.rb b/spec/resource_registry/operations/graphs/create_spec.rb new file mode 100644 index 00000000..4eed3ba8 --- /dev/null +++ b/spec/resource_registry/operations/graphs/create_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ResourceRegistry::Operations::Graphs::Create do + include RegistryDataSeed + + subject { described_class.new.call(namespaces, registry) } + + let(:registry) { ResourceRegistry::Registry.new } + let(:features) { ResourceRegistry::Operations::Registries::Create.new.call(path: feature_group_template_path, registry: registry).success } + let(:namespace_types) { %w[feature_list nav] } + let(:namespaces) { ResourceRegistry::Serializers::Namespaces::Serialize.new.call(features: features, namespace_types: namespace_types).success } + let(:features_with_meta) { features.select{|f| f.meta.to_h.present?} } + let(:matched_features) { features_with_meta.select{|feature| namespace_types.include?(feature.namespace_path&.meta&.content_type&.to_s)} } + + context 'When a valid namespace passed' do + + it 'should return success monad' do + expect(subject).to be_a Dry::Monads::Result::Success + end + + it 'should return a directed acyclic graph' do + value = subject.success + expect(value).to be_a RGL::DirectedAdjacencyGraph + expect(value.directed?).to be_truthy + expect(value.cycles).to be_empty + end + + it 'should create vertices with valid namespace paths' do + graph = subject.success + namespace_paths = matched_features.collect{|feature| feature.namespace_path.path}.uniq + + namespace_paths.inject([]){|vertex_paths, path| vertex_paths += path_to_vertex_path(path)}.uniq.each do |vertex_path| + vertex = graph.vertices.detect{|vertex| vertex.path == vertex_path} + expect(vertex).to be_present + end + end + end + + def path_to_vertex_path(path = []) + return if path.empty? + paths = [] + vertex_paths = [] + while true + current = path.shift + paths.push(current) + vertex_paths << paths.dup + break if path.empty? + end + vertex_paths + end + + context 'When invalid namespaces passed' do + let(:feature1) { + ResourceRegistry::Feature.new({ + :key => :health_packages, + :namespace_path => { + :path => [:dchbx, :enroll_app, :aca_shop_market], + :meta => { + :label => "ACA Shop", + :content_type => :feature_list, + :is_required => true, + :is_visible => true + } + }, + :is_enabled => true, + :item => :features_display, + :meta => { + :label => "Health Packages", + :content_type => :legend, + :is_required => true, + :is_visible => true + }, + }) + } + + let(:feature2) { + ResourceRegistry::Feature.new({ + :key => :health_packages_two, + :namespace_path => { + :path => [:aca_shop_market, :dchbx, :enroll_app], + :meta => { + :label => "ACA Shop", + :content_type => :feature_list, + :is_required => true, + :is_visible => true + } + }, + :is_enabled => true, + :item => :features_display, + :meta => { + :label => "Health Packages", + :content_type => :legend, + :is_required => true, + :is_visible => true + }, + }) + } + + let(:features) { [feature1, feature2] } + + # Pending + it 'should return with error' do + subject + end + end +end \ No newline at end of file diff --git a/spec/resource_registry/operations/namespaces/build_spec.rb b/spec/resource_registry/operations/namespaces/build_spec.rb new file mode 100644 index 00000000..b1dbedc9 --- /dev/null +++ b/spec/resource_registry/operations/namespaces/build_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ResourceRegistry::Operations::Namespaces::Build do + include RegistryDataSeed + + subject { described_class.new.call(feature, content_types) } + + let(:registry) { ResourceRegistry::Registry.new } + let(:features) { ResourceRegistry::Operations::Registries::Create.new.call(path: feature_template_path, registry: registry).success } + + let(:namespace_content_type) { :legend } + + let(:namespace_path) { + { + :path =>[:features, :enroll_app], + :meta => { + :label => "SHOP", + :content_type => namespace_content_type, + :default => true, + :is_required => false, + :is_visible => true + } + } + } + + let(:feature_meta) { + { + :label => "Enable ACA SHOP Market", + :content_type => :boolean, + :default => true, + :is_required => false, + :is_visible => true + } + } + + let(:feature) { + ResourceRegistry::Feature.new({ + :key => :employer_sic, + :namespace_path => namespace_path, + :is_enabled => false, + :meta => feature_meta + }) + } + + context 'When feature with missing meta passed' do + let(:content_types) { nil } + let(:feature_meta) { {} } + + it 'should fail with error' do + result = subject + + expect(result).to be_a Dry::Monads::Result::Failure + expect(result.failure).to include("feature meta can't be empty") + end + end + + context 'When namespace content_types argument passed' do + let(:content_types) { %w[feature_list nav] } + + context 'and feature namespace content_type mismatched' do + let(:namespace_content_type) {:legend} + + it 'should fail with error' do + result = subject + + expect(result).to be_a Dry::Monads::Result::Failure + expect(result.failure).to include("namesapce content type should be #{content_types}") + end + end + + context 'and feature namespace content_type matched' do + let(:namespace_content_type) {:nav} + + it 'should pass and return namespace' do + result = subject + + expect(result).to be_a Dry::Monads::Result::Success + output = result.success + expect(output[:path]).to eq(feature.namespace_path.path) + expect(output[:feature_keys]).to include(feature.key) + end + end + end + + context 'When namespace content_types argument not passed' do + let(:content_types) { nil } + + context 'and feature has meta and namespace_path' do + it 'should pass and return namespace' do + result = subject + + expect(result).to be_a Dry::Monads::Result::Success + output = result.success + expect(output[:path]).to eq(feature.namespace_path.path) + expect(output[:feature_keys]).to include(feature.key) + end + end + end +end \ No newline at end of file diff --git a/spec/resource_registry/operations/namespaces/create_spec.rb b/spec/resource_registry/operations/namespaces/create_spec.rb new file mode 100644 index 00000000..f211c1af --- /dev/null +++ b/spec/resource_registry/operations/namespaces/create_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ResourceRegistry::Operations::Namespaces::Create do + include RegistryDataSeed + + subject { described_class.new.call(params) } + + let(:feature) { + ResourceRegistry::Feature.new({ + :key => :health_packages, + :namespace_path => { + :path => [:dchbx, :enroll_app, :aca_shop_market], + :meta => { + :label => "ACA Shop", + :content_type => :feature_list, + :is_required => true, + :is_visible => true + } + }, + :is_enabled => true, + :item => :features_display, + :meta => { + :label => "Health Packages", + :content_type => :legend, + :is_required => true, + :is_visible => true + }, + }) + } + + context 'When valid params passed' do + let(:params) { + { + key: feature.namespace_path.path.map(&:to_s).join('_'), + path: feature.namespace_path.path, + feature_keys: [feature.key], + meta: feature.namespace_path.meta.to_h + } + } + + it "should return namespace entity" do + subject + expect(subject).to be_a Dry::Monads::Result::Success + expect(subject.success).to be_a ResourceRegistry::Namespace + end + end + + context 'When invalid params passed' do + let(:params) { + { + key: feature.namespace_path.path.map(&:to_s).join('_'), + feature_keys: [feature.key], + meta: feature.namespace_path.meta.to_h + } + } + + it "should return error" do + subject + expect(subject).to be_a Dry::Monads::Result::Failure + expect(subject.failure.errors.to_h).to eq({:path=>["is missing"]}) + end + end +end \ No newline at end of file diff --git a/spec/resource_registry/serializers/namespaces/serialize_spec.rb b/spec/resource_registry/serializers/namespaces/serialize_spec.rb new file mode 100644 index 00000000..20f874b7 --- /dev/null +++ b/spec/resource_registry/serializers/namespaces/serialize_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ResourceRegistry::Serializers::Namespaces::Serialize do + include RegistryDataSeed + + subject { described_class.new.call(features: features, namespace_types: namespace_types) } + + context 'When features set passed' do + + let(:registry) { ResourceRegistry::Registry.new } + let(:features) { ResourceRegistry::Operations::Registries::Create.new.call(path: feature_group_template_path, registry: registry).success } + let(:features_with_meta) { features.select{|f| f.meta.to_h.present?} } + + context "when namespace_types ignored" do + let(:namespace_types) { [] } + + it "should return namespace hash with meta defined features" do + result = subject.success + + expect(subject).to be_a Dry::Monads::Result::Success + features_with_meta.each do |feature| + namespace_key = feature.namespace_path.path.map(&:to_s).join('_') + namespace_hash = result.detect{|values| values[:key] == namespace_key} + expect(namespace_hash).to be_present + expect(namespace_hash[:feature_keys]).to include(feature.key) + end + end + end + + context "when namespace_types passed" do + let(:namespace_types) { %w[feature_list nav] } + + it "should return namespace hash with meta defined and namespace_type matching features" do + result = subject.success + expected_features = features_with_meta.select{|feature| namespace_types.include?(feature.namespace_path&.meta&.content_type&.to_s)} + + expect(subject).to be_a Dry::Monads::Result::Success + expected_features.each do |feature| + namespace_key = feature.namespace_path.path.map(&:to_s).join('_') + namespace_hash = result.detect{|values| values[:key] == namespace_key} + expect(namespace_hash).to be_present + expect(namespace_hash[:feature_keys]).to include(feature.key) + end + end + end + end +end \ No newline at end of file From 402206f80cba73d8419b0f204858493e09fe409d Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Fri, 12 Feb 2021 10:59:38 -0500 Subject: [PATCH 37/40] Added rubocop fixes for specs --- spec/resource_registry/feature_dsl_spec.rb | 2 +- spec/resource_registry/feature_spec.rb | 2 +- .../operations/features/renew_spec.rb | 6 +- .../operations/features/update_spec.rb | 2 +- .../operations/graphs/create_spec.rb | 92 +++++++++---------- .../operations/namespaces/build_spec.rb | 38 ++++---- .../operations/namespaces/create_spec.rb | 52 +++++------ .../namespaces/update_features_spec.rb | 20 ++-- .../operations/registries/create_spec.rb | 4 +- spec/resource_registry/registry_spec.rb | 2 +- .../serializers/namespaces/serialize_spec.rb | 2 +- spec/resource_registry/setting_spec.rb | 2 +- 12 files changed, 112 insertions(+), 112 deletions(-) diff --git a/spec/resource_registry/feature_dsl_spec.rb b/spec/resource_registry/feature_dsl_spec.rb index e9913a4e..27ec7c28 100644 --- a/spec/resource_registry/feature_dsl_spec.rb +++ b/spec/resource_registry/feature_dsl_spec.rb @@ -139,7 +139,7 @@ def call(params) let(:dsl_sans_meta) { described_class.new(feature_sans_meta) } it "should return an empty hash" do - expect(dsl_sans_meta.meta).to eq Hash.new + expect(dsl_sans_meta.meta).to eq({}) end context "and meta key is not present" do diff --git a/spec/resource_registry/feature_spec.rb b/spec/resource_registry/feature_spec.rb index 115600a4..aa48a13f 100644 --- a/spec/resource_registry/feature_spec.rb +++ b/spec/resource_registry/feature_spec.rb @@ -65,7 +65,7 @@ def call(params) end context "Given hash params include a class as the item value" do - let(:greet_message) { "Hello " + options[:name] } + let(:greet_message) { "Hello #{options[:name]}" } it "should invoke the class with the passed options parameters" do setting = described_class.new(all_params) diff --git a/spec/resource_registry/operations/features/renew_spec.rb b/spec/resource_registry/operations/features/renew_spec.rb index 99223636..090fd072 100644 --- a/spec/resource_registry/operations/features/renew_spec.rb +++ b/spec/resource_registry/operations/features/renew_spec.rb @@ -12,7 +12,7 @@ let(:registry) { ResourceRegistry::Registry.new } let!(:register) { ResourceRegistry::Operations::Registries::Create.new.call(path: feature_group_template_path, registry: registry) } - let(:params) { + let(:params) do { params: { target_feature: 'health_product_package_2021', @@ -20,8 +20,8 @@ }, registry: registry } - } - + end + it "should return success with hash output" do stub_const("EnrollRegistry", registry) diff --git a/spec/resource_registry/operations/features/update_spec.rb b/spec/resource_registry/operations/features/update_spec.rb index ea53901a..e793d86e 100644 --- a/spec/resource_registry/operations/features/update_spec.rb +++ b/spec/resource_registry/operations/features/update_spec.rb @@ -20,7 +20,7 @@ { :key => :aca_shop_market, :is_enabled => true, - :namespace_path => {:path=>[:features, :enroll_app]}, + :namespace_path => {:path => [:features, :enroll_app]}, :settings => [ { enroll_prior_to_effective_on_max: { days: 10 } }, { enroll_after_effective_on_max: { days: 60 } }, diff --git a/spec/resource_registry/operations/graphs/create_spec.rb b/spec/resource_registry/operations/graphs/create_spec.rb index 4eed3ba8..e70e7775 100644 --- a/spec/resource_registry/operations/graphs/create_spec.rb +++ b/spec/resource_registry/operations/graphs/create_spec.rb @@ -11,7 +11,7 @@ let(:features) { ResourceRegistry::Operations::Registries::Create.new.call(path: feature_group_template_path, registry: registry).success } let(:namespace_types) { %w[feature_list nav] } let(:namespaces) { ResourceRegistry::Serializers::Namespaces::Serialize.new.call(features: features, namespace_types: namespace_types).success } - let(:features_with_meta) { features.select{|f| f.meta.to_h.present?} } + let(:features_with_meta) { features.select{|f| f.meta.to_h.present?} } let(:matched_features) { features_with_meta.select{|feature| namespace_types.include?(feature.namespace_path&.meta&.content_type&.to_s)} } context 'When a valid namespace passed' do @@ -42,7 +42,7 @@ def path_to_vertex_path(path = []) return if path.empty? paths = [] vertex_paths = [] - while true + loop do current = path.shift paths.push(current) vertex_paths << paths.dup @@ -52,54 +52,54 @@ def path_to_vertex_path(path = []) end context 'When invalid namespaces passed' do - let(:feature1) { + let(:feature1) do ResourceRegistry::Feature.new({ - :key => :health_packages, - :namespace_path => { - :path => [:dchbx, :enroll_app, :aca_shop_market], - :meta => { - :label => "ACA Shop", - :content_type => :feature_list, - :is_required => true, - :is_visible => true - } - }, - :is_enabled => true, - :item => :features_display, - :meta => { - :label => "Health Packages", - :content_type => :legend, - :is_required => true, - :is_visible => true - }, - }) - } - - let(:feature2) { + :key => :health_packages, + :namespace_path => { + :path => [:dchbx, :enroll_app, :aca_shop_market], + :meta => { + :label => "ACA Shop", + :content_type => :feature_list, + :is_required => true, + :is_visible => true + } + }, + :is_enabled => true, + :item => :features_display, + :meta => { + :label => "Health Packages", + :content_type => :legend, + :is_required => true, + :is_visible => true + } + }) + end + + let(:feature2) do ResourceRegistry::Feature.new({ - :key => :health_packages_two, - :namespace_path => { - :path => [:aca_shop_market, :dchbx, :enroll_app], - :meta => { - :label => "ACA Shop", - :content_type => :feature_list, - :is_required => true, - :is_visible => true - } - }, - :is_enabled => true, - :item => :features_display, - :meta => { - :label => "Health Packages", - :content_type => :legend, - :is_required => true, - :is_visible => true - }, - }) - } + :key => :health_packages_two, + :namespace_path => { + :path => [:aca_shop_market, :dchbx, :enroll_app], + :meta => { + :label => "ACA Shop", + :content_type => :feature_list, + :is_required => true, + :is_visible => true + } + }, + :is_enabled => true, + :item => :features_display, + :meta => { + :label => "Health Packages", + :content_type => :legend, + :is_required => true, + :is_visible => true + } + }) + end let(:features) { [feature1, feature2] } - + # Pending it 'should return with error' do subject diff --git a/spec/resource_registry/operations/namespaces/build_spec.rb b/spec/resource_registry/operations/namespaces/build_spec.rb index b1dbedc9..64633713 100644 --- a/spec/resource_registry/operations/namespaces/build_spec.rb +++ b/spec/resource_registry/operations/namespaces/build_spec.rb @@ -9,12 +9,12 @@ let(:registry) { ResourceRegistry::Registry.new } let(:features) { ResourceRegistry::Operations::Registries::Create.new.call(path: feature_template_path, registry: registry).success } - + let(:namespace_content_type) { :legend } - let(:namespace_path) { + let(:namespace_path) do { - :path =>[:features, :enroll_app], + :path => [:features, :enroll_app], :meta => { :label => "SHOP", :content_type => namespace_content_type, @@ -23,9 +23,9 @@ :is_visible => true } } - } + end - let(:feature_meta) { + let(:feature_meta) do { :label => "Enable ACA SHOP Market", :content_type => :boolean, @@ -33,16 +33,16 @@ :is_required => false, :is_visible => true } - } + end - let(:feature) { + let(:feature) do ResourceRegistry::Feature.new({ - :key => :employer_sic, - :namespace_path => namespace_path, - :is_enabled => false, - :meta => feature_meta - }) - } + :key => :employer_sic, + :namespace_path => namespace_path, + :is_enabled => false, + :meta => feature_meta + }) + end context 'When feature with missing meta passed' do let(:content_types) { nil } @@ -56,12 +56,12 @@ end end - context 'When namespace content_types argument passed' do + context 'When namespace content_types argument passed' do let(:content_types) { %w[feature_list nav] } context 'and feature namespace content_type mismatched' do let(:namespace_content_type) {:legend} - + it 'should fail with error' do result = subject @@ -78,8 +78,8 @@ expect(result).to be_a Dry::Monads::Result::Success output = result.success - expect(output[:path]).to eq(feature.namespace_path.path) - expect(output[:feature_keys]).to include(feature.key) + expect(output[:path]).to eq(feature.namespace_path.path) + expect(output[:feature_keys]).to include(feature.key) end end end @@ -93,8 +93,8 @@ expect(result).to be_a Dry::Monads::Result::Success output = result.success - expect(output[:path]).to eq(feature.namespace_path.path) - expect(output[:feature_keys]).to include(feature.key) + expect(output[:path]).to eq(feature.namespace_path.path) + expect(output[:feature_keys]).to include(feature.key) end end end diff --git a/spec/resource_registry/operations/namespaces/create_spec.rb b/spec/resource_registry/operations/namespaces/create_spec.rb index f211c1af..fd7cf949 100644 --- a/spec/resource_registry/operations/namespaces/create_spec.rb +++ b/spec/resource_registry/operations/namespaces/create_spec.rb @@ -7,38 +7,38 @@ subject { described_class.new.call(params) } - let(:feature) { + let(:feature) do ResourceRegistry::Feature.new({ - :key => :health_packages, - :namespace_path => { - :path => [:dchbx, :enroll_app, :aca_shop_market], - :meta => { - :label => "ACA Shop", - :content_type => :feature_list, - :is_required => true, - :is_visible => true - } - }, - :is_enabled => true, - :item => :features_display, - :meta => { - :label => "Health Packages", - :content_type => :legend, - :is_required => true, - :is_visible => true - }, - }) - } + :key => :health_packages, + :namespace_path => { + :path => [:dchbx, :enroll_app, :aca_shop_market], + :meta => { + :label => "ACA Shop", + :content_type => :feature_list, + :is_required => true, + :is_visible => true + } + }, + :is_enabled => true, + :item => :features_display, + :meta => { + :label => "Health Packages", + :content_type => :legend, + :is_required => true, + :is_visible => true + } + }) + end context 'When valid params passed' do - let(:params) { + let(:params) do { key: feature.namespace_path.path.map(&:to_s).join('_'), path: feature.namespace_path.path, feature_keys: [feature.key], meta: feature.namespace_path.meta.to_h } - } + end it "should return namespace entity" do subject @@ -48,18 +48,18 @@ end context 'When invalid params passed' do - let(:params) { + let(:params) do { key: feature.namespace_path.path.map(&:to_s).join('_'), feature_keys: [feature.key], meta: feature.namespace_path.meta.to_h } - } + end it "should return error" do subject expect(subject).to be_a Dry::Monads::Result::Failure - expect(subject.failure.errors.to_h).to eq({:path=>["is missing"]}) + expect(subject.failure.errors.to_h).to eq({:path => ["is missing"]}) end end end \ No newline at end of file diff --git a/spec/resource_registry/operations/namespaces/update_features_spec.rb b/spec/resource_registry/operations/namespaces/update_features_spec.rb index ffcc25c8..b202b33c 100644 --- a/spec/resource_registry/operations/namespaces/update_features_spec.rb +++ b/spec/resource_registry/operations/namespaces/update_features_spec.rb @@ -22,18 +22,18 @@ let(:params) do { features: { - :"0"=>{key: 'health', is_enabled: 'true', namespace: 'shop.2021'}, - :"1"=>{key: 'dental', is_enabled: 'true', namespace: 'shop.2021'}, - :"2"=>{ - key: 'benefit_market_catalog', - namespace: 'shop.2021', - is_enabled: 'true', - settings: { - :"0"=>{title: "DC Health Link SHOP Benefit Catalog"}, - :"1"=>{description: "Test Description"} - } + :"0" => {key: 'health', is_enabled: 'true', namespace: 'shop.2021'}, + :"1" => {key: 'dental', is_enabled: 'true', namespace: 'shop.2021'}, + :"2" => { + key: 'benefit_market_catalog', + namespace: 'shop.2021', + is_enabled: 'true', + settings: { + :"0" => {title: "DC Health Link SHOP Benefit Catalog"}, + :"1" => {description: "Test Description"} } } + } } end diff --git a/spec/resource_registry/operations/registries/create_spec.rb b/spec/resource_registry/operations/registries/create_spec.rb index e40280b4..040e1cfd 100644 --- a/spec/resource_registry/operations/registries/create_spec.rb +++ b/spec/resource_registry/operations/registries/create_spec.rb @@ -14,7 +14,7 @@ it "should return success with hash output" do subject expect(subject).to be_a Dry::Monads::Result::Success - expect(subject.value!).to include(a_kind_of ResourceRegistry::Feature) + expect(subject.value!).to include(a_kind_of(ResourceRegistry::Feature)) end end @@ -25,7 +25,7 @@ it "should return success with hash output" do subject expect(subject).to be_a Dry::Monads::Result::Failure - expect(subject.failure.errors[:namespace_path]).to eq [{:text=>"invalid meta", :error=>{:path=>["must be an array"]}}] + expect(subject.failure.errors[:namespace_path]).to eq [{:text => "invalid meta", :error => {:path => ["must be an array"]}}] end end end \ No newline at end of file diff --git a/spec/resource_registry/registry_spec.rb b/spec/resource_registry/registry_spec.rb index 4942381c..c56c0709 100644 --- a/spec/resource_registry/registry_spec.rb +++ b/spec/resource_registry/registry_spec.rb @@ -62,7 +62,7 @@ def call(params) let(:key) { :greeter_feature } let(:namespace) { {path: [:level_1, :level_2, :level_3]} } let(:namespace_str) { 'level_1.level_2.level_3'} - let(:namespace_key) { namespace_str + '.' + key.to_s } + let(:namespace_key) { "#{namespace_str}.#{key}" } let(:is_enabled) { false } let(:item) { Greeter.new } diff --git a/spec/resource_registry/serializers/namespaces/serialize_spec.rb b/spec/resource_registry/serializers/namespaces/serialize_spec.rb index 20f874b7..00d1b8af 100644 --- a/spec/resource_registry/serializers/namespaces/serialize_spec.rb +++ b/spec/resource_registry/serializers/namespaces/serialize_spec.rb @@ -12,7 +12,7 @@ let(:registry) { ResourceRegistry::Registry.new } let(:features) { ResourceRegistry::Operations::Registries::Create.new.call(path: feature_group_template_path, registry: registry).success } let(:features_with_meta) { features.select{|f| f.meta.to_h.present?} } - + context "when namespace_types ignored" do let(:namespace_types) { [] } diff --git a/spec/resource_registry/setting_spec.rb b/spec/resource_registry/setting_spec.rb index 91e1e168..7b448e5c 100644 --- a/spec/resource_registry/setting_spec.rb +++ b/spec/resource_registry/setting_spec.rb @@ -59,7 +59,7 @@ def call(params) end context "Given hash params include a class as the item value" do - let(:greet_message) { "Hello " + options[:name] } + let(:greet_message) { "Hello #{options[:name]}" } it "should invoke the class with the passed options parameters" do setting = described_class.new(all_params) From ede7aab1e121d041cc9b92c95be51e63debbb87d Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Fri, 12 Feb 2021 12:34:59 -0500 Subject: [PATCH 38/40] Added rubocop fixes --- .rubocop.yml | 126 ++++++++++++--- lib/resource_registry.rb | 3 +- .../helpers/input_controls.rb | 68 ++++----- .../helpers/view_controls.rb | 144 +++++++++--------- lib/resource_registry/namespace.rb | 4 +- lib/resource_registry/navigation.rb | 76 ++++----- .../operations/configurations/create.rb | 1 - .../operations/features/authorize.rb | 3 +- .../operations/features/configure.rb | 3 +- .../operations/features/create.rb | 2 +- .../operations/features/disable.rb | 3 +- .../operations/features/enable.rb | 3 +- .../operations/features/renew.rb | 27 ++-- .../operations/features/update.rb | 17 +-- .../operations/graphs/create.rb | 3 +- .../operations/namespaces/build.rb | 14 +- .../operations/namespaces/create.rb | 1 - .../operations/namespaces/form.rb | 3 +- .../operations/namespaces/list_features.rb | 8 +- .../operations/namespaces/update_features.rb | 1 + .../operations/registries/create.rb | 6 +- .../operations/registries/load.rb | 5 +- lib/resource_registry/rgl.rb | 7 +- .../serializers/features/serialize.rb | 2 +- .../serializers/namespaces/serialize.rb | 24 +-- .../serializers/yaml/serialize.rb | 1 + lib/resource_registry/stores.rb | 2 +- .../stores/active_record/find.rb | 4 +- .../stores/active_record/persist.rb | 4 +- .../stores/active_record/update.rb | 2 +- .../stores/container/find.rb | 3 +- .../stores/container/read.rb | 3 +- .../stores/container/write.rb | 6 +- .../stores/file/list_path.rb | 2 +- lib/resource_registry/stores/file/read.rb | 8 +- lib/resource_registry/stores/mongoid/find.rb | 4 +- .../stores/mongoid/persist.rb | 20 +-- .../validation/application_contract.rb | 6 +- .../validation/configuration_contract.rb | 6 +- .../validation/namespace_contract.rb | 22 ++- .../validation/setting_contract.rb | 24 +-- .../operations/graphs/create_spec.rb | 6 +- 42 files changed, 374 insertions(+), 303 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index f96b01db..4f700076 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,13 +1,35 @@ # This yaml describes our current checks. # Any checks marked "# TODO: RWC" are "re-enable when corrected.RWC" are "re-enable when corrected. +# We will add the following back in later, but they cause +# a completely outsized amount of cop failures for the number of files: +# - db/seedfiles +# - lib/tasks +AllCops: + TargetRubyVersion: 2.5 + Exclude: + - "./bin/**/*" + - "./project_gems/effective_datatables-2.6.14/effective_datatables-2.6.14.gemspec" + - "./node_modules/**/*" + - "./db/seedfiles/**/*" + - "./lib/tasks/**/*" + - "./script/**/*" + - "./components/benefit_markets/spec/dummy/**/*" + - "./components/benefit_markets/db/seedfiles/**/*" + - "./components/benefit_sponsors/spec/dummy/**/*" + - "./components/benefit_sponsors/db/seedfiles/**/*" + - "./components/notifier/spec/dummy/**/*" + - "./components/old_sponsored_benefits/**/*" + - "./components/sponsored_benefits/spec/dummy/**/*" + - "./components/transport_profiles/spec/dummy/**/*" + - "./components/transport_profiles/spec/dummy/**/*" + - "./eyes/enroll.eye.rb" + +# TODO: RWC Layout/CommentIndentation: - Enabled: true - -Layout/EmptyLines: Enabled: false -Layout/EmptyLineAfterGuardClause: +Layout/EmptyLines: Enabled: false Layout/EmptyLinesAroundBlockBody: @@ -19,9 +41,15 @@ Layout/LeadingCommentSpace: Layout/ExtraSpacing: Enabled: false +Layout/EmptyLineAfterGuardClause: + Enabled: false + Layout/EmptyLinesAroundClassBody: Enabled: false +Layout/FirstArrayElementIndentation: + Enabled: false + # Re-enable once other problems are solved Layout/SpaceAfterComma: Enabled: false @@ -35,10 +63,16 @@ Layout/SpaceInsideHashLiteralBraces: Layout/SpaceInsideBlockBraces: Enabled: false +Layout/TrailingEmptyLines: + Enabled: false + Layout/IndentationWidth: Enabled: true -Layout/Tab: +Layout/LineLength: + Max: 250 + +Layout/IndentationStyle: Enabled: true Layout/TrailingWhitespace: @@ -56,18 +90,12 @@ Metrics/CyclomaticComplexity: Metrics/BlockLength: Enabled: false -Metrics/LineLength: - Max: 250 - Metrics/MethodLength: Max: 50 Metrics/PerceivedComplexity: Max: 15 -Naming/AccessorMethodName: - Enabled: false - Naming/PredicateName: Enabled: false @@ -82,32 +110,32 @@ Style/BlockComments: # We will want to turn this back on or customize it more fully Style/Documentation: - Enabled: false + Enabled: true Style/EachWithObject: Enabled: false -Style/EmptyLiteral: - Enabled: false - -Style/EmptyMethod: - Enabled: false - Style/ExpandPathArguments: Enabled: false Style/HashSyntax: Enabled: false +Style/HashEachMethods: + Enabled: true + +Style/HashTransformKeys: + Enabled: true + +Style/HashTransformValues: + Enabled: true + Style/NumericPredicate: Enabled: false Style/RedundantSelf: Enabled: false -Style/SafeNavigation: - Enabled: false - Style/StringLiterals: Enabled: false @@ -124,4 +152,58 @@ Bundler/OrderedGems: Enabled: false Gemspec/OrderedDependencies: - Enabled: false + Enabled: + false + +Layout/SpaceBeforeBrackets: # (new in 1.7) + Enabled: true +Lint/AmbiguousAssignment: # (new in 1.7) + Enabled: true +Lint/DeprecatedConstants: # (new in 1.8) + Enabled: true +Lint/DuplicateBranch: # (new in 1.3) + Enabled: true +Lint/DuplicateRegexpCharacterClassElement: # (new in 1.1) + Enabled: true +Lint/EmptyBlock: # (new in 1.1) + Enabled: true +Lint/EmptyClass: # (new in 1.3) + Enabled: true +Lint/LambdaWithoutLiteralBlock: # (new in 1.8) + Enabled: true +Lint/NoReturnInBeginEndBlocks: # (new in 1.2) + Enabled: true +Lint/RedundantDirGlobSort: # (new in 1.8) + Enabled: true +Lint/ToEnumArguments: # (new in 1.1) + Enabled: true +Lint/UnexpectedBlockArity: # (new in 1.5) + Enabled: true +Lint/UnmodifiedReduceAccumulator: # (new in 1.1) + Enabled: true +Style/ArgumentsForwarding: # (new in 1.1) + Enabled: true +Style/CollectionCompact: # (new in 1.2) + Enabled: true +Style/DocumentDynamicEvalDefinition: # (new in 1.1) + Enabled: true +Style/EndlessMethod: # (new in 1.8) + Enabled: true +Style/HashExcept: # (new in 1.7) + Enabled: true +Style/NegatedIfElseCondition: # (new in 1.2) + Enabled: true +Style/NilLambda: # (new in 1.3) + Enabled: true +Style/RedundantArgument: # (new in 1.4) + Enabled: true +Style/SwapValues: # (new in 1.1) + Enabled: true + +require: + - ./cops/lint/empty_rescue_clause.rb +# Deprecated +# Style/TrailingBlankLines: +# Enabled: false +# AllCops: +# RunRailsCops: true diff --git a/lib/resource_registry.rb b/lib/resource_registry.rb index 2903f6c9..a0e374eb 100644 --- a/lib/resource_registry.rb +++ b/lib/resource_registry.rb @@ -28,9 +28,8 @@ require 'resource_registry/registry' module ResourceRegistry - def self.logger - @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT) + @@logger ||= defined?(Rails) ? Rails.logger : Logger.new($stdout) end def self.logger=(logger) diff --git a/lib/resource_registry/helpers/input_controls.rb b/lib/resource_registry/helpers/input_controls.rb index 00d1f064..7274417e 100644 --- a/lib/resource_registry/helpers/input_controls.rb +++ b/lib/resource_registry/helpers/input_controls.rb @@ -1,5 +1,7 @@ -module InputControls +# frozen_string_literal: true +# Helper methods to render setting input fields +module InputControls def build_option_field(option, form, attrs = {}) type = option.meta.content_type&.to_sym input_control = case type @@ -47,7 +49,7 @@ def build_option_field(option, form, attrs = {}) end end - def input_filter_control(form, feature, data) + def input_filter_control(_form, feature, data) filter_setting = feature.settings.detect{|s| s.key == :filter_params} filter_params = setting_value(filter_setting) @@ -68,13 +70,13 @@ def input_filter_control(form, feature, data) end end - def feature_enabled_control(option, form) + def feature_enabled_control(option, _form) tag.div(class: "form-group") do content = option.key.to_s.titleize content += tag.label(class: 'switch') do tag.input(type: 'hidden', value: option.key, name: 'feature_key') + - tag.input(type: "checkbox", checked: option.is_enabled) + - tag.span(class: "slider") + tag.input(type: "checkbox", checked: option.is_enabled) + + tag.span(class: "slider") end content += tag.div(class: "spinner-border d-none text-success", role: "status") do @@ -87,13 +89,13 @@ def feature_enabled_control(option, form) end end - def slider_switch_control(option, form) + def slider_switch_control(option, _form) tag.div(class: "form-group") do content = option.key.to_s.titleize content += tag.label(class: 'switch') do tag.input(type: 'hidden', value: option.key, name: 'feature_key') + - tag.input(type: "checkbox", checked: option.item) + - tag.span(class: "slider") + tag.input(type: "checkbox", checked: option.item) + + tag.span(class: "slider") end content.html_safe @@ -115,14 +117,14 @@ def select_control(setting, form) choices.each do |choice| choice = choice.is_a?(Hash) ? choice.first : [choice.to_s, choice.to_s.humanize] - option_list += tag.option(choice[1], selected: (choice[0]== value.to_s), value: choice[0]) + option_list += tag.option(choice[1], selected: (choice[0] == value.to_s), value: choice[0]) end tag.select(option_list, id: id, class: "form-control", name: input_name_for(setting, form)) end def select_dropdown(input_id, list, show_default = false, selected = nil) - name = (input_id.to_s.scan(/supported_languages/).present? ? input_id : 'admin[' + input_id.to_s + ']') + name = (input_id.to_s.scan(/supported_languages/).present? ? input_id : "admin[#{input_id}]") return unless list.is_a? Array content_tag(:select, class: "form-control", id: input_id, name: name, required: true) do @@ -154,7 +156,7 @@ def input_import_control(setting, _form) tag.span('Upload', class: "input-group-text", id: id) end + tag.div(class: "custom-file") do - tag.input(nil, type: "file", id: id, name: id + "[value]", class: "custom-file-input", aria: { describedby: aria_describedby }) + + tag.input(nil, type: "file", id: id, name: "#{id}[value]", class: "custom-file-input", aria: { describedby: aria_describedby }) + tag.label('Choose File', for: id, value: label, class: "custom-file-label") end end @@ -167,7 +169,7 @@ def input_radio_control(setting, form) if setting.is_a?(ResourceRegistry::Setting) element_name = input_name_for(setting, form) else - element_name = form&.object_name.to_s + "[is_enabled]" + element_name = "#{form&.object_name}[is_enabled]" input_value = form.object&.is_enabled input_value = setting.is_enabled if input_value.blank? end @@ -178,7 +180,7 @@ def input_radio_control(setting, form) tag.div(tag.div(tag.input(nil, type: "radio", name: element_name, value: choice.first[0], checked: input_value.to_s == choice.first[0].to_s, required: true), class: "input-group-text"), class: "input-group-prepend") + tag.input(nil, type: "text", placeholder: choice.first[1], class: "form-control", aria: {label: aria_label }) end - end.join('').html_safe + end.join.html_safe end def input_checkbox_control(setting, form) @@ -188,10 +190,11 @@ def input_checkbox_control(setting, form) meta.enum.collect do |choice| choice = send(choice) if choice.is_a?(String) input_group do - tag.div(tag.div(tag.input(nil, type: 'checkbox', name: "#{input_name_for(setting, form)}[]", value: choice.first[0], checked: input_value.include?(choice.first[0].to_s), required: false), class: 'input-group-text'), class: 'input-group-prepend') + + tag.div(tag.div(tag.input(nil, type: 'checkbox', name: "#{input_name_for(setting, form)}[]", value: choice.first[0], checked: input_value.include?(choice.first[0].to_s), required: false), class: 'input-group-text'), + class: 'input-group-prepend') + tag.input(nil, type: 'text', placeholder: choice.first[1], class: 'form-control', aria: {label: aria_label }) end - end.join('').html_safe + end.join.html_safe end def input_file_control(setting, form) @@ -216,15 +219,12 @@ def input_file_control(setting, form) tag.label('Choose File', for: id, value: label, class: "custom-file-label") end - control = - tag.div(class: "col-2") do - preview - end + + tag.div(class: "col-2") do + preview + end + tag.div(class: 'input-group') do control_inputs end - - control end def input_text_control(setting, form, options = {}) @@ -261,7 +261,6 @@ def input_date_control(setting, form) tag.input(nil, type: "date", value: input_value, id: id, name: input_name_for(setting, form), placeholder: "mm/dd/yyyy", class: "form-control", required: is_required) end - def input_date_range_control(setting, form) meta = setting[:meta] @@ -280,11 +279,10 @@ def input_date_range_control(setting, form) to_input_name = form&.object_name.to_s + "[settings][#{setting.key}][end]" tag.input(nil, type: "date", value: date_bounds[0], id: from_input_name, name: from_input_name, placeholder: "mm/dd/yyyy", class: "form-control", required: is_required) + - tag.div(class: 'input-group-addon') { 'to' } + - tag.input(nil, type: "date", value: date_bounds[1], id: to_input_name, name: to_input_name, placeholder: "mm/dd/yyyy", class: "form-control", required: is_required) + tag.div(class: 'input-group-addon') { 'to' } + + tag.input(nil, type: "date", value: date_bounds[1], id: to_input_name, name: to_input_name, placeholder: "mm/dd/yyyy", class: "form-control", required: is_required) end - def input_number_control(setting, form) id = setting[:key].to_s meta = setting[:meta] @@ -328,7 +326,7 @@ def input_swatch_control(setting, form) meta = setting[:meta] color = meta.value || meta.default - tag.input(nil, type: "text", value: color, id: id, name: form&.object_name.to_s + "[value]",class: "js-color-swatch form-control") + + tag.input(nil, type: "text", value: color, id: id, name: "#{form&.object_name}[value]",class: "js-color-swatch form-control") + tag.div(tag.button(type: "button", id: id, class: "btn", value: "", style: "background-color: #{color}"), class: "input-group-append") end @@ -384,7 +382,7 @@ def form_group(setting, control, options = {}) id = setting[:key].to_s # label = setting[:title] || id.titleize label = setting.meta.label || id.titleize - help_id = id + 'HelpBlock' + help_id = "#{id}HelpBlock" # help_text = setting[:description] # aria_label = setting[:aria_label] || "Radio button for following text input" help_text = setting.meta.description @@ -398,12 +396,12 @@ def form_group(setting, control, options = {}) label end end + - tag.div(class: 'col col-sm-12 col-md-1') do - tag.i(class: 'fas fa-info-circle', rel: 'tooltip', title: setting.meta.description) - end + - tag.div(class: 'col col-sm-12 col-md-7') do - input_group { control }# + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) - end + tag.div(class: 'col col-sm-12 col-md-1') do + tag.i(class: 'fas fa-info-circle', rel: 'tooltip', title: setting.meta.description) + end + + tag.div(class: 'col col-sm-12 col-md-7') do + input_group { control } # + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) + end end else tag.label(for: id, value: label, aria: { label: aria_label }) do @@ -418,7 +416,7 @@ def custom_form_group(setting, control) id = setting[:key].to_s # label = setting[:title] || id.titleize label = setting.meta.label || id.titleize - help_id = id + 'HelpBlock' + help_id = "#{id}HelpBlock" help_text = setting.meta.description aria_label = "#{setting.meta.content_type.to_s.humanize} button for following text input" #setting[:aria_label] || "Radio button for following text input" @@ -443,7 +441,7 @@ def build_attribute_field(form, attribute) end def setting_value(setting) - if setting.is_a?(ResourceRegistry::Setting) && setting.item&.is_a?(String) + if setting.is_a?(ResourceRegistry::Setting) && setting.item.is_a?(String) JSON.parse(setting.item) else setting&.item diff --git a/lib/resource_registry/helpers/view_controls.rb b/lib/resource_registry/helpers/view_controls.rb index d5e90176..a30073c4 100644 --- a/lib/resource_registry/helpers/view_controls.rb +++ b/lib/resource_registry/helpers/view_controls.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require_relative 'input_controls' - +# Helper methods to render interface from features/settings module RegistryViewControls include ::InputControls @@ -25,7 +25,7 @@ def render_model_settings(feature, form, registry, options) query_setting = feature.settings.detect{|setting| setting.key == :model_query_params} query_params = setting_value(query_setting) result = @filter_result - result = registry[feature.key]{ query_params || {}}.success unless result + result ||= registry[feature.key]{ query_params || {}}.success filter_setting = feature.settings.detect{|s| s.key == :filter_params} content = '' @@ -45,7 +45,7 @@ def render_model_settings(feature, form, registry, options) def namespace_panel(namespace, feature_registry, options = {}) tag.div(class: 'card') do - tag.div(class: 'card-body') do + tag.div(class: 'card-body') do if namespace.features.any?{|f| f.meta.content_type == :model_attributes} namespace.features.collect{|feature| construct_feature_form(feature, feature_registry, options)}.join(tag.hr(class: 'mt-2 mb-3')).html_safe else @@ -55,7 +55,7 @@ def namespace_panel(namespace, feature_registry, options = {}) end end - def construct_namespace_form(namespace, registry, options) + def construct_namespace_form(namespace, _registry, options) form_for(namespace, as: 'namespace', url: update_namespace_exchanges_configurations_path, method: :post, remote: true, authenticity_token: true) do |form| namespace_content = form.hidden_field(:path, value: namespace.path.map(&:to_s).join('.')) @@ -63,24 +63,24 @@ def construct_namespace_form(namespace, registry, options) namespace_content += form.fields_for :features, feature, {index: index} do |feature_form| tag.div(id: feature.key.to_s, role: 'tabpanel', 'aria-labelledby': "list-#{feature.key}-list", class: 'mt-2') do feature_form.hidden_field(:key) + - render_settings(feature, feature_form, feature_registry, options) + render_settings(feature, feature_form, feature_registry, options) end end end namespace_content += tag.div(class: 'row mt-3') do - tag.div(class: 'col-4') do - form.submit('Save', class: 'btn btn-primary') - end + + tag.div(class: 'col-4') do + form.submit('Save', class: 'btn btn-primary') + end + tag.div(class: 'col-6') do - tag.div(class: 'flash-message', id: namespace.path.map(&:to_s).join('-') + '-alert') + tag.div(class: 'flash-message', id: "#{namespace.path.map(&:to_s).join('-')}-alert") end - end + end namespace_content.html_safe end end - + def feature_panel(feature_key, registry, options = {}) @filter_result = options[:filter_result] @horizontal = true if options[:horizontal] @@ -94,10 +94,10 @@ def feature_panel(feature_key, registry, options = {}) feature_group_display(feature, registry) else if feature.item == 'feature_collection' - setting = feature.settings.detect{|setting| setting.meta&.content_type.to_s == 'feature_list_panel'} - features = setting_value(setting) + list_panel_setting = feature.settings.detect{|setting| setting.meta&.content_type.to_s == 'feature_list_panel'} + features = setting_value(list_panel_setting) end - features.collect{|feature| construct_feature_form(feature, registry, options)}.join(tag.hr(class: 'mt-2 mb-3')).html_safe + features.collect{|f| construct_feature_form(f, registry, options)}.join(tag.hr(class: 'mt-2 mb-3')).html_safe end end end @@ -105,34 +105,32 @@ def feature_panel(feature_key, registry, options = {}) end def construct_feature_form(feature, registry, options) - if options[:action_params] - renew_action = options[:action_params][:action].to_s == 'renew' - end + renew_action = options[:action_params][:action].to_s == 'renew' if options[:action_params] submit_path = if renew_action - renew_feature_exchanges_configuration_path(feature.key) - else - update_feature_exchanges_configuration_path(feature.key) - end + renew_feature_exchanges_configuration_path(feature.key) + else + update_feature_exchanges_configuration_path(feature.key) + end tag.div(id: feature.key.to_s, 'aria-labelledby': "list-#{feature.key}-list", class: 'card border-0') do tag.div(class: 'card-body') do tag.div(class: 'card-title h6 font-weight-bold mb-4') do - feature.meta&.label || feature.key.to_s.titleize + feature.meta&.label || feature.key.to_s.titleize end + - form_for(feature, as: 'feature', url: submit_path, method: :post, remote: true, authenticity_token: true) do |form| - form.hidden_field(:key) + - (renew_action ? hidden_field_tag('feature[target_feature]', options[:action_params][:key]) : '') + - render_settings(feature, form, registry, options) + - tag.div(class: 'row mt-3') do - tag.div(class: 'col-4') do - form.submit('Save', class: 'btn btn-primary') - end + - tag.div(class: 'col-6') do - tag.div(class: 'flash-message', id: feature.key.to_s + '-alert') - end - end - end + form_for(feature, as: 'feature', url: submit_path, method: :post, remote: true, authenticity_token: true) do |form| + form.hidden_field(:key) + + (renew_action ? hidden_field_tag('feature[target_feature]', options[:action_params][:key]) : '') + + render_settings(feature, form, registry, options) + + tag.div(class: 'row mt-3') do + tag.div(class: 'col-4') do + form.submit('Save', class: 'btn btn-primary') + end + + tag.div(class: 'col-6') do + tag.div(class: 'flash-message', id: "#{feature.key}-alert") + end + end + end end end end @@ -148,7 +146,7 @@ def feature_group_display(feature, registry) end end - def feature_group_control(features, registry) + def feature_group_control(features, _registry) features = features.select{|feature| feature.meta.present? && feature.meta.content_type.to_s != 'feature_action' } features.collect do |feature| @@ -158,46 +156,46 @@ def feature_group_control(features, registry) tag.div(class: 'row') do tag.div(class: 'col-md-6') do tag.h4 do - feature.meta&.label || feature.key.to_s.titleize + feature.meta&.label || feature.key.to_s.titleize end end + - tag.div(class: 'col-md-6') do - action_setting = settings_with_meta.detect{|setting| setting.meta.content_type.to_s == 'feature_action'} - if action_setting - form_with(model: feature, url: action_setting.item, method: :get, remote: true, local: false) do |f| - hidden_field_tag('feature[action]', 'renew') + - hidden_field_tag('feature[key]', feature.key) + - f.submit(action_setting.key.to_s.titleize, class: 'btn btn-link') - end.html_safe + tag.div(class: 'col-md-6') do + action_setting = settings_with_meta.detect{|setting| setting.meta.content_type.to_s == 'feature_action'} + if action_setting + form_with(model: feature, url: action_setting.item, method: :get, remote: true, local: false) do |f| + hidden_field_tag('feature[action]', 'renew') + + hidden_field_tag('feature[key]', feature.key) + + f.submit(action_setting.key.to_s.titleize, class: 'btn btn-link') + end.html_safe + end end - end end + - settings_with_meta.collect do |setting| - next if setting.meta.content_type.to_s == 'feature_action' - section_name = setting.meta&.label || setting.key.to_s.titleize - tag.div(class: 'mt-3') do - tag.div(class: 'row') do - tag.div(class: 'col-md-4') do - tag.strong do - section_name - end - end + - tag.div(class: 'col-md-4') do - tag.a(href: "/exchanges/configurations/#{feature.key}/edit", data: {remote: true}) do - tag.span do - "View #{section_name}" + settings_with_meta.collect do |setting| + next if setting.meta.content_type.to_s == 'feature_action' + section_name = setting.meta&.label || setting.key.to_s.titleize + tag.div(class: 'mt-3') do + tag.div(class: 'row') do + tag.div(class: 'col-md-4') do + tag.strong do + section_name + end + end + + tag.div(class: 'col-md-4') do + tag.a(href: "/exchanges/configurations/#{feature.key}/edit", data: {remote: true}) do + tag.span do + "View #{section_name}" + end + end + end + + tag.div(class: 'col-md-6') do + tag.ul(class: 'list-group list-group-flush ml-2') do + feature_list = setting_value(setting) + feature_list.collect{|f| tag.li(class: 'list-group-item'){ f.key.to_s.titleize }}.join.html_safe + end end - end - end + - tag.div(class: 'col-md-6') do - tag.ul(class: 'list-group list-group-flush ml-2') do - feature_list = setting_value(setting) - feature_list.collect{|feature| tag.li(class: 'list-group-item'){ feature.key.to_s.titleize }}.join.html_safe - end end end - end - end.compact.join.html_safe + end.compact.join.html_safe end end.join end @@ -214,10 +212,10 @@ def find_feature(feature_key) def setting_value(setting) value = if setting.is_a?(ResourceRegistry::Setting) - JSON.parse(setting.item) - else - setting.item - end + JSON.parse(setting.item) + else + setting.item + end if value.is_a?(Hash) && value['operation'] elements = value['operation'].split(/\./) diff --git a/lib/resource_registry/namespace.rb b/lib/resource_registry/namespace.rb index a4daa4a2..869000ef 100644 --- a/lib/resource_registry/namespace.rb +++ b/lib/resource_registry/namespace.rb @@ -28,7 +28,7 @@ class Namespace < Dry::Struct # @!attribute [r] meta (optional) # Configuration settings and attributes that support presenting and updating their values in the User Interface # @return [ResourceRegistry::Meta] - attribute :meta, ResourceRegistry::Meta.default(Hash.new.freeze).meta(omittable: true) + attribute :meta, ResourceRegistry::Meta.default({}.freeze).meta(omittable: true) # @!attribute [r] feature_keys (optional) # Key references to the features under this namespace @@ -48,7 +48,7 @@ class Namespace < Dry::Struct def persisted? - false + false end end end \ No newline at end of file diff --git a/lib/resource_registry/navigation.rb b/lib/resource_registry/navigation.rb index 66652827..b91b99ef 100644 --- a/lib/resource_registry/navigation.rb +++ b/lib/resource_registry/navigation.rb @@ -1,46 +1,48 @@ -require 'action_view' +# frozen_string_literal: true +require 'action_view' module ResourceRegistry + # This would render navigation menu from registerd namespaces and features class Navigation include ActionView::Helpers::TagHelper include ActionView::Context TAG_OPTION_DEFAULTS = { - ul: { - options: {class: 'nav flex-column flex-nowrap overflow-auto'} - }, - nested_ul: { - options: {class: 'flex-column nav pl-4'} - }, - li: { - options: {class: 'nav-item'} - }, - a: { - namespace_link: { - options: {class: 'nav-link collapsed text-truncate', 'data-toggle': 'collapse'}, - }, - feature_link: { - options: {class: 'nav-link', 'data-remote': true} - } - } - } + ul: { + options: {class: 'nav flex-column flex-nowrap overflow-auto'} + }, + nested_ul: { + options: {class: 'flex-column nav pl-4'} + }, + li: { + options: {class: 'nav-item'} + }, + a: { + namespace_link: { + options: {class: 'nav-link collapsed text-truncate', 'data-toggle': 'collapse'} + }, + feature_link: { + options: {class: 'nav-link', 'data-remote': true} + } + } + }.freeze AUTHORIZATION_DEFAULTS = { - authorization_defaults: {} - } + authorization_defaults: {} + }.freeze NAMESPACE_OPTION_DEFAULTS = { - include_all_disabled_features: true, - include_no_features_defined: true, - active_item: [:aca_shop, :benefit_market_catalogs, :catalog_2019], # TODO - starting_namespaces: [] # start vertices for graph - } + include_all_disabled_features: true, + include_no_features_defined: true, + active_item: [:aca_shop, :benefit_market_catalogs, :catalog_2019], # TODO + starting_namespaces: [] # start vertices for graph + }.freeze OPTION_DEFAULTS = { - tag_options: TAG_OPTION_DEFAULTS, - authorization_options: AUTHORIZATION_DEFAULTS, - namespace_options: NAMESPACE_OPTION_DEFAULTS - } + tag_options: TAG_OPTION_DEFAULTS, + authorization_options: AUTHORIZATION_DEFAULTS, + namespace_options: NAMESPACE_OPTION_DEFAULTS + }.freeze attr_reader :options, :namespaces @@ -53,7 +55,7 @@ def initialize(registry, options = {}) end def render_html - namespaces.collect{|namespace| to_ul(namespace)}.join('').html_safe + namespaces.collect{|namespace| to_ul(namespace)}.join.html_safe end private @@ -67,10 +69,10 @@ def build_namespaces end def to_namespace(vertex) - namespace_dict = [vertex].inject({}) do |dict, vertex| - dict = vertex.to_h + namespace_dict = [vertex].inject({}) do |dict, record| + dict = record.to_h dict[:features] = dict[:feature_keys].collect{|key| @registry[key].feature.to_h.except(:settings)} - dict[:namespaces] = @graph.adjacent_vertices(vertex).collect{|adjacent_vertex| to_namespace(adjacent_vertex)} + dict[:namespaces] = @graph.adjacent_vertices(record).collect{|adjacent_vertex| to_namespace(adjacent_vertex)} attrs_to_skip = [:feature_keys] attrs_to_skip << :meta if dict[:meta].empty? dict.except(*attrs_to_skip) @@ -102,7 +104,7 @@ def content_to_expand(element) tag.div(class: 'collapse', id: "nav_#{element[:key]}", 'aria-expanded': 'false') do nav_features = element[:features].select{|feature| feature[:meta][:content_type] == :nav} (nav_features + element[:namespaces]).reduce('') do |list, child_ele| - list += to_ul(child_ele, true) + list + to_ul(child_ele, true) end.html_safe end end @@ -117,8 +119,8 @@ def namespace_nav_link(element) end def feature_nav_link(element) - feature_url = element[:item] if element[:item].to_s.match?(/^\/.*$/) - feature_url ||= ('/exchanges/configurations/' + element[:key].to_s + '/edit') + feature_url = element[:item] if element[:item].to_s.match?(%r{^/.*$}) + feature_url ||= "/exchanges/configurations/#{element[:key]}/edit" tag.a(options[:tag_options][:a][:feature_link][:options].merge(href: feature_url)) do tag.span do link_title(element) diff --git a/lib/resource_registry/operations/configurations/create.rb b/lib/resource_registry/operations/configurations/create.rb index 63fc1dd6..865c1095 100644 --- a/lib/resource_registry/operations/configurations/create.rb +++ b/lib/resource_registry/operations/configurations/create.rb @@ -3,7 +3,6 @@ module ResourceRegistry module Operations module Configurations - # Create a Configuration class Create send(:include, Dry::Monads[:result, :do]) diff --git a/lib/resource_registry/operations/features/authorize.rb b/lib/resource_registry/operations/features/authorize.rb index b21a1366..91178a21 100644 --- a/lib/resource_registry/operations/features/authorize.rb +++ b/lib/resource_registry/operations/features/authorize.rb @@ -20,8 +20,7 @@ def call(_account, _domain) private - def verify(params) - end + def verify(params); end end end diff --git a/lib/resource_registry/operations/features/configure.rb b/lib/resource_registry/operations/features/configure.rb index 8600bb4e..a57ec9db 100644 --- a/lib/resource_registry/operations/features/configure.rb +++ b/lib/resource_registry/operations/features/configure.rb @@ -6,11 +6,10 @@ module Features class Configure send(:include, Dry::Monads[:result, :do]) - def call(params) + def call(_params) yield configuration end - private end end diff --git a/lib/resource_registry/operations/features/create.rb b/lib/resource_registry/operations/features/create.rb index e379171e..02baeafe 100644 --- a/lib/resource_registry/operations/features/create.rb +++ b/lib/resource_registry/operations/features/create.rb @@ -19,7 +19,7 @@ def call(params) def construct(params) if params[:namespace_path].blank? - params['namespace_path'] = params['namespace'].is_a?(Hash) ? params.delete('namespace') : {path: params.delete('namespace')} + params['namespace_path'] = params['namespace'].is_a?(Hash) ? params.delete('namespace') : {path: params.delete('namespace')} end Success(params) diff --git a/lib/resource_registry/operations/features/disable.rb b/lib/resource_registry/operations/features/disable.rb index 00eeb2c2..766b3b83 100644 --- a/lib/resource_registry/operations/features/disable.rb +++ b/lib/resource_registry/operations/features/disable.rb @@ -13,8 +13,7 @@ class Disable def call(name:, options: {}) feature(name).disable(args) end - end end end -end +end \ No newline at end of file diff --git a/lib/resource_registry/operations/features/enable.rb b/lib/resource_registry/operations/features/enable.rb index 2968e9c6..e1726b55 100644 --- a/lib/resource_registry/operations/features/enable.rb +++ b/lib/resource_registry/operations/features/enable.rb @@ -13,8 +13,7 @@ class Enable def call(name:, options: {}) feature(name).enable(args) end - end end end -end +end \ No newline at end of file diff --git a/lib/resource_registry/operations/features/renew.rb b/lib/resource_registry/operations/features/renew.rb index fb097c63..f30ae6ec 100644 --- a/lib/resource_registry/operations/features/renew.rb +++ b/lib/resource_registry/operations/features/renew.rb @@ -13,7 +13,7 @@ def call(params:, registry:) options = yield extract_options(params, registry) params = yield construct_params(options) features = yield create(params) - registry = yield persist(features, registry) + yield persist(features, registry) Success(features) end @@ -47,13 +47,13 @@ def construct_params(options) end def create(feature_hashes) - Try { + Try do feature_hashes.collect do |feature_hash| result = ResourceRegistry::Operations::Features::Create.new.call(feature_hash) return result if result.failure? result.value! end - }.to_result + end.to_result end def persist(features, registry) @@ -66,22 +66,23 @@ def persist(features, registry) end def get_features(item) - return item unless (item.is_a?(Hash) && item['operation'].present?) + return item unless item.is_a?(Hash) && item['operation'].present? elements = item['operation'].split(/\./) elements[0].constantize.send(elements[1]).call(item['params'].symbolize_keys).success end def serialize_hash(attributes) attributes.reduce({}) do |values, (key, value)| - values[key] = if value.is_a?(Hash) - serialize_hash(value) - elsif value.is_a?(Array) - value.collect do |element| - element.is_a?(Hash) ? serialize_hash(element) : serialize_text(element) - end - else - serialize_text(value) - end + values[key] = case value + when Hash + serialize_hash(value) + when Array + value.collect do |element| + element.is_a?(Hash) ? serialize_hash(element) : serialize_text(element) + end + else + serialize_text(value) + end values end diff --git a/lib/resource_registry/operations/features/update.rb b/lib/resource_registry/operations/features/update.rb index 568b23c0..706da552 100644 --- a/lib/resource_registry/operations/features/update.rb +++ b/lib/resource_registry/operations/features/update.rb @@ -11,8 +11,8 @@ def call(params) feature_params = yield build_params(params[:feature].to_h, params[:registry]) feature_values = yield validate(feature_params, params[:registry]) entity = yield create_entity(feature_values) - feature = yield save_record(entity, params[:registry], params[:filter]) if defined? Rails - updated_feature = yield update_registry(entity, params[:registry]) + yield save_record(entity, params[:registry], params[:filter]) if defined? Rails + yield update_registry(entity, params[:registry]) Success(entity) end @@ -49,15 +49,14 @@ def validate(feature_params, registry) if date_range_settings.present? feature_params[:settings].each do |setting_hash| - if date_range_setting = date_range_settings.detect{|s| s.key == setting_hash[:key].to_sym} - expected_year = date_range_setting.item.min.year + next unless (date_range_setting = date_range_settings.detect{|s| s.key == setting_hash[:key].to_sym}) + expected_year = date_range_setting.item.min.year - date_begin = Date.strptime(setting_hash[:item][:begin], "%Y-%m-%d") - date_end = Date.strptime(setting_hash[:item][:end], "%Y-%m-%d") + date_begin = Date.strptime(setting_hash[:item][:begin], "%Y-%m-%d") + date_end = Date.strptime(setting_hash[:item][:end], "%Y-%m-%d") - return Failure("#{setting_hash[:key].to_s.humanize} should be with in calender year.") unless (date_begin.year == expected_year && date_end.year == expected_year) - return Failure("#{setting_hash[:key].to_s.humanize} invalid date range selected.") unless date_end > date_begin - end + return Failure("#{setting_hash[:key].to_s.humanize} should be with in calender year.") unless date_begin.year == expected_year && date_end.year == expected_year + return Failure("#{setting_hash[:key].to_s.humanize} invalid date range selected.") unless date_end > date_begin end end diff --git a/lib/resource_registry/operations/graphs/create.rb b/lib/resource_registry/operations/graphs/create.rb index e23bb4f3..6b2120ef 100644 --- a/lib/resource_registry/operations/graphs/create.rb +++ b/lib/resource_registry/operations/graphs/create.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'dry/monads' require 'rgl/adjacency' require 'rgl/implicit' @@ -28,7 +29,7 @@ def create(namespaces, registry) namespace_vertices = namespace_to_vertices(namespace) namespace_vertices.each_index do |index| if namespace_vertices[index + 1].present? - graph.add_edge(namespace_vertices[index], namespace_vertices[index+1]) + graph.add_edge(namespace_vertices[index], namespace_vertices[index + 1]) else graph.add_vertex(namespace_vertices[index]) end diff --git a/lib/resource_registry/operations/namespaces/build.rb b/lib/resource_registry/operations/namespaces/build.rb index 178b3558..00a7a849 100644 --- a/lib/resource_registry/operations/namespaces/build.rb +++ b/lib/resource_registry/operations/namespaces/build.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true + require 'dry/monads' module ResourceRegistry module Operations module Namespaces + # This would construct namespace hash from feature when feature meta, namespace content_type validations are successful class Build send(:include, Dry::Monads[:result, :do]) @@ -31,13 +33,13 @@ def validate(feature, content_types) def build(feature) namespace_path = feature.namespace_path - + Success({ - key: namespace_path.path.map(&:to_s).join('_'), - path: namespace_path.path, - feature_keys: [feature.key], - meta: namespace_path.meta.to_h - }) + key: namespace_path.path.map(&:to_s).join('_'), + path: namespace_path.path, + feature_keys: [feature.key], + meta: namespace_path.meta.to_h + }) end end end diff --git a/lib/resource_registry/operations/namespaces/create.rb b/lib/resource_registry/operations/namespaces/create.rb index 666c8a12..0c066d6f 100644 --- a/lib/resource_registry/operations/namespaces/create.rb +++ b/lib/resource_registry/operations/namespaces/create.rb @@ -3,7 +3,6 @@ module ResourceRegistry module Operations module Namespaces - # Create a Namespace class Create send(:include, Dry::Monads[:result, :do]) diff --git a/lib/resource_registry/operations/namespaces/form.rb b/lib/resource_registry/operations/namespaces/form.rb index 457b7b11..c5e56cf0 100644 --- a/lib/resource_registry/operations/namespaces/form.rb +++ b/lib/resource_registry/operations/namespaces/form.rb @@ -3,7 +3,6 @@ module ResourceRegistry module Operations module Namespaces - # Create a Namespace class Form send(:include, Dry::Monads[:result, :do, :try]) @@ -22,7 +21,7 @@ def call(namespace:, registry:) def find_features(namespace, registry) feature_keys = registry[:feature_graph].vertices.detect{|v| v.path == namespace}.feature_keys features = feature_keys.collect{|feature_key| find_feature(feature_key, registry)} - features_for_display = features.select{|feature| feature[:meta][:content_type] != :nav } + features_for_display = features.reject{|feature| feature[:meta][:content_type] == :nav } Success(features_for_display) end diff --git a/lib/resource_registry/operations/namespaces/list_features.rb b/lib/resource_registry/operations/namespaces/list_features.rb index fca708fb..76ab9be3 100644 --- a/lib/resource_registry/operations/namespaces/list_features.rb +++ b/lib/resource_registry/operations/namespaces/list_features.rb @@ -25,10 +25,10 @@ def validate(params) return Failure("Unable to find namespace #{params[:namespace]} under #{params[:registry]}.") unless registry.namespaces.include?(params[:namespace]) Success({ - namespace: params[:namespace], - registry: registry, - order: params[:order] - }) + namespace: params[:namespace], + registry: registry, + order: params[:order] + }) end def list_features(values) diff --git a/lib/resource_registry/operations/namespaces/update_features.rb b/lib/resource_registry/operations/namespaces/update_features.rb index 78cd5d0d..25b23c1b 100644 --- a/lib/resource_registry/operations/namespaces/update_features.rb +++ b/lib/resource_registry/operations/namespaces/update_features.rb @@ -3,6 +3,7 @@ module ResourceRegistry module Operations module Namespaces + # Update features with incoming parameters class UpdateFeatures send(:include, Dry::Monads[:result, :do]) diff --git a/lib/resource_registry/operations/registries/create.rb b/lib/resource_registry/operations/registries/create.rb index 06ee5476..ed2083b1 100644 --- a/lib/resource_registry/operations/registries/create.rb +++ b/lib/resource_registry/operations/registries/create.rb @@ -12,7 +12,7 @@ def call(path:, registry:) params = yield deserialize(file_io) feature_hashes = yield serialize(params) features = yield create(feature_hashes) - registry = yield persist(features, registry) + yield persist(features, registry) Success(features) end @@ -50,13 +50,13 @@ def serialize(params) end def create(feature_hashes) - Try { + Try do feature_hashes.collect do |feature_hash| result = ResourceRegistry::Operations::Features::Create.new.call(feature_hash) return result if result.failure? result.value! end - }.to_result + end.to_result end def persist(features, registry) diff --git a/lib/resource_registry/operations/registries/load.rb b/lib/resource_registry/operations/registries/load.rb index 054f8694..279e2792 100644 --- a/lib/resource_registry/operations/registries/load.rb +++ b/lib/resource_registry/operations/registries/load.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'dry/monads' module ResourceRegistry @@ -27,14 +28,14 @@ def list_paths(registry) end def load_features(paths, registry) - Try { + Try do paths = paths.value! paths.reduce([]) do |features, path| result = ResourceRegistry::Operations::Registries::Create.new.call(path: path, registry: registry) features << result.success if result.success? features end.flatten - }.to_result + end.to_result end def serialize_namespaces(features) diff --git a/lib/resource_registry/rgl.rb b/lib/resource_registry/rgl.rb index c9181eeb..25f0d296 100644 --- a/lib/resource_registry/rgl.rb +++ b/lib/resource_registry/rgl.rb @@ -1,4 +1,7 @@ +# frozen_string_literal: true + module RGL + # Extend RGL library with useful methods class DirectedAdjacencyGraph def forest @@ -11,11 +14,11 @@ def trees def trees_with_features edges_with_features = self.edges.select{|edge| edge.to_a.any?{|ele| ele.feature_keys.present?}} - edges_with_features.collect{|edge| edge.source}.uniq - edges_with_features.collect{|edge| edge.target}.uniq + edges_with_features.collect(&:source).uniq - edges_with_features.collect(&:target).uniq end def vertices_for(options) - return options[:starting_namespaces]if options[:starting_namespaces].present? + return options[:starting_namespaces] if options[:starting_namespaces].present? return trees_with_features unless options[:include_no_features_defined] trees end diff --git a/lib/resource_registry/serializers/features/serialize.rb b/lib/resource_registry/serializers/features/serialize.rb index a78633a5..09826dfd 100644 --- a/lib/resource_registry/serializers/features/serialize.rb +++ b/lib/resource_registry/serializers/features/serialize.rb @@ -21,7 +21,7 @@ def transform(params) return Success([]) if params.empty? || params['registry'].blank? features = params['registry'].reduce([]) do |features_list, namespace| - next unless namespace['features'] + next features_list unless namespace['features'] path = namespace['namespace'] if namespace.key?('namespace') namespace_features = namespace['features'].reduce([]) do |ns_features_list, feature_hash| diff --git a/lib/resource_registry/serializers/namespaces/serialize.rb b/lib/resource_registry/serializers/namespaces/serialize.rb index c1e76145..d38f188c 100644 --- a/lib/resource_registry/serializers/namespaces/serialize.rb +++ b/lib/resource_registry/serializers/namespaces/serialize.rb @@ -19,25 +19,25 @@ def call(features:, namespace_types:) private def build(features, namespace_types) - Try { + Try do features.collect do |feature| namespace = ResourceRegistry::Operations::Namespaces::Build.new.call(feature, namespace_types) namespace.success if namespace.success? end.compact - }.to_result + end.to_result end def merge(namespaces) - Try { - namespaces.reduce({}) do |namespaces, ns| - if namespaces[ns[:key]] - namespaces[ns[:key]][:feature_keys] += ns[:feature_keys] - else - namespaces[ns[:key]] = ns - end - namespaces - end - }.to_result + Try do + namespaces.reduce({}) do |data, ns| + if data[ns[:key]] + data[ns[:key]][:feature_keys] += ns[:feature_keys] + else + data[ns[:key]] = ns + end + data + end + end.to_result end end end diff --git a/lib/resource_registry/serializers/yaml/serialize.rb b/lib/resource_registry/serializers/yaml/serialize.rb index e6e5506b..da4441d1 100644 --- a/lib/resource_registry/serializers/yaml/serialize.rb +++ b/lib/resource_registry/serializers/yaml/serialize.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'yaml' module ResourceRegistry diff --git a/lib/resource_registry/stores.rb b/lib/resource_registry/stores.rb index 81d4110c..52fd9350 100644 --- a/lib/resource_registry/stores.rb +++ b/lib/resource_registry/stores.rb @@ -8,8 +8,8 @@ require_relative 'stores/active_record/find' require_relative 'stores/active_record/update' - module ResourceRegistry + # This module will provide interface for DB stores module Stores class << self diff --git a/lib/resource_registry/stores/active_record/find.rb b/lib/resource_registry/stores/active_record/find.rb index a2b18593..e7f4a3c5 100644 --- a/lib/resource_registry/stores/active_record/find.rb +++ b/lib/resource_registry/stores/active_record/find.rb @@ -11,7 +11,7 @@ class Find # @return [Dry::Container] A non-finalized Dry::Container with associated configuration values wrapped in Dry::Monads::Result def call(key) feature = yield find(key) - + Success(feature) end @@ -19,7 +19,7 @@ def call(key) def find(key) feature = ResourceRegistry::ActiveRecord::Feature.where(key: key).first - + Success(feature) end end diff --git a/lib/resource_registry/stores/active_record/persist.rb b/lib/resource_registry/stores/active_record/persist.rb index 5ae1f340..6422b4ef 100644 --- a/lib/resource_registry/stores/active_record/persist.rb +++ b/lib/resource_registry/stores/active_record/persist.rb @@ -17,10 +17,10 @@ def call(feature_entity) private - def persist(feature_entity) + def persist(_feature_entity) # if registry.db_connection&.table_exists?(:resource_registry_features) feature = ResourceRegistry::ActiveRecord::Feature.where(key: feature.key).first - feature = ResourceRegistry::ActiveRecord::Feature.new(feature.to_h).save unless feature + feature ||= ResourceRegistry::ActiveRecord::Feature.new(feature.to_h).save # else # result = ResourceRegistry::Operations::Features::Create.new.call(feature_record.to_h) # feature = result.success if result.success? # TODO: Verify Failure Scenario diff --git a/lib/resource_registry/stores/active_record/update.rb b/lib/resource_registry/stores/active_record/update.rb index 167b54f9..b4a9fbdc 100644 --- a/lib/resource_registry/stores/active_record/update.rb +++ b/lib/resource_registry/stores/active_record/update.rb @@ -22,7 +22,7 @@ def update(feature_entity) feature.is_enabled = feature_entity.is_enabled feature_entity.settings.each do |setting_entity| - if setting = feature.settings.detect{|setting| setting.key == setting_entity.key} + if (setting = feature.settings.detect{|s| s.key == setting_entity.key}) setting.item = setting_entity.item else feature.settings.build(setting_entity.to_h) diff --git a/lib/resource_registry/stores/container/find.rb b/lib/resource_registry/stores/container/find.rb index 3f1b374f..54b9f89e 100644 --- a/lib/resource_registry/stores/container/find.rb +++ b/lib/resource_registry/stores/container/find.rb @@ -6,8 +6,7 @@ module Container class Find send(:include, Dry::Monads[:result, :do]) - def call(params = {}) - end + def call(params = {}); end end end diff --git a/lib/resource_registry/stores/container/read.rb b/lib/resource_registry/stores/container/read.rb index b766437f..bf2e7ebe 100644 --- a/lib/resource_registry/stores/container/read.rb +++ b/lib/resource_registry/stores/container/read.rb @@ -6,8 +6,7 @@ module Container class Read send(:include, Dry::Monads[:result, :do]) - def call(params = {}) - end + def call(params = {}); end end end diff --git a/lib/resource_registry/stores/container/write.rb b/lib/resource_registry/stores/container/write.rb index e1ebf69c..2cbd2a12 100644 --- a/lib/resource_registry/stores/container/write.rb +++ b/lib/resource_registry/stores/container/write.rb @@ -17,11 +17,9 @@ def call(container, keys) private - def find_or_create_namespace(namespace) - end + def find_or_create_namespace(namespace); end - def write(container, keys) - end + def write(container, keys); end end end diff --git a/lib/resource_registry/stores/file/list_path.rb b/lib/resource_registry/stores/file/list_path.rb index 649f5a63..c7a1e156 100644 --- a/lib/resource_registry/stores/file/list_path.rb +++ b/lib/resource_registry/stores/file/list_path.rb @@ -8,7 +8,7 @@ class ListPath send(:include, Dry::Monads[:result, :do]) def call(dir) - paths = ::Dir[::File.join(dir, '**', '*') ].reject { |p| ::File.directory? p } + paths = ::Dir[::File.join(dir, '**', '*')].reject { |p| ::File.directory? p } Success(paths) end end diff --git a/lib/resource_registry/stores/file/read.rb b/lib/resource_registry/stores/file/read.rb index 64d22f93..6762f474 100644 --- a/lib/resource_registry/stores/file/read.rb +++ b/lib/resource_registry/stores/file/read.rb @@ -17,11 +17,9 @@ def call(params) private def load(params) - begin - Success(::File.read(params.to_s)) - rescue Errno::ENOENT - Failure["No such file or directory", params: params] - end + Success(::File.read(params.to_s)) + rescue Errno::ENOENT + Failure["No such file or directory", params: params] end end diff --git a/lib/resource_registry/stores/mongoid/find.rb b/lib/resource_registry/stores/mongoid/find.rb index 58dbf9b9..e881fd5b 100644 --- a/lib/resource_registry/stores/mongoid/find.rb +++ b/lib/resource_registry/stores/mongoid/find.rb @@ -11,7 +11,7 @@ class Find # @return [Dry::Container] A non-finalized Dry::Container with associated configuration values wrapped in Dry::Monads::Result def call(key) feature = yield find(key) - + Success(feature) end @@ -19,7 +19,7 @@ def call(key) def find(key) feature = ResourceRegistry::Mongoid::Feature.where(key: key).first - + Success(feature) end end diff --git a/lib/resource_registry/stores/mongoid/persist.rb b/lib/resource_registry/stores/mongoid/persist.rb index 665ad46a..449c6a76 100644 --- a/lib/resource_registry/stores/mongoid/persist.rb +++ b/lib/resource_registry/stores/mongoid/persist.rb @@ -13,7 +13,7 @@ class Persist def call(entity, registry, params = {}) record = yield find(entity, registry, params[:filter]) record = yield persist(entity, record) - + Success(record) end @@ -21,10 +21,10 @@ def call(entity, registry, params = {}) def find(entity, registry, filter_params = nil) record = if filter_params - registry[entity.key]{ filter_params }.success[:record] - else - ResourceRegistry::Mongoid::Feature.where(key: entity.key).first - end + registry[entity.key]{ filter_params }.success[:record] + else + ResourceRegistry::Mongoid::Feature.where(key: entity.key).first + end Success(record) end @@ -38,17 +38,17 @@ def persist(entity, record) end def create(entity) - Try { + Try do ResourceRegistry::Mongoid::Feature.new(entity.to_h).save - }.to_result + end.to_result end def update(entity, record) - Try { + Try do if record.class.to_s.match?(/ResourceRegistry/) record.is_enabled = entity.is_enabled entity.settings.each do |setting_entity| - if setting = record.settings.detect{|setting| setting.key == setting_entity.key} + if (setting = record.settings.detect{|s| s.key == setting_entity.key}) setting.item = setting_entity.item else record.settings.build(setting_entity.to_h) @@ -63,7 +63,7 @@ def update(entity, record) end record.save(validate: false) record - }.to_result + end.to_result end end end diff --git a/lib/resource_registry/validation/application_contract.rb b/lib/resource_registry/validation/application_contract.rb index e78bece4..81ed7729 100644 --- a/lib/resource_registry/validation/application_contract.rb +++ b/lib/resource_registry/validation/application_contract.rb @@ -24,7 +24,7 @@ def apply_contract_for(evaluator) contract_klass = create_contract_klass(rule_keys) result = contract_klass.new.call(evaluator.value) - (result && result.failure?) ? { text: "invalid #{rule_keys[0]}", error: result.errors.to_h } : {} + result&.failure? ? { text: "invalid #{rule_keys[0]}", error: result.errors.to_h } : {} end # Construct a fully namespaced constant for contract based on naming conventions @@ -34,7 +34,7 @@ def create_contract_klass(rule_keys) module_name = klass_parts.reduce([]) { |memo, word| memo << word.capitalize }.join klass_name = module_name.chomp('s') - full_klass_name = ["ResourceRegistry", module_name, "Validation", klass_name + "Contract"].join('::') + full_klass_name = ["ResourceRegistry", module_name, "Validation", "#{klass_name}Contract"].join('::') ::Kernel.const_get(full_klass_name) end @@ -48,7 +48,7 @@ def create_contract_klass(rule_keys) if result.failure? # Use dry-validation metadata error form to pass error hash along with text to calling service - key.failure(text: "invalid settings", error: result.errors.to_h) if result && result.failure? + key.failure(text: "invalid settings", error: result.errors.to_h) if result&.failure? else results << result.to_h end diff --git a/lib/resource_registry/validation/configuration_contract.rb b/lib/resource_registry/validation/configuration_contract.rb index 9e7ad8c0..dc646c3f 100644 --- a/lib/resource_registry/validation/configuration_contract.rb +++ b/lib/resource_registry/validation/configuration_contract.rb @@ -39,12 +39,10 @@ class ConfigurationContract < ResourceRegistry::Validation::ApplicationContract end # rubocop:disable Style/RescueModifier - + # Verifies the Pathname exists rule(:root) do - if key? && value - value.realpath rescue key.failure("pathname not found: #{value}") - end + value.realpath rescue key.failure("pathname not found: #{value}") if key? && value end # rubocop:enable Style/RescueModifier diff --git a/lib/resource_registry/validation/namespace_contract.rb b/lib/resource_registry/validation/namespace_contract.rb index 987ca25a..85470f34 100644 --- a/lib/resource_registry/validation/namespace_contract.rb +++ b/lib/resource_registry/validation/namespace_contract.rb @@ -25,27 +25,25 @@ class NamespaceContract < ResourceRegistry::Validation::ApplicationContract end rule(:features) do - if key? && value - if value.none?{|feature| feature.is_a?(ResourceRegistry::Feature)} - feature_results = value.inject([]) do |results, feature_hash| - result = ResourceRegistry::Validation::FeatureContract.new.call(feature_hash) + if key? && value && value.none?{|feature| feature.is_a?(ResourceRegistry::Feature)} + feature_results = value.inject([]) do |results, feature_hash| + result = ResourceRegistry::Validation::FeatureContract.new.call(feature_hash) - if result.failure? - key.failure(text: "invalid feature", error: result.errors.to_h) if result && result.failure? - else - results << result.to_h - end + if result.failure? + key.failure(text: "invalid feature", error: result.errors.to_h) if result&.failure? + else + results << result.to_h end - - values.merge!(features: feature_results) end + + values.merge!(features: feature_results) end end rule(:namespaces).each do if key? && value result = ResourceRegistry::Validation::NamespaceContract.new.call(value) - key.failure(text: "invalid namespace", error: result.errors.to_h) if result && result.failure? + key.failure(text: "invalid namespace", error: result.errors.to_h) if result&.failure? end end end diff --git a/lib/resource_registry/validation/setting_contract.rb b/lib/resource_registry/validation/setting_contract.rb index d65d2d61..7431ff96 100644 --- a/lib/resource_registry/validation/setting_contract.rb +++ b/lib/resource_registry/validation/setting_contract.rb @@ -21,17 +21,17 @@ class SettingContract < ResourceRegistry::Validation::ApplicationContract before(:value_coercer) do |setting| item = if setting[:meta] && setting[:meta][:content_type] == :duration - Types::Duration[setting[:item]] - elsif setting[:item].is_a? String - dates = setting[:item].scan(/(\d{4}\-\d{2}\-\d{2})\.\.(\d{4}\-\d{2}\-\d{2})/).flatten - if dates.present? - dates = dates.collect{|str| Date.strptime(str, "%Y-%m-%d") } - Range.new(*dates) - end - elsif setting[:item].is_a?(Hash) && setting[:item][:begin] && setting[:item][:end] - dates = [setting[:item][:begin], setting[:item][:end]].collect{|str| Date.strptime(str, "%Y-%m-%d") } - Range.new(*dates) - end + Types::Duration[setting[:item]] + elsif setting[:item].is_a? String + dates = setting[:item].scan(/(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})/).flatten + if dates.present? + dates = dates.collect{|str| Date.strptime(str, "%Y-%m-%d") } + Range.new(*dates) + end + elsif setting[:item].is_a?(Hash) && setting[:item][:begin] && setting[:item][:end] + dates = [setting[:item][:begin], setting[:item][:end]].collect{|str| Date.strptime(str, "%Y-%m-%d") } + Range.new(*dates) + end setting.to_h.merge(item: item) if item end @@ -41,7 +41,7 @@ class SettingContract < ResourceRegistry::Validation::ApplicationContract if key? && value result = ResourceRegistry::Validation::MetaContract.new.call(value) # Use dry-validation metadata error form to pass error hash along with text to calling service - key.failure(text: "invalid meta", error: result.errors.to_h) if result && result.failure? + key.failure(text: "invalid meta", error: result.errors.to_h) if result&.failure? end end end diff --git a/spec/resource_registry/operations/graphs/create_spec.rb b/spec/resource_registry/operations/graphs/create_spec.rb index e70e7775..28eec448 100644 --- a/spec/resource_registry/operations/graphs/create_spec.rb +++ b/spec/resource_registry/operations/graphs/create_spec.rb @@ -31,9 +31,9 @@ graph = subject.success namespace_paths = matched_features.collect{|feature| feature.namespace_path.path}.uniq - namespace_paths.inject([]){|vertex_paths, path| vertex_paths += path_to_vertex_path(path)}.uniq.each do |vertex_path| - vertex = graph.vertices.detect{|vertex| vertex.path == vertex_path} - expect(vertex).to be_present + namespace_paths.inject([]){|vertex_paths, path| vertex_paths + path_to_vertex_path(path)}.uniq.each do |vertex_path| + matched_vertex = graph.vertices.detect{|vertex| vertex.path == vertex_path} + expect(matched_vertex).to be_present end end end From bb9db100b01aba11aa03bd6770d6857ef72064c6 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Wed, 17 Feb 2021 16:06:22 -0500 Subject: [PATCH 39/40] Fixed rubocop errors --- .rubocop.yml | 5 +- cops/lint/empty_rescue_clause.rb | 19 ++ lib/resource_registry.rb | 4 +- .../helpers/date_controls.rb | 39 +++ .../helpers/form_group_controls.rb | 119 +++++++++ .../helpers/input_controls.rb | 228 +++--------------- .../helpers/view_controls.rb | 89 +++---- lib/resource_registry/navigation.rb | 8 +- 8 files changed, 266 insertions(+), 245 deletions(-) create mode 100644 cops/lint/empty_rescue_clause.rb create mode 100644 lib/resource_registry/helpers/date_controls.rb create mode 100644 lib/resource_registry/helpers/form_group_controls.rb diff --git a/.rubocop.yml b/.rubocop.yml index 4f700076..a158936b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -84,8 +84,11 @@ Metrics/AbcSize: Metrics/ClassLength: Max: 300 +Metrics/ModuleLength: + Max: 300 + Metrics/CyclomaticComplexity: - Max: 8 + Max: 11 Metrics/BlockLength: Enabled: false diff --git a/cops/lint/empty_rescue_clause.rb b/cops/lint/empty_rescue_clause.rb new file mode 100644 index 00000000..1a805bbf --- /dev/null +++ b/cops/lint/empty_rescue_clause.rb @@ -0,0 +1,19 @@ +module RuboCop + module Cop + module Lint + class EmptyRescueClause < Cop + include RescueNode + + def_node_matcher :rescue_with_empty_body?, <<-PATTERN + (resbody _ _ {nil? (nil) (array) (hash)}) + PATTERN + + def on_resbody(node) + rescue_with_empty_body?(node) do |_error| + add_offense(node, location: node.source_range, message: 'Avoid empty `rescue` bodies.') + end + end + end + end + end +end diff --git a/lib/resource_registry.rb b/lib/resource_registry.rb index a0e374eb..c62d8a16 100644 --- a/lib/resource_registry.rb +++ b/lib/resource_registry.rb @@ -29,10 +29,10 @@ module ResourceRegistry def self.logger - @@logger ||= defined?(Rails) ? Rails.logger : Logger.new($stdout) + @logger ||= defined?(Rails) ? Rails.logger : Logger.new($stdout) end def self.logger=(logger) - @@logger = logger + @logger = logger end end diff --git a/lib/resource_registry/helpers/date_controls.rb b/lib/resource_registry/helpers/date_controls.rb new file mode 100644 index 00000000..ef291f24 --- /dev/null +++ b/lib/resource_registry/helpers/date_controls.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# Helper methods to render setting date fields +module DateControls + + def input_date_control(setting, form) + id = setting[:key].to_s + + date_value = value_for(setting, form) + date_value = date_value.to_date if date_value.is_a?(Time) + date_value = date_value.to_s(:db) if date_value.is_a?(Date) + + meta = setting[:meta] + input_value = date_value || setting.item || meta&.default + is_required = meta[:is_required] == false ? meta[:is_required] : true + + tag.input(nil, type: "date", value: input_value, id: id, name: input_name_for(setting, form), placeholder: "mm/dd/yyyy", class: "form-control", required: is_required) + end + + def input_date_range_control(setting, form) + meta = setting[:meta] + date_bounds = setting.item.split('..').collect do |date_str| + if date_str.match?(/\d{4}-\d{2}-\d{2}/) + date_str + else + date = Date.strptime(date_str, "%m/%d/%Y") + date.to_s(:db) + end + end + + is_required = meta[:is_required] == false ? meta[:is_required] : true + from_input_name = form&.object_name.to_s + "[settings][#{setting.key}][begin]" + to_input_name = form&.object_name.to_s + "[settings][#{setting.key}][end]" + + tag.input(nil, type: "date", value: date_bounds[0], id: from_input_name, name: from_input_name, placeholder: "mm/dd/yyyy", class: "form-control", required: is_required) + + tag.div(class: 'input-group-addon') { 'to' } + + tag.input(nil, type: "date", value: date_bounds[1], id: to_input_name, name: to_input_name, placeholder: "mm/dd/yyyy", class: "form-control", required: is_required) + end +end \ No newline at end of file diff --git a/lib/resource_registry/helpers/form_group_controls.rb b/lib/resource_registry/helpers/form_group_controls.rb new file mode 100644 index 00000000..07370074 --- /dev/null +++ b/lib/resource_registry/helpers/form_group_controls.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require_relative 'input_controls' +require_relative 'date_controls' +# Helper methods to render interface from features/settings +module FormGroupControls + include ::InputControls + include ::DateControls + + def build_option_field(option, form, attrs = {}) + type = option.meta.content_type&.to_sym + input_control = case type + when :swatch + input_swatch_control(option, form) + when :base_64 + input_file_control(option, form) + when :radio_select + input_radio_control(option, form) + when :checkbox_select + input_checkbox_control(option, form) + when :select + select_control(option, form) + when :number + input_number_control(option, form) + when :email + input_email_control(option, form) + when :date + input_date_control(option, form) + when :date_range + input_date_range_control(option, form) + when :currency + input_currency_control(option, form) + when :feature_enabled + feature_enabled_control(option, form) + when :slider_switch + slider_switch_control(option, form) + else + input_text_control(option, form, attrs) + end + # else :text_field + # input_text_control(option, form) + # else + # # :dan_check_box + # # find dan_check_box_control helper + # # else + # # custom_helper #catch_all for custom types + # end + + if [:radio_select, :checkbox_select].include?(type) + custom_form_group(option, input_control) + else + return input_control if [:feature_enabled, :slider_switch].include?(type) + form_group(option, input_control, attrs) + end + end + + # Build a general-purpose form group wrapper around the supplied input control + def form_group(setting, control, options = {}) + id = setting[:key].to_s + # label = setting[:title] || id.titleize + label = setting.meta.label || id.titleize + help_id = "#{id}HelpBlock" + # help_text = setting[:description] + # aria_label = setting[:aria_label] || "Radio button for following text input" + help_text = setting.meta.description + aria_label = "Radio button for following text input" #setting[:aria_label] || "Radio button for following text input" + + tag.div(class: "form-group") do + if options[:horizontal] + tag.div(class: 'row') do + tag.div(class: 'col col-sm-12 col-md-4') do + tag.label(for: id, value: label, aria: { label: aria_label }) do + label + end + end + + tag.div(class: 'col col-sm-12 col-md-1') do + tag.i(class: 'fas fa-info-circle', rel: 'tooltip', title: setting.meta.description) + end + + tag.div(class: 'col col-sm-12 col-md-7') do + input_group { control } # + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) + end + end + else + tag.label(for: id, value: label, aria: { label: aria_label }) do + label + end + + input_group { control } + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) + end + end + end + + def custom_form_group(setting, control) + id = setting[:key].to_s + # label = setting[:title] || id.titleize + label = setting.meta.label || id.titleize + help_id = "#{id}HelpBlock" + help_text = setting.meta.description + aria_label = "#{setting.meta.content_type.to_s.humanize} button for following text input" #setting[:aria_label] || "Radio button for following text input" + + tag.div(class: "form-group") do + tag.label(for: id, value: label, aria: { label: aria_label }) do + label + end + + control + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) + end + end + + def build_attribute_field(form, attribute) + setting = { + key: attribute, + default: form.object.send(attribute), + type: :string, + attribute: true + } + + input_control = input_text_control(setting, form) + form_group(setting, input_control) + end +end \ No newline at end of file diff --git a/lib/resource_registry/helpers/input_controls.rb b/lib/resource_registry/helpers/input_controls.rb index 7274417e..fb89e830 100644 --- a/lib/resource_registry/helpers/input_controls.rb +++ b/lib/resource_registry/helpers/input_controls.rb @@ -2,53 +2,6 @@ # Helper methods to render setting input fields module InputControls - def build_option_field(option, form, attrs = {}) - type = option.meta.content_type&.to_sym - input_control = case type - when :swatch - input_swatch_control(option, form) - when :base_64 - input_file_control(option, form) - when :radio_select - input_radio_control(option, form) - when :checkbox_select - input_checkbox_control(option, form) - when :select - select_control(option, form) - when :number - input_number_control(option, form) - when :email - input_email_control(option, form) - when :date - input_date_control(option, form) - when :date_range - input_date_range_control(option, form) - when :currency - input_currency_control(option, form) - when :feature_enabled - feature_enabled_control(option, form) - when :slider_switch - slider_switch_control(option, form) - else - input_text_control(option, form, attrs) - end - # else :text_field - # input_text_control(option, form) - # else - # # :dan_check_box - # # find dan_check_box_control helper - # # else - # # custom_helper #catch_all for custom types - # end - - if [:radio_select, :checkbox_select].include?(type) - custom_form_group(option, input_control) - else - return input_control if [:feature_enabled, :slider_switch].include?(type) - form_group(option, input_control, attrs) - end - end - def input_filter_control(_form, feature, data) filter_setting = feature.settings.detect{|s| s.key == :filter_params} filter_params = setting_value(filter_setting) @@ -109,7 +62,7 @@ def select_control(setting, form) # aria_describedby = id - value = value_for(setting, form) || setting.item || meta&.default + value = parse_input_value(setting, form) option_list = tag.option(selected_option, selected: (value.blank? ? true : false)) choices = meta.enum @@ -123,27 +76,29 @@ def select_control(setting, form) tag.select(option_list, id: id, class: "form-control", name: input_name_for(setting, form)) end - def select_dropdown(input_id, list, show_default = false, selected = nil) + def select_dropdown(input_id, list, show_default, selected = nil) name = (input_id.to_s.scan(/supported_languages/).present? ? input_id : "admin[#{input_id}]") return unless list.is_a? Array content_tag(:select, class: "form-control", id: input_id, name: name, required: true) do concat(content_tag(:option, "Select", value: "")) unless show_default - list.each do |item| - if item.is_a? Array - is_selected = false - is_selected = true if selected.present? && selected == item[1] - concat(content_tag(:option, item[0], value: item[1], selected: is_selected)) - elsif item.is_a? Hash - concat(content_tag(:option, item.first[1], value: item.first[0])) - elsif input_id == 'state' - concat(content_tag(:option, item.to_s.titleize, value: item)) - elsif show_default - concat(content_tag(:option, item, value: item)) - else - concat(content_tag(:option, item.to_s.humanize, value: item)) - end - end + list.each{|item| select_options_for(item, selected) } + end + end + + def select_options_for(item, selected) + if item.is_a? Array + is_selected = false + is_selected = true if selected.present? && selected == item[1] + concat(content_tag(:option, item[0], value: item[1], selected: is_selected)) + elsif item.is_a? Hash + concat(content_tag(:option, item.first[1], value: item.first[0])) + elsif input_id == 'state' + concat(content_tag(:option, item.to_s.titleize, value: item)) + elsif show_default + concat(content_tag(:option, item, value: item)) + else + concat(content_tag(:option, item.to_s.humanize, value: item)) end end @@ -163,7 +118,7 @@ def input_import_control(setting, _form) def input_radio_control(setting, form) meta = setting.meta - input_value = value_for(setting, form) || setting.item || meta&.default + input_value = parse_input_value(setting, form) aria_label = "Radio button for following text input" #setting[:aria_label] || "Radio button for following text input" if setting.is_a?(ResourceRegistry::Setting) @@ -171,7 +126,7 @@ def input_radio_control(setting, form) else element_name = "#{form&.object_name}[is_enabled]" input_value = form.object&.is_enabled - input_value = setting.is_enabled if input_value.blank? + input_value ||= setting.is_enabled end meta.enum.collect do |choice| @@ -185,7 +140,7 @@ def input_radio_control(setting, form) def input_checkbox_control(setting, form) meta = setting.meta - input_value = value_for(setting, form) || setting.item || meta&.default + input_value = parse_input_value(setting, form) aria_label = 'Checkbox button for following text input' meta.enum.collect do |choice| choice = send(choice) if choice.is_a?(String) @@ -197,6 +152,10 @@ def input_checkbox_control(setting, form) end.join.html_safe end + def parse_input_value(setting, form) + value_for(setting, form) || setting.item || setting.meta.default + end + def input_file_control(setting, form) meta = setting.meta id = setting.key.to_s @@ -229,87 +188,30 @@ def input_file_control(setting, form) def input_text_control(setting, form, options = {}) id = setting[:key].to_s - meta = setting[:meta] input_value = value_for(setting, form, options) || setting.item || meta&.default - # aria_describedby = id - is_required = meta[:is_required] == false ? meta[:is_required] : true placeholder = "Enter #{meta[:label]}".gsub('*','') if meta[:description].blank? - # if meta[:attribute] - # tag.input(nil, type: "text", value: input_value, id: id, name: form&.object_name.to_s + "[#{id}]",class: "form-control", required: true) - # else tag.input(nil, type: "text", value: input_value, id: id, name: input_name_for(setting, form), placeholder: placeholder, class: "form-control", required: is_required) - # end - end - - def input_date_control(setting, form) - id = setting[:key].to_s - - date_value = value_for(setting, form) - date_value = date_value.to_date if date_value.is_a?(Time) - date_value = date_value.to_s(:db) if date_value.is_a?(Date) - - meta = setting[:meta] - input_value = date_value || setting.item || meta&.default - # aria_describedby = id - - is_required = meta[:is_required] == false ? meta[:is_required] : true - - tag.input(nil, type: "date", value: input_value, id: id, name: input_name_for(setting, form), placeholder: "mm/dd/yyyy", class: "form-control", required: is_required) - end - - def input_date_range_control(setting, form) - meta = setting[:meta] - - date_bounds = setting.item.split('..').collect do |date_str| - if date_str.match?(/\d{4}-\d{2}-\d{2}/) - date_str - else - date = Date.strptime(date_str, "%m/%d/%Y") - date.to_s(:db) - end - end - - is_required = meta[:is_required] == false ? meta[:is_required] : true - - from_input_name = form&.object_name.to_s + "[settings][#{setting.key}][begin]" - to_input_name = form&.object_name.to_s + "[settings][#{setting.key}][end]" - - tag.input(nil, type: "date", value: date_bounds[0], id: from_input_name, name: from_input_name, placeholder: "mm/dd/yyyy", class: "form-control", required: is_required) + - tag.div(class: 'input-group-addon') { 'to' } + - tag.input(nil, type: "date", value: date_bounds[1], id: to_input_name, name: to_input_name, placeholder: "mm/dd/yyyy", class: "form-control", required: is_required) end def input_number_control(setting, form) id = setting[:key].to_s meta = setting[:meta] input_value = value_for(setting, form) || meta.value || meta.default - # input_value = setting[:value] || setting[:default] - # aria_describedby = id placeholder = "Enter #{meta[:label]}".gsub('*','') if meta[:description].blank? - # if setting[:attribute] tag.input(nil, type: "number", step: "any", value: input_value, id: id, name: input_name_for(setting, form), placeholder: placeholder, class: "form-control", required: true, oninput: "check(this)") - # else - # tag.input(nil, type: "number", step:"any", value: input_value, id: id, name: form&.object_name.to_s + "[value]",class: "form-control", required: true, oninput: "check(this)") - # end end def input_email_control(setting, form) id = setting[:key].to_s meta = setting[:meta] input_value = meta.value || meta.default - # input_value = setting[:value] || setting[:default] - # aria_describedby = id - # if setting[:attribute] tag.input(nil, type: "email", step: "any", value: input_value, id: id, name: input_name_for(setting, form),class: "form-control", required: true, oninput: "check(this)") - # else - # tag.input(nil, type: "email", step:"any", value: input_value, id: id, name: form&.object_name.to_s + "[value]",class: "form-control", required: true, oninput: "check(this)") - # end end def input_color_control(setting) @@ -320,8 +222,6 @@ def input_color_control(setting) end def input_swatch_control(setting, form) - # id = setting[:key].to_s - # color = setting[:value] || setting[:default] id = setting[:key].to_s meta = setting[:meta] color = meta.value || meta.default @@ -334,9 +234,6 @@ def input_currency_control(setting, form) id = setting[:key].to_s meta = setting[:meta] input_value = meta.value || meta.default - - # id = setting[:key].to_s - # input_value = setting[:value] || setting[:default] aria_map = { label: "Amount (to the nearest dollar)"} tag.div(tag.span('$', class: "input-group-text"), class: "input-group-prepend") + @@ -372,74 +269,6 @@ def input_name_for(setting, form) end end - # Wrap any input group in
tag - def input_group - tag.div(yield, class: "input-group") - end - - # Build a general-purpose form group wrapper around the supplied input control - def form_group(setting, control, options = {}) - id = setting[:key].to_s - # label = setting[:title] || id.titleize - label = setting.meta.label || id.titleize - help_id = "#{id}HelpBlock" - # help_text = setting[:description] - # aria_label = setting[:aria_label] || "Radio button for following text input" - help_text = setting.meta.description - aria_label = "Radio button for following text input" #setting[:aria_label] || "Radio button for following text input" - - tag.div(class: "form-group") do - if options[:horizontal] - tag.div(class: 'row') do - tag.div(class: 'col col-sm-12 col-md-4') do - tag.label(for: id, value: label, aria: { label: aria_label }) do - label - end - end + - tag.div(class: 'col col-sm-12 col-md-1') do - tag.i(class: 'fas fa-info-circle', rel: 'tooltip', title: setting.meta.description) - end + - tag.div(class: 'col col-sm-12 col-md-7') do - input_group { control } # + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) - end - end - else - tag.label(for: id, value: label, aria: { label: aria_label }) do - label - end + - input_group { control } + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) - end - end - end - - def custom_form_group(setting, control) - id = setting[:key].to_s - # label = setting[:title] || id.titleize - label = setting.meta.label || id.titleize - help_id = "#{id}HelpBlock" - help_text = setting.meta.description - aria_label = "#{setting.meta.content_type.to_s.humanize} button for following text input" #setting[:aria_label] || "Radio button for following text input" - - tag.div(class: "form-group") do - tag.label(for: id, value: label, aria: { label: aria_label }) do - label - end + - control + tag.small(help_text, id: help_id, class: ['form-text', 'text-muted']) - end - end - - def build_attribute_field(form, attribute) - setting = { - key: attribute, - default: form.object.send(attribute), - type: :string, - attribute: true - } - - input_control = input_text_control(setting, form) - form_group(setting, input_control) - end - def setting_value(setting) if setting.is_a?(ResourceRegistry::Setting) && setting.item.is_a?(String) JSON.parse(setting.item) @@ -447,4 +276,9 @@ def setting_value(setting) setting&.item end end + + # Wrap any input group in
tag + def input_group + tag.div(yield, class: "input-group") + end end \ No newline at end of file diff --git a/lib/resource_registry/helpers/view_controls.rb b/lib/resource_registry/helpers/view_controls.rb index a30073c4..31f0c466 100644 --- a/lib/resource_registry/helpers/view_controls.rb +++ b/lib/resource_registry/helpers/view_controls.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require_relative 'input_controls' +require_relative 'form_group_controls' # Helper methods to render interface from features/settings module RegistryViewControls - include ::InputControls + include ::FormGroupControls def render_settings(feature, form, registry, options) return render_model_settings(feature, form, registry, options) if feature.meta.content_type == :model_attributes @@ -150,54 +150,61 @@ def feature_group_control(features, _registry) features = features.select{|feature| feature.meta.present? && feature.meta.content_type.to_s != 'feature_action' } features.collect do |feature| - settings_with_meta = feature.settings.select{|s| s.meta.present?} + tag.div(class: 'mt-3') do + render_feature_action(feature) + render_feature_list(feature) + end + end.join + end + + def render_feature_action(feature) + settings_with_meta = feature.settings.select{|s| s.meta.present?} + tag.div(class: 'row') do + tag.div(class: 'col-md-6') do + tag.h4 do + feature.meta&.label || feature.key.to_s.titleize + end + end + + tag.div(class: 'col-md-6') do + action_setting = settings_with_meta.detect{|setting| setting.meta.content_type.to_s == 'feature_action'} + if action_setting + form_with(model: feature, url: action_setting.item, method: :get, remote: true, local: false) do |f| + hidden_field_tag('feature[action]', 'renew') + + hidden_field_tag('feature[key]', feature.key) + + f.submit(action_setting.key.to_s.titleize, class: 'btn btn-link') + end.html_safe + end + end + end + end + def render_feature_list(feature) + settings_with_meta = feature.settings.select{|s| s.meta.present?} + settings_with_meta.collect do |setting| + next if setting.meta.content_type.to_s == 'feature_action' + section_name = setting.meta&.label || setting.key.to_s.titleize tag.div(class: 'mt-3') do tag.div(class: 'row') do - tag.div(class: 'col-md-6') do - tag.h4 do - feature.meta&.label || feature.key.to_s.titleize + tag.div(class: 'col-md-4') do + tag.strong do + section_name end end + - tag.div(class: 'col-md-6') do - action_setting = settings_with_meta.detect{|setting| setting.meta.content_type.to_s == 'feature_action'} - if action_setting - form_with(model: feature, url: action_setting.item, method: :get, remote: true, local: false) do |f| - hidden_field_tag('feature[action]', 'renew') + - hidden_field_tag('feature[key]', feature.key) + - f.submit(action_setting.key.to_s.titleize, class: 'btn btn-link') - end.html_safe + tag.div(class: 'col-md-4') do + tag.a(href: "/exchanges/configurations/#{feature.key}/edit", data: {remote: true}) do + tag.span do + "View #{section_name}" + end end - end - end + - settings_with_meta.collect do |setting| - next if setting.meta.content_type.to_s == 'feature_action' - section_name = setting.meta&.label || setting.key.to_s.titleize - tag.div(class: 'mt-3') do - tag.div(class: 'row') do - tag.div(class: 'col-md-4') do - tag.strong do - section_name - end - end + - tag.div(class: 'col-md-4') do - tag.a(href: "/exchanges/configurations/#{feature.key}/edit", data: {remote: true}) do - tag.span do - "View #{section_name}" - end - end - end + - tag.div(class: 'col-md-6') do - tag.ul(class: 'list-group list-group-flush ml-2') do - feature_list = setting_value(setting) - feature_list.collect{|f| tag.li(class: 'list-group-item'){ f.key.to_s.titleize }}.join.html_safe - end - end + end + + tag.div(class: 'col-md-6') do + tag.ul(class: 'list-group list-group-flush ml-2') do + feature_list = setting_value(setting) + feature_list.collect{|f| tag.li(class: 'list-group-item'){ f.key.to_s.titleize }}.join.html_safe end end - end.compact.join.html_safe + end end - end.join + end.compact.join.html_safe end def get_feature(feature_key, registry) diff --git a/lib/resource_registry/navigation.rb b/lib/resource_registry/navigation.rb index b91b99ef..bec8ec24 100644 --- a/lib/resource_registry/navigation.rb +++ b/lib/resource_registry/navigation.rb @@ -55,7 +55,7 @@ def initialize(registry, options = {}) end def render_html - namespaces.collect{|namespace| to_ul(namespace)}.join.html_safe + namespaces.collect{|namespace| to_ul(vertex: namespace)}.join.html_safe end private @@ -70,7 +70,7 @@ def build_namespaces def to_namespace(vertex) namespace_dict = [vertex].inject({}) do |dict, record| - dict = record.to_h + dict.merge!(record.to_h) dict[:features] = dict[:feature_keys].collect{|key| @registry[key].feature.to_h.except(:settings)} dict[:namespaces] = @graph.adjacent_vertices(record).collect{|adjacent_vertex| to_namespace(adjacent_vertex)} attrs_to_skip = [:feature_keys] @@ -82,7 +82,7 @@ def to_namespace(vertex) result.to_h end - def to_ul(vertex, nested = false) + def to_ul(vertex:, nested: false) dict = to_namespace(vertex) if vertex.is_a?(ResourceRegistry::Namespace) tag.ul(options[:tag_options][(nested ? :nested_ul : :ul)][:options]) do @@ -104,7 +104,7 @@ def content_to_expand(element) tag.div(class: 'collapse', id: "nav_#{element[:key]}", 'aria-expanded': 'false') do nav_features = element[:features].select{|feature| feature[:meta][:content_type] == :nav} (nav_features + element[:namespaces]).reduce('') do |list, child_ele| - list + to_ul(child_ele, true) + list + to_ul(vertex: child_ele, nested: true) end.html_safe end end From 37beddbf84c46a7aa3c6e6d2e2e22f737ac193e1 Mon Sep 17 00:00:00 2001 From: Raghuram Ganjala Date: Thu, 18 Feb 2021 15:59:36 -0500 Subject: [PATCH 40/40] Fixed feature_enabled? method --- lib/resource_registry/registry.rb | 4 +--- spec/resource_registry/registry_spec.rb | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/resource_registry/registry.rb b/lib/resource_registry/registry.rb index 95691ead..41b9ddb6 100644 --- a/lib/resource_registry/registry.rb +++ b/lib/resource_registry/registry.rb @@ -176,9 +176,7 @@ def feature_enabled?(key) return false unless feature.enabled? namespaces = feature.namespace.split('.') - namespaces.detect(-> {true}) do |ancestor_key| - feature?(ancestor_key) ? resolve_feature(ancestor_key.to_sym).disabled? : false - end + namespaces.all? {|ancestor_key| feature?(ancestor_key) ? resolve_feature(ancestor_key.to_sym).enabled? : true } end private diff --git a/spec/resource_registry/registry_spec.rb b/spec/resource_registry/registry_spec.rb index c56c0709..7e2faad2 100644 --- a/spec/resource_registry/registry_spec.rb +++ b/spec/resource_registry/registry_spec.rb @@ -285,9 +285,8 @@ def call(params) end it "the child feature should be disabled" do - expect(registry.feature_enabled?(:trawler)).to be_truthy + expect(registry.feature_enabled?(:trawler)).to be_falsey end - end end end