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 @@
>
<% @nodes.each do |node| %>
- <%= render Servactory::Web::UiKit::Organisms::TreeNodeComponent.new(node: node, level: 0) %>
+ <%= render Servactory::Web::UiKit::Organisms::TreeNodeComponent.new(node: node, level: 0, route_type: @route_type, gem_name: @gem_name) %>
<% end %>
\ 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 @@
<% @node[:children].each do |child| %>
- <%= render Servactory::Web::UiKit::Organisms::TreeNodeComponent.new(node: child, level: @level + 1) %>
+ <%= render Servactory::Web::UiKit::Organisms::TreeNodeComponent.new(node: child, level: @level + 1, route_type: @route_type, gem_name: @gem_name) %>
<% end %>
@@ -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