diff --git a/.gitignore b/.gitignore index eed7237..b569f62 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ .gem_rbs_collection .rspec_status node_modules +spec/sandbox/log/ +spec/sandbox/storage/ +spec/sandbox/tmp/ diff --git a/.rubocop.yml b/.rubocop.yml index 052c7eb..bb0b5e9 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,4 +1,8 @@ inherit_gem: { servactory-rubocop: rubocop-gem.yml } +plugins: + - rubocop-capybara + - rubocop-rspec_rails + Style/Documentation: Enabled: false diff --git a/Gemfile.lock b/Gemfile.lock index 4da8da0..1a7fb7a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -262,6 +262,9 @@ GEM rubocop-ast (1.45.1) parser (>= 3.3.7.2) prism (~> 1.4) + rubocop-capybara (2.22.1) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) rubocop-factory_bot (2.27.1) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) @@ -350,6 +353,7 @@ DEPENDENCIES rbs (>= 3.8) rspec (>= 3.13) rspec-rails (>= 7.0) + rubocop-capybara (>= 0.9) servactory-rubocop (>= 0.9) servactory-web! sqlite3 (>= 2.1) diff --git a/app/components/servactory/web/ui_kit/organisms/navbar_component.html.erb b/app/components/servactory/web/ui_kit/organisms/navbar_component.html.erb index 88edfcd..c931a8d 100644 --- a/app/components/servactory/web/ui_kit/organisms/navbar_component.html.erb +++ b/app/components/servactory/web/ui_kit/organisms/navbar_component.html.erb @@ -2,7 +2,7 @@

- <%= render Servactory::Web::UiKit::Atoms::LinkComponent.new(href: Servactory::Web::Engine.routes.url_helpers.services_path, text: "Servactory", options: { class: "font-bold text-gray-900 hover:text-blue-600 transition-colors", aria: { label: "Home" } }) %> + <%= render Servactory::Web::UiKit::Atoms::LinkComponent.new(href: Servactory::Web::Engine.routes.url_helpers.internal_services_path, text: "Servactory", options: { class: "font-bold text-gray-900 hover:text-blue-600 transition-colors", aria: { label: "Home" } }) %>

