Skip to content

Commit 11c4d6a

Browse files
committed
Dynamically resolve extension constants for installed extensions.
1 parent 278de92 commit 11c4d6a

File tree

1 file changed

+101
-0
lines changed

1 file changed

+101
-0
lines changed

lib/sqlpkg.rb

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,107 @@
11
# frozen_string_literal: true
22

33
module Sqlpkg
4+
DIR = ".sqlpkg"
5+
FILE_PATTERN = "*.{dylib,so,dll}"
6+
7+
# Helper methods
8+
module H
9+
extend self
10+
11+
# Don't depend on ActiveSupport just to convert a constant name to a path segment.
12+
# Handles acronyms and camelcase constants, e.g. UUID => uuid and SomeName => some_name
13+
def constant_to_path(constant_name)
14+
constant_name.to_s
15+
.gsub("::", "/")
16+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
17+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
18+
.tr("-", "_")
19+
.downcase
20+
end
21+
end
22+
23+
# The directory where `sqlpkg` stores installed extensions
24+
# => "./.sqlpkg"
25+
def self.file_dir
26+
File.join(__dir__, DIR)
27+
end
28+
29+
# List of file paths for all installed extensions
30+
# => ["./.sqlpkg/asg017/ulid/ulid0.dylib", "./.sqlpkg/nalgeon/uuid/uuid.dylib"]
31+
def self.paths
32+
Dir.glob File.join(file_dir, "**", FILE_PATTERN)
33+
end
34+
35+
# Dynamically resolve extension constants for installed extensions.
36+
# Allows this gem to work with the new sqlite3[https://sparklemotion.github.io/sqlite3-ruby/]
37+
# database configuration option `extensions:`, which allows an application to load extensions
38+
# when using `sqlite3` >= v2.4.0. The array members may be filesystem paths or the names of
39+
# modules that respond to `.to_path`. So, we dynamically build constants that respond to `.to_path`.
40+
# Allow either direct access `Sqlpkg::UUID` or owner-scoped access `Sqlpkg::Nalgeon::UUID`.
41+
def self.const_missing(const)
42+
path_segment = H.constant_to_path(const)
43+
is_owner = false
44+
path = paths.find do |it|
45+
_path, pkg_path = it.split("#{DIR}#{File::SEPARATOR}")
46+
owner, name, _file = pkg_path.split(File::SEPARATOR)
47+
48+
if path_segment == owner
49+
is_owner = true
50+
else
51+
path_segment == name
52+
end
53+
end
54+
55+
return super unless path
56+
57+
if is_owner
58+
# Dynamically generated module for the extension owner.
59+
# This module has `.file_dir`, `.paths`, and `.const_missing` defined.
60+
# Any dynamically generated classes _must be_ installed extensions
61+
# for this owner.
62+
const_set(const, Module.new do
63+
# The directory where `sqlpkg` stores installed extensions for this owner
64+
# => "./.sqlpkg/nalgeon"
65+
def self.file_dir
66+
File.join(__dir__, ".#{H.constant_to_path(self.name)}")
67+
end
68+
69+
# List of file paths for all installed extensions belonging to this owner
70+
# => ["./.sqlpkg/nalgeon/uuid/uuid.dylib"]
71+
def self.paths
72+
Dir.glob File.join(file_dir, "**", FILE_PATTERN)
73+
end
74+
75+
def self.const_missing(const)
76+
path_segment = H.constant_to_path(const)
77+
path = paths.find do |it|
78+
_path, pkg_path = it.split("#{DIR}#{File::SEPARATOR}")
79+
_owner, name, _file = pkg_path.split(File::SEPARATOR)
80+
81+
path_segment == name
82+
end
83+
84+
return super unless path
85+
86+
# Dynamically generated module for the extension.
87+
# This class only has `.to_path` defined.
88+
const_set(const, Class.new do
89+
define_singleton_method :to_path do
90+
path
91+
end
92+
end)
93+
end
94+
end)
95+
else
96+
# Dynamically generated module for the extension.
97+
# This class only has `.to_path` defined.
98+
const_set(const, Class.new do
99+
define_singleton_method :to_path do
100+
path
101+
end
102+
end)
103+
end
104+
end
4105
end
5106

6107
require_relative "sqlpkg/version"

0 commit comments

Comments
 (0)