Class: Woods::Extractors::PoroExtractor

Inherits:
Object
  • Object
show all
Includes:
SharedDependencyScanner, SharedUtilityMethods
Defined in:
lib/woods/extractors/poro_extractor.rb

Overview

PoroExtractor handles plain Ruby object extraction from app/models/.

Scans app/models/ for Ruby files that define classes which are NOT ActiveRecord descendants (those are handled by ModelExtractor). Captures value objects, form objects, CurrentAttributes subclasses, Struct.new wrappers, and any other non-AR class living alongside AR models.

Files under app/models/concerns/ are excluded — those are handled by ConcernExtractor. Module-only files are also excluded.

Examples:

extractor = PoroExtractor.new
units = extractor.extract_all
money = units.find { |u| u.identifier == "Money" }
money.[:parent_class]  # => nil
money.[:method_count]  # => 3

Constant Summary collapse

MODELS_GLOB =

Glob pattern for all Ruby files in app/models/ (recursive).

'app/models/**/*.rb'
CONCERNS_SEGMENT =

Subdirectory to exclude — handled by ConcernExtractor.

'/concerns/'

Constants included from SharedDependencyScanner

SharedDependencyScanner::FORM_ACTION_HELPER, SharedDependencyScanner::ROUTE_HELPER_PATTERN

Instance Method Summary collapse

Methods included from SharedDependencyScanner

#extract_constantize_targets, #scan_common_dependencies, #scan_form_dependencies, #scan_job_dependencies, #scan_mailer_dependencies, #scan_model_dependencies, #scan_navigation_dependencies, #scan_service_dependencies

Methods included from SharedUtilityMethods

#app_source?, #condition_label, #count_loc, #detect_entry_points, #extract_action_filter_actions, #extract_callback_conditions, #extract_class_methods, #extract_class_name, #extract_custom_errors, #extract_initialize_params, #extract_namespace, #extract_parent_class, #extract_public_methods, #resolve_source_location, #skip_file?

Constructor Details

#initializePoroExtractor

Returns a new instance of PoroExtractor.



35
36
37
# File 'lib/woods/extractors/poro_extractor.rb', line 35

def initialize
  @models_dir = Rails.root.join('app/models')
end

Instance Method Details

#extract_allArray<ExtractedUnit>

Extract all PORO units from app/models/.

Filters out ActiveRecord descendants by name so we don’t duplicate what ModelExtractor already produces. Concerns/ subdir is also skipped.

Returns:



45
46
47
48
49
50
51
52
53
54
55
# File 'lib/woods/extractors/poro_extractor.rb', line 45

def extract_all
  return [] unless @models_dir.directory?

  ar_names = ActiveRecord::Base.descendants.filter_map(&:name).to_set

  Dir[Rails.root.join(MODELS_GLOB)].filter_map do |file|
    next if file.include?(CONCERNS_SEGMENT)

    extract_poro_file(file, ar_names: ar_names)
  end
end

#extract_poro_file(file_path, ar_names: Set.new) ⇒ ExtractedUnit?

Extract a single PORO file.

Returns nil if the file is not a PORO (e.g., module-only, no class or PORO pattern found, or the inferred class is an AR descendant).

Parameters:

  • file_path (String)

    Absolute path to the Ruby file

  • ar_names (Set<String>) (defaults to: Set.new)

    Set of AR descendant names to skip

Returns:



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/woods/extractors/poro_extractor.rb', line 65

def extract_poro_file(file_path, ar_names: Set.new)
  source = File.read(file_path)

  return nil unless poro_file?(source)
  return nil if module_only?(source)

  class_name = infer_class_name(file_path, source)
  return nil unless class_name
  return nil if ar_names.include?(class_name)

  unit = ExtractedUnit.new(
    type: :poro,
    identifier: class_name,
    file_path: file_path
  )

  unit.namespace    = extract_namespace(class_name)
  unit.source_code  = annotate_source(source, class_name)
  unit.     = (source, class_name)
  unit.dependencies = extract_dependencies(source)

  unit
rescue StandardError => e
  Rails.logger.error("Failed to extract PORO #{file_path}: #{e.message}")
  nil
end