<% if @app_name.present? %> @@ -18,4 +18,4 @@ <%= render Servactory::Web::UiKit::Atoms::LinkComponent.new(href: @github_url, text: "GitHub", options: { class: "text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors", target: "_blank", rel: "nofollow", aria: { label: "GitHub repository" } }) %>
- \ No newline at end of file + diff --git a/app/components/servactory/web/ui_kit/organisms/tree_component.html.erb b/app/components/servactory/web/ui_kit/organisms/tree_component.html.erb index 7592208..ffeaaa1 100644 --- a/app/components/servactory/web/ui_kit/organisms/tree_component.html.erb +++ b/app/components/servactory/web/ui_kit/organisms/tree_component.html.erb @@ -1,7 +1,7 @@ \ No newline at end of file diff --git a/app/components/servactory/web/ui_kit/organisms/tree_component.rb b/app/components/servactory/web/ui_kit/organisms/tree_component.rb index c8ac52c..6b79383 100644 --- a/app/components/servactory/web/ui_kit/organisms/tree_component.rb +++ b/app/components/servactory/web/ui_kit/organisms/tree_component.rb @@ -6,9 +6,11 @@ module UiKit module Organisms class TreeComponent < ViewComponent::Base include Servactory::Web::UiKit::Concerns::ComponentOptions - def initialize(nodes: [], class_name: nil, options: {}) + def initialize(route_type:, nodes: [], class_name: nil, options: {}, gem_name: nil) super() @nodes = nodes + @route_type = route_type + @gem_name = gem_name initialize_component_options(class_name:, options:) end end diff --git a/app/components/servactory/web/ui_kit/organisms/tree_node_component.html.erb b/app/components/servactory/web/ui_kit/organisms/tree_node_component.html.erb index 84c5c5f..4d211e5 100644 --- a/app/components/servactory/web/ui_kit/organisms/tree_node_component.html.erb +++ b/app/components/servactory/web/ui_kit/organisms/tree_node_component.html.erb @@ -7,7 +7,7 @@
@@ -17,7 +17,12 @@
<%= render Servactory::Web::UiKit::Atoms::IconComponent.new(name: :file, class_name: 'size-3 text-gray-500') %> - <%= render Servactory::Web::UiKit::Atoms::LinkComponent.new(href: Servactory::Web::Engine.routes.url_helpers.service_path(@node[:path]), text: @node[:name], options: { class: "text-sm text-gray-700 hover:text-blue-600 transition-colors", aria: { label: "View service: #{@node[:name]}" } }) %> + <% url = if @route_type.to_sym == :internal + Servactory::Web::Engine.routes.url_helpers.internal_service_path(@node[:path]) + else + Servactory::Web::Engine.routes.url_helpers.external_service_path(@gem_name, @node[:path]) + end %> + <%= render Servactory::Web::UiKit::Atoms::LinkComponent.new(href: url, text: @node[:name], options: { class: "text-sm text-gray-700 hover:text-blue-600 transition-colors", aria: { label: "View service: #{@node[:name]}" } }) %>
diff --git a/app/components/servactory/web/ui_kit/organisms/tree_node_component.rb b/app/components/servactory/web/ui_kit/organisms/tree_node_component.rb index 4df61dc..cc3bd3d 100644 --- a/app/components/servactory/web/ui_kit/organisms/tree_node_component.rb +++ b/app/components/servactory/web/ui_kit/organisms/tree_node_component.rb @@ -5,11 +5,13 @@ module Web module UiKit module Organisms class TreeNodeComponent < ViewComponent::Base - def initialize(node:, level: 0) + def initialize(node:, route_type:, level: 0, gem_name: nil) super() @node = node @level = level + @route_type = route_type + @gem_name = gem_name end def border_class diff --git a/app/controllers/servactory/web/external/services_controller.rb b/app/controllers/servactory/web/external/services_controller.rb new file mode 100644 index 0000000..1f3384e --- /dev/null +++ b/app/controllers/servactory/web/external/services_controller.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Servactory + module Web + module External + class ServicesController < ActionController::Base + layout "servactory/web/application" + + before_action :assign_gem + + def index + @gem_services_tree = Servactory::Web::Services::External::TreeBuilder.build(@gem) + end + + def show + service_data = Servactory::Web::Services::External::TreeBuilder.find_service(@gem, params[:id]) + @base_path = service_data[:base_path] + @file_path = service_data[:file_path] + @service_class = service_data[:service_class] + @source_code = service_data[:source_code] + end + + private + + def assign_gem + @gem = Bundler.definition.specs.find { |s| s.name == params[:gem_name] } + end + end + end + end +end diff --git a/app/controllers/servactory/web/internal/services_controller.rb b/app/controllers/servactory/web/internal/services_controller.rb new file mode 100644 index 0000000..6eb39d4 --- /dev/null +++ b/app/controllers/servactory/web/internal/services_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Servactory + module Web + module Internal + class ServicesController < ActionController::Base + layout "servactory/web/application" + + def index + @app_services_tree = Servactory::Web::Services::Internal::TreeBuilder.build + end + + def show + service_data = Servactory::Web::Services::Internal::TreeBuilder.find_service(params[:id]) + @file_path = service_data[:file_path] + @service_class = service_data[:service_class] + @source_code = service_data[:source_code] + end + end + end + end +end diff --git a/app/controllers/servactory/web/services_controller.rb b/app/controllers/servactory/web/services_controller.rb deleted file mode 100644 index a0d4f5c..0000000 --- a/app/controllers/servactory/web/services_controller.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module Servactory - module Web - class ServicesController < ActionController::Base - layout "servactory/web/application" - - def index - @services_tree = Servactory::Web::Services::TreeBuilder.build - end - - def show - service_path = params[:id].to_s.gsub(%r{^services/?}, "").sub(%r{^/+}, "") - file_path = Servactory::Web::Services::TreeBuilder::SERVICES_PATH.join("#{service_path}.rb") - class_name = Servactory::Web::Services::TreeBuilder.class_name_from_path(Pathname.new(file_path)) - - @service_class = class_name.safe_constantize - @source_code = (File.read(file_path) if @service_class && File.exist?(file_path)) - end - end - end -end diff --git a/app/views/servactory/web/external/services/index.html.erb b/app/views/servactory/web/external/services/index.html.erb new file mode 100644 index 0000000..93e3689 --- /dev/null +++ b/app/views/servactory/web/external/services/index.html.erb @@ -0,0 +1,19 @@ +<% if @gem %> + <%= render Servactory::Web::UiKit::Organisms::PageHeaderComponent.new( + title: "Services: #{@gem.full_name}", + description: "List of all services written using Servactory in the Gem" + ) %> + <%= render Servactory::Web::UiKit::Organisms::CardComponent.new(class_name: 'shadow-lg') do %> + <%= render Servactory::Web::UiKit::Organisms::TreeComponent.new(nodes: @gem_services_tree, route_type: :external, gem_name: @gem.name) %> + <% end %> +<% else %> + <%= render Servactory::Web::UiKit::Organisms::PageHeaderComponent.new( + title: "Service Not Found", + description: nil + ) %> + <%= render Servactory::Web::UiKit::Organisms::CardComponent.new do %> +
+ No services found for the specified
<%= params[:gem_name] %>
library +
+ <% end %> +<% end %> diff --git a/app/views/servactory/web/services/show.html.erb b/app/views/servactory/web/external/services/show.html.erb similarity index 99% rename from app/views/servactory/web/services/show.html.erb rename to app/views/servactory/web/external/services/show.html.erb index 6825bf1..373997a 100644 --- a/app/views/servactory/web/services/show.html.erb +++ b/app/views/servactory/web/external/services/show.html.erb @@ -35,7 +35,7 @@ empty_message: 'No output attributes defined' ) %> - <% + <% # Transform stages data to a flat structure for the component actions_data = {} if @service_class.info.stages.present? diff --git a/app/views/servactory/web/services/index.html.erb b/app/views/servactory/web/internal/services/index.html.erb similarity index 74% rename from app/views/servactory/web/services/index.html.erb rename to app/views/servactory/web/internal/services/index.html.erb index 544c708..1a45e85 100644 --- a/app/views/servactory/web/services/index.html.erb +++ b/app/views/servactory/web/internal/services/index.html.erb @@ -1,7 +1,7 @@ <%= render Servactory::Web::UiKit::Organisms::PageHeaderComponent.new( - title: 'Services', - description: 'List of all services written using Servactory in the project' + title: 'Services: Application', + description: 'List of all services written using Servactory in the Application' ) %> <%= render Servactory::Web::UiKit::Organisms::CardComponent.new(class_name: 'shadow-lg') do %> - <%= render Servactory::Web::UiKit::Organisms::TreeComponent.new(nodes: @services_tree) %> + <%= render Servactory::Web::UiKit::Organisms::TreeComponent.new(nodes: @app_services_tree, route_type: :internal) %> <% end %> diff --git a/app/views/servactory/web/internal/services/show.html.erb b/app/views/servactory/web/internal/services/show.html.erb new file mode 100644 index 0000000..373997a --- /dev/null +++ b/app/views/servactory/web/internal/services/show.html.erb @@ -0,0 +1,67 @@ +<% if @service_class && @source_code %> + <%= render Servactory::Web::UiKit::Organisms::PageHeaderComponent.new( + title: @service_class.name, + description: 'Detailed information about the service inputs, internals, outputs, and implementation' + ) %> +
+ <%= render Servactory::Web::UiKit::Organisms::SectionCardComponent.new( + title: 'Inputs', + items: @service_class.info.inputs, + border_class: 'border-blue-500', + text_class: 'text-blue-700', + bg_class: 'bg-blue-50', + icon_name: :inputs, + empty_message: 'No input attributes defined', + class_name: 'mb-4' + ) %> + + <%= render Servactory::Web::UiKit::Organisms::SectionCardComponent.new( + title: 'Internals', + items: @service_class.info.internals, + border_class: 'border-purple-500', + text_class: 'text-purple-700', + bg_class: 'bg-blue-50', + icon_name: :internals, + empty_message: 'No internal attributes defined' + ) %> + + <%= render Servactory::Web::UiKit::Organisms::SectionCardComponent.new( + title: 'Outputs', + items: @service_class.info.outputs, + border_class: 'border-green-500', + text_class: 'text-green-700', + bg_class: 'bg-blue-50', + icon_name: :outputs, + empty_message: 'No output attributes defined' + ) %> + + <% + # Transform stages data to a flat structure for the component + actions_data = {} + if @service_class.info.stages.present? + @service_class.info.stages.each do |_stage_name, actions| + actions_data.merge!(actions) + end + end + %> + + <%= render Servactory::Web::UiKit::Organisms::SectionCardComponent.new( + title: 'Actions', + items: actions_data, + border_class: 'border-orange-500', + text_class: 'text-orange-700', + bg_class: 'bg-orange-50', + icon_name: :actions, + empty_message: 'No actions defined' + ) %> +
+ <%= render Servactory::Web::UiKit::Organisms::CodeBlockComponent.new(code: @source_code, language: "ruby", copy_button: true) %> +<% else %> + <%= render Servactory::Web::UiKit::Organisms::PageHeaderComponent.new( + title: 'Service Not Found', + description: 'The requested service could not be found' + ) %> + <%= render Servactory::Web::UiKit::Organisms::CardComponent.new do %> +

