Module: Clacky::ApiExtensionLoader

Defined in:
lib/clacky/extension/api_loader.rb

Overview

Loads HTTP API extensions from ext.yml containers into the shared Clacky::ApiExtension registry, keyed by ext_id. Every ext contributes at most one api unit (contributes.api: <path> in ext.yml), so the registry key is simply the ext id.

A broken extension (syntax error, missing base class, no routes) is isolated: skipped with a logged warning, never aborts sibling loads.

Defined Under Namespace

Classes: Result

Class Method Summary collapse

Class Method Details

.ensure_fresh(ext_id) ⇒ Object

Per-request hot path used by the dispatcher. Re-loads the handler only when its file mtime has changed since the last load.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/clacky/extension/api_loader.rb', line 39

def ensure_fresh(ext_id)
  loader_result = Clacky::ExtensionLoader.load_all
  unit = loader_result.api.find { |u| u.ext_id == ext_id.to_s }
  return unless unit

  path = unit.spec["handler_abs"]
  current_mtime = File.mtime(path).to_f
  cache = handler_mtime_cache
  return if cache[ext_id.to_s] == current_mtime

  container = loader_result.containers[unit.ext_id]
  r = Result.new(loaded: [], skipped: [])
  load_one(unit.ext_id, path, unit.dir, container, r, reload: true)
  r.skipped.each { |(id, reason)| log_skip(id, reason) }
  cache[ext_id.to_s] = current_mtime
rescue StandardError => e
  Clacky::Logger.warn("[ApiExtensionLoader] ensure_fresh(#{ext_id}) failed: #{e.message}")
end

.last_resultObject



33
34
35
# File 'lib/clacky/extension/api_loader.rb', line 33

def last_result
  @last_result || load_all
end

.load_all(reload: false) ⇒ Object



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/clacky/extension/api_loader.rb', line 15

def load_all(reload: false)
  result = Result.new(loaded: [], skipped: [])
  Clacky::ApiExtension.reset_registry!
  handler_mtime_cache.clear

  loader_result = Clacky::ExtensionLoader.load_all
  loader_result.api.each do |unit|
    container = loader_result.containers[unit.ext_id]
    load_one(unit.ext_id, unit.spec["handler_abs"], unit.dir, container, result, reload: reload)
  end

  @last_result = result
  log_summary(result)
  result
end

.load_one(ext_id, handler_path, ext_dir, container, result, reload: false) ⇒ Object



62
63
64
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/clacky/extension/api_loader.rb', line 62

def load_one(ext_id, handler_path, ext_dir, container, result, reload: false)
  before = Clacky::ApiExtension.pending_subclasses.size

  existing = reload ? Clacky::ApiExtension.registry[ext_id] : nil
  existing&.reset_routes!

  if reload
    old_verbose = $VERBOSE
    $VERBOSE = nil
    begin
      load(handler_path)
    ensure
      $VERBOSE = old_verbose
    end
  else
    require(handler_path)
  end

  new_subclasses = Clacky::ApiExtension.pending_subclasses[before..] || []
  klass = new_subclasses.last || existing

  unless klass
    result.skipped << [ext_id, "no Clacky::ApiExtension subclass defined in handler.rb"]
    log_skip(ext_id, result.skipped.last[1])
    return
  end

  klass.ext_id  = ext_id
  klass.ext_dir = ext_dir
  klass.meta    = (container && container[:raw]) || {}

  if klass.routes.empty?
    result.skipped << [ext_id, "no routes declared (use get/post/... DSL)"]
    log_skip(ext_id, result.skipped.last[1])
    Clacky::ApiExtension.registry.delete(ext_id)
    return
  end

  if (gap = validate_public_endpoints(klass, container))
    result.skipped << [ext_id, gap]
    log_skip(ext_id, gap)
    return
  end

  Clacky::ApiExtension.register(ext_id, klass)
  result.loaded << ext_id
  handler_mtime_cache[ext_id] = File.mtime(handler_path).to_f rescue nil
  public_count = klass.public_paths.size
  suffix = public_count > 0 ? " (#{public_count} public)" : ""
  Clacky::Logger.info("[ApiExtensionLoader] Loaded '#{ext_id}' — #{klass.routes.size} route(s)#{suffix}")
rescue StandardError, ScriptError => e
  result.skipped << [ext_id, e.message]
  log_skip(ext_id, e.message)
end