File Coverage

blib/lib/Text/Names/Canonicalize/Rules.pm
Criterion Covered Total %
statement 72 72 100.0
branch 27 44 61.3
condition 7 11 63.6
subroutine 12 12 100.0
pod 0 1 0.0
total 118 140 84.2


line stmt bran cond sub pod time code
1             package Text::Names::Canonicalize::Rules;
2              
3 11     11   74 use strict;
  11         43  
  11         492  
4 11     11   60 use warnings;
  11         34  
  11         843  
5 11     11   63 use Carp qw(croak);
  11         20  
  11         822  
6 11     11   3997 use YAML::XS qw(LoadFile);
  11         30994  
  11         819  
7 11     11   81 use File::Spec;
  11         20  
  11         356  
8 11     11   51 use File::Basename qw(dirname);
  11         22  
  11         10253  
9              
10             # ----------------------------------------------------------------------
11             # Load a YAML ruleset if available.
12             # YAML files live in:
13             # lib/Text/Names/Canonicalize/Rules/.yaml
14             # ----------------------------------------------------------------------
15             sub _load_yaml_rules {
16 56     56   112 my ($locale) = @_;
17              
18             # __FILE__ = .../Text/Names/Canonicalize/Rules.pm
19             # YAML lives in .../Text/Names/Canonicalize/Rules/*.yaml
20              
21 56         3079 my $base = File::Spec->catdir( dirname(__FILE__), 'Rules' );
22 56         551 my $file = File::Spec->catfile( $base, "$locale.yaml" );
23              
24 56 50       1581 return unless -e $file;
25              
26 56         107 my $yaml = eval { LoadFile($file) };
  56         187  
27 56 50       8677 croak "Failed to load YAML rules for $locale: $@" if $@;
28              
29 56 50       183 croak "YAML rules for $locale must be a hash" unless ref $yaml eq 'HASH';
30              
31 56         170 return $yaml;
32             }
33              
34             # ----------------------------------------------------------------------
35             # Fetch a ruleset:
36             # 1. Try YAML
37             # 2. Fall back to Perl registry
38             # ----------------------------------------------------------------------
39             sub get {
40 25     25 0 73 my ($class, $locale, $ruleset) = @_;
41 25   50     66 $ruleset ||= 'default';
42              
43             # Built-in YAML (may be undef)
44 25         93 my $builtin = _load_yaml_rules($locale);
45 25 50       97 $builtin = _resolve_includes($locale, $ruleset, $builtin) if $builtin;
46              
47             # User override YAML (may be undef)
48 25         65 my $user = _load_user_yaml_rules($locale);
49 25 100       97 $user = _resolve_includes($locale, $ruleset, $user) if $user;
50              
51             # Extract rulesets
52 25 50       94 my $builtin_rules = $builtin ? $builtin->{$ruleset} : undef;
53 25 100       77 my $user_rules = $user ? $user->{$ruleset} : undef;
54              
55 25 50 33     72 croak "Ruleset '$ruleset' not found for locale '$locale'"
56             unless $builtin_rules || $user_rules;
57              
58             # Merge: user overrides built-in
59 25   50     148 my $merged = _merge_rules($builtin_rules || {}, $user_rules || {});
      100        
60              
61 25         131 return $merged;
62             }
63              
64             sub _user_rules_dir {
65              
66             # If CONFIG_DIR is set, use:
67             # $CONFIG_DIR/text-names-canonicalize/rules
68 25 100   25   90 if ($ENV{CONFIG_DIR}) {
69             return File::Spec->catdir(
70             $ENV{CONFIG_DIR},
71 1         17 'text-names-canonicalize',
72             'rules'
73             );
74             }
75              
76             # Otherwise use:
77             # ~/.config/text-names-canonicalize/rules
78 24 50       78 my $home = $ENV{HOME} or return;
79 24         305 return File::Spec->catdir(
80             $home,
81             '.config',
82             'text-names-canonicalize',
83             'rules'
84             );
85             }
86              
87             sub _load_user_yaml_rules {
88 25     25   88 my ($locale) = @_;
89              
90 25 50       69 my $dir = _user_rules_dir() or return;
91 25         200 my $file = File::Spec->catfile($dir, "$locale.yaml");
92              
93 25 100       427 return unless -e $file;
94              
95 1         2 my $yaml = eval { LoadFile($file) };
  1         3  
96 1 50       99 croak "Failed to load user YAML rules for $locale: $@" if $@;
97              
98 1         3 return $yaml;
99             }
100              
101             sub _merge_rules {
102 56     56   113 my ($base, $override) = @_;
103 56 50       115 return $base unless $override;
104              
105 56         304 my %merged = (%$base, %$override);
106 56         235 return \%merged;
107             }
108              
109             sub _resolve_includes {
110 57     57   182 my ($locale, $ruleset, $yaml, $seen) = @_;
111 57 50       127 return $yaml unless $yaml;
112              
113 57   100     200 $seen ||= {};
114              
115 57 50       146 my $spec = $yaml->{$ruleset}
116             or return $yaml;
117              
118 57         92 my @parents;
119 57 100       141 if (exists $spec->{include}) {
120 31         55 my $inc = $spec->{include};
121              
122 31 50       136 @parents =
    50          
123             ref $inc eq 'ARRAY' ? @$inc :
124             ref $inc eq '' ? ($inc) :
125             croak "Invalid include format in $locale/$ruleset";
126             }
127              
128 57         108 delete $spec->{include};
129              
130 57         118 for my $parent (@parents) {
131              
132             # CIRCULAR INCLUDE DETECTION
133             croak "Circular include detected: $locale → $parent"
134 31 50       72 if $seen->{$parent};
135              
136 31         72 $seen->{$parent} = 1;
137              
138 31 50       53 my $parent_yaml = _load_yaml_rules($parent)
139             or croak "Included locale '$parent' not found";
140              
141 31         111 $parent_yaml = _resolve_includes($parent, $ruleset, $parent_yaml, $seen);
142              
143 31 50       96 my $parent_rules = $parent_yaml->{$ruleset}
144             or croak "Included ruleset '$ruleset' not found in '$parent'";
145              
146 31         73 $spec = _merge_rules($parent_rules, $spec);
147             }
148              
149 57         120 $yaml->{$ruleset} = $spec;
150 57         141 return $yaml;
151             }
152              
153             1;