Service not found or source code could not be retrieved.

+ <% end %> +<% end %> diff --git a/config/routes.rb b/config/routes.rb index 5cdec81..a2ab6fa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,15 @@ # frozen_string_literal: true Servactory::Web::Engine.routes.draw do - root "servactory/web/services#index" + root "servactory/web/internal/services#index" - resources :services, only: %i[index show], controller: "servactory/web/services" + scope :internal, as: :internal do + resources :services, only: %i[index show], controller: "servactory/web/internal/services" + end + + scope :external, as: :external do + scope ":gem_name" do + resources :services, only: %i[index show], controller: "servactory/web/external/services" + end + end end diff --git a/lib/servactory/web/configuration.rb b/lib/servactory/web/configuration.rb index 52f9dc1..d6a6a7f 100644 --- a/lib/servactory/web/configuration.rb +++ b/lib/servactory/web/configuration.rb @@ -4,13 +4,17 @@ module Servactory module Web class Configuration attr_accessor :app_name, - :app_url + :app_url, + :gem_names, + :gem_service_directories attr_reader :app_services_directory def initialize @app_services_directory = Rails.root.join("app/services") @app_url = nil + @gem_names = [] + @gem_service_directories = %w[app/services lib] end def app_services_directory=(value) diff --git a/lib/servactory/web/services/external/tree_builder.rb b/lib/servactory/web/services/external/tree_builder.rb new file mode 100644 index 0000000..2dfd779 --- /dev/null +++ b/lib/servactory/web/services/external/tree_builder.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true + +module Servactory + module Web + module Services + module External + # Builds a tree structure of service classes for UI display (gems only) + class TreeBuilder + GEM_NAMES = Servactory::Web.configuration.gem_names + GEM_SERVICE_DIRECTORIES = Servactory::Web.configuration.gem_service_directories + + def self.build(gem) + new(gem).build + end + + def initialize(gem) + @gem = gem + end + + def build + services = ServiceFileCollector.collect_service_files_for_gem(@gem) + TreeConstructor.build_tree(services) + end + + def self.class_name_from_path(path, base_path) + PathConverter.class_name_from_path(path, base_path) + end + + def self.find_service(gem, service_path) + new(gem).find_service(service_path) + end + + def find_service(service_path) # rubocop:disable Metrics/MethodLength + service_path = service_path.to_s.gsub(%r{^services/?}, "").sub(%r{^/+}, "") + gem_root = Pathname.new(@gem.gem_dir) + + found = GEM_SERVICE_DIRECTORIES.find do |subdir| + base_path = gem_root.join(subdir) + file_path = base_path.join("#{service_path}.rb") + if file_path.exist? + return { + base_path:, + file_path:, + service_class: load_service_class(file_path, base_path), + source_code: load_source_code(file_path) + } + end + false + end + + return if found + + { + base_path: nil, + file_path: nil, + service_class: nil, + source_code: nil + } + end + + private + + def load_service_class(file_path, base_path) + class_name = PathConverter.class_name_from_path(file_path, base_path) + class_name.safe_constantize + end + + def load_source_code(file_path) + File.read(file_path) if File.exist?(file_path) + end + + class PathConverter + def self.class_name_from_path(path, base_path) + rel_path = path.relative_path_from(base_path).to_s + rel_path.chomp!(".rb") + rel_path.split("/").map(&:camelize).join("::") + end + + def self.extract_display_name(pathname, base_path) + pathname.relative_path_from(base_path) + .to_s + .chomp(".rb") + .split("/") + .last + .camelize + end + end + + class ServiceClassVerifier + def self.service_class?(class_name) + service_class = class_name.safe_constantize + return false unless service_class.is_a?(Class) + return false unless service_class.respond_to?(:servactory?) + + service_class.servactory? + rescue StandardError + false + end + end + + class ServiceFileCollector + def self.collect_service_files_for_gem(gem) + gem_root = Pathname.new(gem.gem_dir) + + GEM_SERVICE_DIRECTORIES.flat_map do |subdir| + collect_services_from_gem_subdir(gem_root, subdir) + end.flatten + rescue Gem::LoadError, Gem::MissingSpecError # rubocop:disable Lint/ShadowedException + [] + end + + def self.collect_services_from_gem_subdir(gem_root, subdir) # rubocop:disable Metrics/MethodLength + base_path = gem_root.join(subdir) + return [] unless base_path.exist? + + Dir.glob(base_path.join("**", "*.rb")).filter_map do |file| + pathname = Pathname.new(file) + class_name = PathConverter.class_name_from_path(pathname, base_path) + next unless ServiceClassVerifier.service_class?(class_name) + + { + path: pathname.relative_path_from(base_path).to_s, + class_name:, + display_name: PathConverter.extract_display_name(pathname, base_path) + } + end + end + end + + class TreeConstructor + def self.build_tree(services) + build_tree_from_paths(services) + end + + def self.build_tree_from_paths(services, parent_path = "") # rubocop:disable Metrics/MethodLength + grouped_services = services.group_by { |s| s[:path].split("/").first } + directories = [] + files = [] + + grouped_services.each do |key, group| + path_parts = group.map { |s| s[:path].split("/") } + + if contains_only_files?(path_parts) + files.concat(process_files(group, parent_path)) + else + directories << process_directory(key, group, parent_path) + end + end + + sort_and_combine(directories, files) + end + + def self.contains_only_files?(path_parts) + path_parts.all? { |parts| parts.size == 1 } + end + + def self.process_files(group, parent_path) + group.map do |service| + full_path = File.join(parent_path, service[:path]) + { + name: service[:display_name], + type: "file", + path: "services/#{full_path}" + } + end + end + + def self.process_directory(key, group, parent_path) + children_services = extract_children_services(group) + + { + name: key.camelize, + type: "directory", + children: build_tree_from_paths(children_services, File.join(parent_path, key)) + } + end + + def self.extract_children_services(group) + group.filter_map do |service| + parts = service[:path].split("/") + next if parts.size == 1 + + { + path: parts[1..].join("/"), + class_name: service[:class_name], + display_name: service[:display_name] + } + end + end + + def self.sort_and_combine(directories, files) + directories.sort_by! { |h| h[:name] } + files.sort_by! { |h| h[:name] } + directories + files + end + end + end + end + end + end +end diff --git a/lib/servactory/web/services/internal/tree_builder.rb b/lib/servactory/web/services/internal/tree_builder.rb new file mode 100644 index 0000000..82fde9f --- /dev/null +++ b/lib/servactory/web/services/internal/tree_builder.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true + +module Servactory + module Web + module Services + module Internal + # Строит дерево сервисов только из app/services + class TreeBuilder + SERVICES_PATH = Servactory::Web.configuration.app_services_directory + + def self.build + new.build + end + + def build + services = ServiceFileCollector.collect_app_service_files + TreeConstructor.build_tree(services) + end + + def self.class_name_from_path(path, base_path = SERVICES_PATH) + PathConverter.class_name_from_path(path, base_path) + end + + def self.find_service(service_path) + new.find_service(service_path) + end + + def find_service(service_path) # rubocop:disable Metrics/MethodLength + service_path = service_path.to_s.gsub(%r{^services/?}, "").sub(%r{^/+}, "") + file_path = SERVICES_PATH.join("#{service_path}.rb") + + if file_path.exist? + service_class = load_service_class(file_path) + source_code = load_source_code(file_path) + else + service_class = nil + source_code = nil + end + + { + file_path:, + service_class:, + source_code: + } + end + + private + + def load_service_class(file_path) + class_name = PathConverter.class_name_from_path(file_path) + class_name.safe_constantize + end + + def load_source_code(file_path) + File.read(file_path) if File.exist?(file_path) + end + + class PathConverter + def self.class_name_from_path(path, base_path = TreeBuilder::SERVICES_PATH) + rel_path = path.relative_path_from(base_path).to_s + rel_path.chomp!(".rb") + rel_path.split("/").map(&:camelize).join("::") + end + + def self.extract_display_name(pathname, base_path = TreeBuilder::SERVICES_PATH) + pathname.relative_path_from(base_path) + .to_s + .chomp(".rb") + .split("/") + .last + .camelize + end + end + + class ServiceClassVerifier + def self.service_class?(class_name) + service_class = class_name.safe_constantize + return false unless service_class.is_a?(Class) + return false unless service_class.respond_to?(:servactory?) + + service_class.servactory? + rescue StandardError + false + end + end + + class ServiceFileCollector + def self.collect_app_service_files # rubocop:disable Metrics/MethodLength + service_files = Dir.glob(TreeBuilder::SERVICES_PATH.join("**", "*.rb")) + + service_files.filter_map do |file| + pathname = Pathname.new(file) + class_name = PathConverter.class_name_from_path(pathname) + next unless ServiceClassVerifier.service_class?(class_name) + + { + path: pathname.relative_path_from(TreeBuilder::SERVICES_PATH).to_s, + class_name:, + display_name: PathConverter.extract_display_name(pathname, TreeBuilder::SERVICES_PATH) + } + end + end + end + + class TreeConstructor + def self.build_tree(services) + build_tree_from_paths(services) + end + + def self.build_tree_from_paths(services, parent_path = "") # rubocop:disable Metrics/MethodLength + grouped_services = services.group_by { |s| s[:path].split("/").first } + directories = [] + files = [] + + grouped_services.each do |key, group| + path_parts = group.map { |s| s[:path].split("/") } + + if contains_only_files?(path_parts) + files.concat(process_files(group, parent_path)) + else + directories << process_directory(key, group, parent_path) + end + end + + sort_and_combine(directories, files) + end + + def self.contains_only_files?(path_parts) + path_parts.all? { |parts| parts.size == 1 } + end + + def self.process_files(group, parent_path) + group.map do |service| + full_path = File.join(parent_path, service[:path]) + { + name: service[:display_name], + type: "file", + path: "services/#{full_path}" + } + end + end + + def self.process_directory(key, group, parent_path) + children_services = extract_children_services(group) + + { + name: key.camelize, + type: "directory", + children: build_tree_from_paths(children_services, File.join(parent_path, key)) + } + end + + def self.extract_children_services(group) + group.filter_map do |service| + parts = service[:path].split("/") + next if parts.size == 1 + + { + path: parts[1..].join("/"), + class_name: service[:class_name], + display_name: service[:display_name] + } + end + end + + def self.sort_and_combine(directories, files) + directories.sort_by! { |h| h[:name] } + files.sort_by! { |h| h[:name] } + directories + files + end + end + end + end + end + end +end diff --git a/lib/servactory/web/services/tree_builder.rb b/lib/servactory/web/services/tree_builder.rb deleted file mode 100644 index bbdb197..0000000 --- a/lib/servactory/web/services/tree_builder.rb +++ /dev/null @@ -1,192 +0,0 @@ -# frozen_string_literal: true - -module Servactory - module Web - module Services - # Builds a tree structure of service classes for UI display - class TreeBuilder - SERVICES_PATH = Servactory::Web.configuration.app_services_directory - - # Builds the complete service tree - # - # @return [Array] Tree structure of services - def self.build - new.build - end - - # Finds a service class by name - # - # @param name [String] The class name to find - # @return [Class, nil] The service class or nil if not found - def self.find_service_class(name) - name.safe_constantize - end - - # Converts a file path to a class name - # - # @param path [Pathname] File path - # @return [String] Class name - def self.class_name_from_path(path) - new.class_name_from_path(path) - end - - # Builds the complete service tree - # - # @return [Array] Tree structure of services - def build - build_tree_from_paths(collect_service_files) - end - - # Converts a file path to a class name - # - # @param path [Pathname] File path - # @return [String] Class name - def class_name_from_path(path) - rel_path = path.relative_path_from(SERVICES_PATH).to_s - rel_path.chomp!(".rb") - rel_path.split("/").map(&:camelize).join("::") - end - - private - - # Collects all service files and their metadata - # - # @return [Array] List of service files with metadata - def collect_service_files # rubocop:disable Metrics/MethodLength - service_files = Dir.glob(SERVICES_PATH.join("**", "*.rb")) - - service_files.filter_map do |file| - pathname = Pathname.new(file) - class_name = class_name_from_path(pathname) - - next unless service_class?(class_name) - - { - path: pathname.relative_path_from(SERVICES_PATH).to_s, - class_name:, - display_name: extract_display_name(pathname) - } - end - end - - # Extracts display name from file path - # - # @param pathname [Pathname] File path - # @return [String] Display name for the service - def extract_display_name(pathname) - pathname.relative_path_from(SERVICES_PATH) - .to_s - .chomp(".rb") - .split("/") - .last - .camelize - end - - # Builds a tree structure from service paths - # - # @param services [Array] List of services with metadata - # @param parent_path [String] Parent path for nested services - # @return [Array] Tree structure of services - def build_tree_from_paths(services, parent_path = "") # rubocop:disable Metrics/MethodLength - grouped_services = services.group_by { |s| s[:path].split("/").first } - directories = [] - files = [] - - grouped_services.each do |key, group| - path_parts = group.map { |s| s[:path].split("/") } - - if contains_only_files?(path_parts) - files.concat(process_files(group, parent_path)) - else - directories << process_directory(key, group, parent_path) - end - end - - sort_and_combine(directories, files) - end - - # Checks if all paths represent files (no subdirectories) - # - # @param path_parts [Array>] Path parts - # @return [Boolean] True if all paths are files - def contains_only_files?(path_parts) - path_parts.all? { |parts| parts.size == 1 } - end - - # Processes file entries - # - # @param group [Array] Group of services - # @param parent_path [String] Parent path - # @return [Array] File entries - def process_files(group, parent_path) - group.map do |service| - full_path = File.join(parent_path, service[:path]) - { - name: service[:display_name], - type: "file", - path: "services/#{full_path}" - } - end - end - - # Processes directory entries - # - # @param key [String] Directory name - # @param group [Array] Group of services - # @param parent_path [String] Parent path - # @return [Hash] Directory entry with children - def process_directory(key, group, parent_path) - children_services = extract_children_services(group) - - { - name: key.camelize, - type: "directory", - children: build_tree_from_paths(children_services, File.join(parent_path, key)) - } - end - - # Extracts children services from a group - # - # @param group [Array] Group of services - # @return [Array] Children services - def extract_children_services(group) - group.filter_map do |service| - parts = service[:path].split("/") - next if parts.size == 1 - - { - path: parts[1..].join("/"), - class_name: service[:class_name], - display_name: service[:display_name] - } - end - end - - # Sorts and combines directories and files - # - # @param directories [Array] Directory entries - # @param files [Array] File entries - # @return [Array] Combined and sorted entries - def sort_and_combine(directories, files) - directories.sort_by! { |h| h[:name] } - files.sort_by! { |h| h[:name] } - directories + files - end - - # Checks if a class is a service class - # - # @param class_name [String] Class name to check - # @return [Boolean] True if it's a service class - def service_class?(class_name) - service_class = class_name.safe_constantize - return false unless service_class.is_a?(Class) - return false unless service_class.respond_to?(:servactory?) - - service_class.servactory? - rescue StandardError - false - end - end - end - end -end diff --git a/servactory-web.gemspec b/servactory-web.gemspec index 00057ef..c2d893d 100644 --- a/servactory-web.gemspec +++ b/servactory-web.gemspec @@ -42,5 +42,6 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rbs", ">= 3.8" spec.add_development_dependency "rspec", ">= 3.13" spec.add_development_dependency "rspec-rails", ">= 7.0" + spec.add_development_dependency "rubocop-capybara", ">= 0.9" # TODO: Add to servactory-rubocop spec.add_development_dependency "servactory-rubocop", ">= 0.9" end diff --git a/spec/servactory/web/services_controller_spec.rb b/spec/internal/services_controller_spec.rb similarity index 75% rename from spec/servactory/web/services_controller_spec.rb rename to spec/internal/services_controller_spec.rb index 1794816..06c35cc 100644 --- a/spec/servactory/web/services_controller_spec.rb +++ b/spec/internal/services_controller_spec.rb @@ -2,20 +2,20 @@ require "spec_helper" -RSpec.describe "ServicesController", type: :request do +RSpec.describe "Servactory::Web::App::ServicesController", type: :request do let(:routes) { Servactory::Web::Engine.routes.url_helpers } - describe "GET /servactory/services" do + describe "GET /servactory/app/services" do it "returns http success" do - get routes.services_path + get routes.internal_services_path expect(response).to have_http_status(:ok) end end - describe "GET /services/:id" do + describe "GET /services/app/:id" do context "when service exists" do it "returns http success and renders service info", :aggregate_failures do - get routes.service_path("full_name_service") + get routes.internal_service_path("full_name_service") expect(response).to have_http_status(:ok) expect(response.body).to include("FullNameService") expect(response.body).to include("Inputs") @@ -28,7 +28,7 @@ context "when service does not exist" do it "returns http success and renders not found message", :aggregate_failures do - get routes.service_path("not_existing_service") + get routes.internal_service_path("not_existing_service") expect(response).to have_http_status(:ok) expect(response.body).to include("Service Not Found") end diff --git a/spec/sandbox/log/test.log b/spec/sandbox/log/test.log deleted file mode 100644 index 316290f..0000000 --- a/spec/sandbox/log/test.log +++ /dev/null @@ -1,273 +0,0 @@ -Started GET "/services" for 127.0.0.1 at 2025-07-07 01:24:36 +0700 - -ActionController::RoutingError (No route matches [GET] "/services"): - -Started GET "/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:24:37 +0700 - -ActionController::RoutingError (No route matches [GET] "/services/full_name_service"): - -Started GET "/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:24:37 +0700 - -ActionController::RoutingError (No route matches [GET] "/services/not_existing_service"): - -Started GET "/services" for 127.0.0.1 at 2025-07-07 01:24:59 +0700 - -ActionController::RoutingError (No route matches [GET] "/services"): - -Started GET "/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:24:59 +0700 - -ActionController::RoutingError (No route matches [GET] "/services/full_name_service"): - -Started GET "/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:24:59 +0700 - -ActionController::RoutingError (No route matches [GET] "/services/not_existing_service"): - -Started GET "/services" for 127.0.0.1 at 2025-07-07 01:25:29 +0700 - -ActionController::RoutingError (No route matches [GET] "/services"): - -Started GET "/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:25:29 +0700 - -ActionController::RoutingError (No route matches [GET] "/services/full_name_service"): - -Started GET "/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:25:29 +0700 - -ActionController::RoutingError (No route matches [GET] "/services/not_existing_service"): - -Started GET "/services" for 127.0.0.1 at 2025-07-07 01:25:45 +0700 - -ActionController::RoutingError (No route matches [GET] "/services"): - -Started GET "/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:25:45 +0700 - -ActionController::RoutingError (No route matches [GET] "/services/full_name_service"): - -Started GET "/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:25:46 +0700 - -ActionController::RoutingError (No route matches [GET] "/services/not_existing_service"): - -Started GET "/services" for 127.0.0.1 at 2025-07-07 01:25:54 +0700 - -ActionController::RoutingError (No route matches [GET] "/services"): - -Started GET "/services" for 127.0.0.1 at 2025-07-07 01:28:12 +0700 - -ActionController::RoutingError (No route matches [GET] "/services"): - -Started GET "/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:28:12 +0700 - -ActionController::RoutingError (No route matches [GET] "/services/full_name_service"): - -Started GET "/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:28:12 +0700 - -ActionController::RoutingError (No route matches [GET] "/services/not_existing_service"): - -Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:28:43 +0700 -Processing by Servactory::Web::ServicesController#index as HTML - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 23.7ms | GC: 9.1ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 35.8ms | GC: 15.5ms) -Completed 500 Internal Server Error in 46ms (ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 16.3ms) -Started GET "/servactory/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:28:43 +0700 -Processing by Servactory::Web::ServicesController#show as HTML - Parameters: {"id" => "full_name_service"} -Completed 500 Internal Server Error in 0ms (ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) -Started GET "/servactory/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:28:44 +0700 -Processing by Servactory::Web::ServicesController#show as HTML - Parameters: {"id" => "not_existing_service"} - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 0.4ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 0.7ms | GC: 0.0ms) -Completed 500 Internal Server Error in 1ms (ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) -Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:28:54 +0700 -Processing by Servactory::Web::ServicesController#index as HTML - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 10.6ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 14.4ms | GC: 0.0ms) -Completed 500 Internal Server Error in 21ms (ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.2ms) -Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:29:08 +0700 -Processing by Servactory::Web::ServicesController#index as HTML - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 12.0ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 15.8ms | GC: 0.0ms) -Completed 200 OK in 23ms (Views: 17.6ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.2ms) -Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:29:31 +0700 -Processing by Servactory::Web::ServicesController#index as HTML - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 22.6ms | GC: 8.4ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 33.7ms | GC: 14.5ms) -Completed 200 OK in 42ms (Views: 35.9ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 14.8ms) -Started GET "/servactory/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:29:31 +0700 -Processing by Servactory::Web::ServicesController#show as HTML - Parameters: {"id" => "full_name_service"} -Completed 500 Internal Server Error in 1ms (ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.6ms) -Started GET "/servactory/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:29:31 +0700 -Processing by Servactory::Web::ServicesController#show as HTML - Parameters: {"id" => "not_existing_service"} - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 0.4ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 0.7ms | GC: 0.0ms) -Completed 200 OK in 1ms (Views: 0.9ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) -Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:29:38 +0700 -Processing by Servactory::Web::ServicesController#index as HTML - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 10.6ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 14.7ms | GC: 0.0ms) -Completed 200 OK in 22ms (Views: 16.2ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.2ms) -Started GET "/servactory/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:29:38 +0700 -Processing by Servactory::Web::ServicesController#show as HTML - Parameters: {"id" => "full_name_service"} -Completed 500 Internal Server Error in 0ms (ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) -Started GET "/servactory/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:29:38 +0700 -Processing by Servactory::Web::ServicesController#show as HTML - Parameters: {"id" => "not_existing_service"} - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 0.3ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 0.6ms | GC: 0.0ms) -Completed 200 OK in 1ms (Views: 0.8ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) -Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:30:42 +0700 -Processing by Servactory::Web::ServicesController#index as HTML - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 14.7ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 18.1ms | GC: 0.0ms) -Completed 200 OK in 41ms (Views: 20.1ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.3ms) -Started GET "/servactory/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:30:42 +0700 -Processing by Servactory::Web::ServicesController#show as HTML - Parameters: {"id" => "full_name_service"} - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 9.2ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 9.6ms | GC: 0.0ms) -Completed 200 OK in 10ms (Views: 9.7ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) -Started GET "/servactory/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:30:42 +0700 -Processing by Servactory::Web::ServicesController#show as HTML - Parameters: {"id" => "not_existing_service"} - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 0.1ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 0.3ms | GC: 0.0ms) -Completed 200 OK in 1ms (Views: 0.4ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) -Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:31:15 +0700 -Processing by Servactory::Web::ServicesController#index as HTML - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 14.8ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 18.1ms | GC: 0.0ms) -Completed 200 OK in 39ms (Views: 19.9ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.2ms) -Started GET "/servactory/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:31:15 +0700 -Processing by Servactory::Web::ServicesController#show as HTML - Parameters: {"id" => "full_name_service"} - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 9.1ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 9.4ms | GC: 0.0ms) -Completed 200 OK in 10ms (Views: 9.5ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) -Started GET "/servactory/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:31:15 +0700 -Processing by Servactory::Web::ServicesController#show as HTML - Parameters: {"id" => "not_existing_service"} - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 0.1ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 0.4ms | GC: 0.0ms) -Completed 200 OK in 1ms (Views: 0.5ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) -Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:31:22 +0700 -Processing by Servactory::Web::ServicesController#index as HTML - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 12.7ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 15.9ms | GC: 0.0ms) -Completed 200 OK in 32ms (Views: 17.3ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.3ms) -Started GET "/servactory/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:31:22 +0700 -Processing by Servactory::Web::ServicesController#show as HTML - Parameters: {"id" => "full_name_service"} - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 11.6ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 12.0ms | GC: 0.0ms) -Completed 200 OK in 13ms (Views: 12.2ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) -Started GET "/servactory/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:31:22 +0700 -Processing by Servactory::Web::ServicesController#show as HTML - Parameters: {"id" => "not_existing_service"} - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 0.1ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 0.4ms | GC: 0.0ms) -Completed 200 OK in 1ms (Views: 0.5ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) -Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:31:27 +0700 -Processing by Servactory::Web::ServicesController#index as HTML - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 10.9ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 13.7ms | GC: 0.0ms) -Completed 200 OK in 28ms (Views: 15.0ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.2ms) -Started GET "/servactory/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:31:27 +0700 -Processing by Servactory::Web::ServicesController#show as HTML - Parameters: {"id" => "full_name_service"} - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 7.1ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 7.4ms | GC: 0.0ms) -Completed 200 OK in 8ms (Views: 7.5ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) -Started GET "/servactory/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:31:27 +0700 -Processing by Servactory::Web::ServicesController#show as HTML - Parameters: {"id" => "not_existing_service"} - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 0.1ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 0.3ms | GC: 0.0ms) -Completed 200 OK in 1ms (Views: 0.4ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) -Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:31:36 +0700 -Processing by Servactory::Web::ServicesController#index as HTML - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 12.0ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 15.1ms | GC: 0.0ms) -Completed 200 OK in 31ms (Views: 16.6ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.2ms) -Started GET "/servactory/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:31:36 +0700 -Processing by Servactory::Web::ServicesController#show as HTML - Parameters: {"id" => "full_name_service"} - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 8.5ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 8.9ms | GC: 0.0ms) -Completed 200 OK in 9ms (Views: 9.1ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) -Started GET "/servactory/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:31:36 +0700 -Processing by Servactory::Web::ServicesController#show as HTML - Parameters: {"id" => "not_existing_service"} - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 0.1ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 0.4ms | GC: 0.0ms) -Completed 200 OK in 1ms (Views: 0.6ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) -Started GET "/servactory/services" for 127.0.0.1 at 2025-07-07 01:31:50 +0700 -Processing by Servactory::Web::ServicesController#index as HTML - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/index.html.erb within layouts/servactory/web/application (Duration: 4.7ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 7.5ms | GC: 0.0ms) -Completed 200 OK in 28ms (Views: 9.4ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) -Started GET "/servactory/services/full_name_service" for 127.0.0.1 at 2025-07-07 01:31:50 +0700 -Processing by Servactory::Web::ServicesController#show as HTML - Parameters: {"id" => "full_name_service"} - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 6.8ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 7.1ms | GC: 0.0ms) -Completed 200 OK in 8ms (Views: 7.3ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) -Started GET "/servactory/services/not_existing_service" for 127.0.0.1 at 2025-07-07 01:31:50 +0700 -Processing by Servactory::Web::ServicesController#show as HTML - Parameters: {"id" => "not_existing_service"} - Rendering layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb - Rendering /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application - Rendered /Users/profox/Projects/servactory/servactory-web/app/views/servactory/web/services/show.html.erb within layouts/servactory/web/application (Duration: 0.1ms | GC: 0.0ms) - Rendered layout /Users/profox/Projects/servactory/servactory-web/app/views/layouts/servactory/web/application.html.erb (Duration: 0.4ms | GC: 0.0ms) -Completed 200 OK in 1ms (Views: 0.5ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms) diff --git a/spec/servactory/web/ui_kit/organisms/attribute_list_component_spec.rb b/spec/servactory/web/ui_kit/organisms/attribute_list_component_spec.rb index d662eff..542bd48 100644 --- a/spec/servactory/web/ui_kit/organisms/attribute_list_component_spec.rb +++ b/spec/servactory/web/ui_kit/organisms/attribute_list_component_spec.rb @@ -34,6 +34,6 @@ empty_message: "No attributes" ) ) - expect(page).not_to have_text("No attributes") # Empty state is not rendered here, only in SectionCardComponent + expect(page).to have_no_text("No attributes") # Empty state is not rendered here, only in SectionCardComponent end end diff --git a/spec/servactory/web/ui_kit/organisms/code_block_component_spec.rb b/spec/servactory/web/ui_kit/organisms/code_block_component_spec.rb index e57c649..4b7dcb8 100644 --- a/spec/servactory/web/ui_kit/organisms/code_block_component_spec.rb +++ b/spec/servactory/web/ui_kit/organisms/code_block_component_spec.rb @@ -17,6 +17,6 @@ it "does not render copy button if copy_button is false" do render_inline(described_class.new(code: "puts 1", copy_button: false)) - expect(page).not_to have_button("Copy") + expect(page).to have_no_button("Copy") end end diff --git a/spec/servactory/web/ui_kit/organisms/page_header_component_spec.rb b/spec/servactory/web/ui_kit/organisms/page_header_component_spec.rb index de9a202..886cfcf 100644 --- a/spec/servactory/web/ui_kit/organisms/page_header_component_spec.rb +++ b/spec/servactory/web/ui_kit/organisms/page_header_component_spec.rb @@ -4,7 +4,7 @@ it "renders title only", :aggregate_failures do render_inline(described_class.new(title: "My Title")) expect(page).to have_css("h2.text-3xl.font-bold", text: "My Title") - expect(page).not_to have_css("p.text-gray-600.text-base") + expect(page).to have_no_css("p.text-gray-600.text-base") end it "renders title and description", :aggregate_failures do diff --git a/spec/servactory/web/ui_kit/organisms/tree_component_spec.rb b/spec/servactory/web/ui_kit/organisms/tree_component_spec.rb index fdad10e..7910779 100644 --- a/spec/servactory/web/ui_kit/organisms/tree_component_spec.rb +++ b/spec/servactory/web/ui_kit/organisms/tree_component_spec.rb @@ -9,7 +9,7 @@ end it "renders tree with directories and files", :aggregate_failures do - render_inline(described_class.new(nodes:)) + render_inline(described_class.new(nodes:, route_type: :internal)) expect(page).to have_css("nav[aria-label='Services navigation']") expect(page).to have_text("Dir1") expect(page).to have_text("File1") @@ -19,7 +19,8 @@ end it "applies custom class_name and options" do - render_inline(described_class.new(nodes:, class_name: "mb-4", options: { class: "bg-gray-100" })) + render_inline(described_class.new(nodes:, class_name: "mb-4", options: { class: "bg-gray-100" }, + route_type: :internal)) expect(page).to have_css(".mb-4.bg-gray-100") end end diff --git a/spec/servactory/web/ui_kit/organisms/tree_node_component_spec.rb b/spec/servactory/web/ui_kit/organisms/tree_node_component_spec.rb index 936b430..3e07ed3 100644 --- a/spec/servactory/web/ui_kit/organisms/tree_node_component_spec.rb +++ b/spec/servactory/web/ui_kit/organisms/tree_node_component_spec.rb @@ -6,7 +6,7 @@ let(:url_helpers) { Servactory::Web::Engine.routes.url_helpers } it "renders directory node with children", :aggregate_failures do - render_inline(described_class.new(node: directory_node)) + render_inline(described_class.new(node: directory_node, route_type: :internal)) expect(page).to have_text("Dir") expect(page).to have_css("li[role='treeitem'][aria-expanded='true']") expect(page).to have_css("svg.size-4.text-amber-600") @@ -14,15 +14,14 @@ end it "renders file node with link", :aggregate_failures do - allow(url_helpers).to receive(:service_path).and_return("/services/file") - render_inline(described_class.new(node: file_node)) + render_inline(described_class.new(node: file_node, route_type: :internal)) expect(page).to have_text("File") expect(page).to have_css("svg.size-3.text-gray-500") - expect(page).to have_link("File", href: "/services/file") + expect(page).to have_link("File", href: url_helpers.internal_service_path(file_node[:path])) end it "applies border class for nested level" do - render_inline(described_class.new(node: file_node, level: 1)) + render_inline(described_class.new(node: file_node, level: 1, route_type: :internal)) expect(page).to have_css(".border-l.border-dashed.border-gray-300.pl-4") end end