Class: Autotype::DiscoveryProfile

Inherits:
Object
  • Object
show all
Defined in:
lib/autotype/discovery_profile.rb

Overview

Infers framework wiring, entity layout, and type hints from Ruby source and the filesystem. Optional autotype.yml supplies skip globs and overrides when heuristics are wrong — nothing else is required.

Constant Summary collapse

PORT_CALLS =
%i[input output].freeze
OPTION_CALLS =
%i[option].freeze
FIELD_CALLS =
%i[field].freeze
HANDLER_METHOD_CANDIDATES =
%i[process handle call perform run execute].freeze
DISPATCH_PARAM_CANDIDATES =
%i[from source port input_name].freeze
EMIT_MESSAGES =
%i[emit publish send emit_entity].freeze
EMIT_PORT_KEYWORDS =
%i[to port output].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root: Dir.pwd, overrides: {}, config_path: nil) ⇒ DiscoveryProfile

Returns a new instance of DiscoveryProfile.



37
38
39
40
41
42
43
44
45
# File 'lib/autotype/discovery_profile.rb', line 37

def initialize(root: Dir.pwd, overrides: {}, config_path: nil)
  @root = File.expand_path(root)
  @overrides = stringify_keys(overrides)
  @config_path = config_path
  @search_roots = Set.new([@root])
  @port_dsl_owners = Set.new
  @referenced_constants = Set.new
  reset_inference!
end

Instance Attribute Details

#rootObject (readonly)

Returns the value of attribute root.



47
48
49
# File 'lib/autotype/discovery_profile.rb', line 47

def root
  @root
end

Class Method Details

.find(start_dir = Dir.pwd) ⇒ Object



23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/autotype/discovery_profile.rb', line 23

def self.find(start_dir = Dir.pwd)
  dir = File.expand_path(start_dir)
  loop do
    candidate = File.join(dir, "autotype.yml")
    return candidate if File.file?(candidate)

    parent = File.dirname(dir)
    break if parent == dir

    dir = parent
  end
  nil
end

.load_overrides(path, root: File.dirname(path)) ⇒ Object



18
19
20
21
# File 'lib/autotype/discovery_profile.rb', line 18

def self.load_overrides(path, root: File.dirname(path))
  raw = YAML.safe_load_file(path, aliases: true) || {}
  new(root: root, overrides: raw, config_path: path)
end

Instance Method Details

#config_hashObject



103
104
105
# File 'lib/autotype/discovery_profile.rb', line 103

def config_hash
  @config_hash
end

#entity_define_receiver?(receiver_slice) ⇒ Boolean

Returns:

  • (Boolean)


120
121
122
123
124
125
126
# File 'lib/autotype/discovery_profile.rb', line 120

def entity_define_receiver?(receiver_slice)
  return false unless receiver_slice

  receiver_slice == "Entity" ||
    receiver_slice == "Data" ||
    receiver_slice.end_with?("::Entity")
end

#finalize!(collectors) ⇒ Object



61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/autotype/discovery_profile.rb', line 61

def finalize!(collectors)
  methods = collectors.flat_map(&:methods)
  ports = merge_ports(collectors)

  @port_wiring ||= infer_port_wiring(methods, ports)
  @output_emit ||= infer_output_emit(methods, ports)
  @config_hash ||= infer_config_hash(methods, collectors)
  @side_effect_methods ||= infer_side_effect_methods(methods, ports)
  @structured_type_prefixes = infer_structured_prefixes(collectors, ports) if @structured_type_prefixes.empty?
  @framework_self_fallbacks = infer_self_fallbacks(methods, ports) if @framework_self_fallbacks.empty?
  collectors.each { collect_referenced_constants(_1) }
  @finalized = true
end

#framework_self_fallbacksObject



107
108
109
# File 'lib/autotype/discovery_profile.rb', line 107

def framework_self_fallbacks
  @framework_self_fallbacks || {}
end

#locate_type_file(type_name) ⇒ Object



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/autotype/discovery_profile.rb', line 159

def locate_type_file(type_name)
  return @type_path_cache[type_name] if @type_path_cache.key?(type_name)

  explicit = explicit_locator(type_name)
  return cache_type_path(type_name, explicit) if explicit

  basename = type_name.split("::").last
  snake = underscore(basename)
  candidates = @search_roots.flat_map do |search_root|
    Dir.glob(File.join(search_root, "**", "#{snake}.rb"))
  end.uniq

  path = candidates.find { defines_type?(_1, type_name, basename) }
  cache_type_path(type_name, path)
end

#member_type_hintsObject



79
80
81
# File 'lib/autotype/discovery_profile.rb', line 79

def member_type_hints
  @member_type_hints ||= parse_type_map(@overrides.dig("types", "members"))
end

#metadata_from(collector) ⇒ Object



144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/autotype/discovery_profile.rb', line 144

def (collector)
  InferenceMetadata.from_collector(
    collector,
    member_type_hints: member_type_hints,
    option_type_hints: option_type_hints,
    side_effect_methods: side_effect_methods,
    structured_type_prefixes: structured_type_prefixes,
    port_wiring: port_wiring,
    output_emit: output_emit,
    config_hash: config_hash,
    framework_self_fallbacks: framework_self_fallbacks,
    type_locators: [method(:locate_type_file)]
  )
end

#option_type_hintsObject



83
84
85
# File 'lib/autotype/discovery_profile.rb', line 83

def option_type_hints
  @option_type_hints ||= parse_type_map(@overrides.dig("types", "options"))
end

#output_emitObject



99
100
101
# File 'lib/autotype/discovery_profile.rb', line 99

def output_emit
  @output_emit
end

#pathObject



49
50
51
# File 'lib/autotype/discovery_profile.rb', line 49

def path
  @config_path
end

#pipeline_class?(collector, owner) ⇒ Boolean

Returns:

  • (Boolean)


111
112
113
114
115
116
117
118
# File 'lib/autotype/discovery_profile.rb', line 111

def pipeline_class?(collector, owner)
  return false if collector.singleton_context? || collector.namespace_empty?

  return true if @port_dsl_owners.include?(owner)

  superclass = collector.superclass_for(owner)
  superclass == "Actor" || superclass&.end_with?("::Actor")
end

#port_wiringObject



95
96
97
# File 'lib/autotype/discovery_profile.rb', line 95

def port_wiring
  @port_wiring
end

#prepare_search!(paths) ⇒ Object



53
54
55
56
57
58
59
# File 'lib/autotype/discovery_profile.rb', line 53

def prepare_search!(paths)
  paths.each do |file|
    next unless File.file?(file)

    @search_roots << find_project_root(File.dirname(File.expand_path(file)))
  end
end

#side_effect_methodsObject



87
88
89
# File 'lib/autotype/discovery_profile.rb', line 87

def side_effect_methods
  @side_effect_methods || Set.new
end

#skipped_filesObject



75
76
77
# File 'lib/autotype/discovery_profile.rb', line 75

def skipped_files
  Array(@overrides.fetch("skip", [])).to_set
end

#structured_type_prefixesObject



91
92
93
# File 'lib/autotype/discovery_profile.rb', line 91

def structured_type_prefixes
  @structured_type_prefixes || []
end

#visit_call_node(collector, node) ⇒ Object



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/autotype/discovery_profile.rb', line 128

def visit_call_node(collector, node)
  return unless node.receiver.nil?
  return if collector.singleton_context?

  owner = collector.current_owner
  if port_call?(node.name)
    record_port(collector, node)
    @port_dsl_owners << owner
  elsif option_call?(node.name)
    record_option(collector, node)
    @port_dsl_owners << owner
  elsif field_call?(node.name) && collector.structured_field_context?
    record_field(collector, node)
  end
end