Class: Pilipinas::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/pilipinas/base.rb

Overview

Abstract base class for all Philippine geographic entities.

Concrete subclasses (Region, Province, City, Barangay) must implement Base.data_file to return the absolute path of their backing YAML file. Subclasses may also override Base.build to handle extra attributes.

Memory model

YAML is parsed exactly once per class per process. Cache stores three structures for each entity type:

  • :records — frozen Array of all instances in file order.

  • :by_code — frozen Hash keyed by down-cased code for O(1) look-ups.

  • :by_name — frozen Hash keyed by down-cased name for O(1) look-ups.

Associated sub-collections (e.g. a Region’s Provinces) are also cached so repeated calls like region.provinces are free after the first call.

Thread-safety

All shared state is managed through Cache, which uses a Mutex with double-checked locking. Entity objects are frozen value objects and are therefore inherently thread-safe.

Direct Known Subclasses

Barangay, City, Province, Region

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(code:, name:) ⇒ Base

Builds a frozen, immutable value object.

Parameters:

  • code (String, Integer, #to_s)

    geographic code

  • name (String, #to_s)

    human-readable name



44
45
46
47
48
# File 'lib/pilipinas/base.rb', line 44

def initialize(code:, name:)
  @code = code.to_s.freeze
  @name = name.to_s.freeze
  freeze
end

Instance Attribute Details

#codeString (readonly)

Returns geographic code (always a String, never nil).

Returns:

  • (String)

    geographic code (always a String, never nil)



35
36
37
# File 'lib/pilipinas/base.rb', line 35

def code
  @code
end

#nameString (readonly)

Returns human-readable name (always a String, never nil).

Returns:

  • (String)

    human-readable name (always a String, never nil)



38
39
40
# File 'lib/pilipinas/base.rb', line 38

def name
  @name
end

Class Method Details

.allArray<Base>

Returns every record for this entity type.

The underlying YAML file is parsed at most once; subsequent calls return the same frozen Array from Cache.

Returns:



84
85
86
# File 'lib/pilipinas/base.rb', line 84

def all
  index[:records]
end

.assoc_collection(code:, dir:) ⇒ Array<Base>

Load an associated sub-collection from a per-code YAML file.

Results are cached in Cache; repeated calls with the same arguments are zero-cost after the first invocation.

Instances are resolved from the canonical index rather than freshly constructed, so no duplicate objects exist in memory when both the full collection (e.g. Barangay.all) and an association (e.g. city.barangays) are accessed in the same process.

Parameters:

  • code (String, Integer)

    parent entity’s code

  • dir (Symbol, String)

    sub-directory name under data/

Returns:



142
143
144
145
146
147
148
149
150
151
152
# File 'lib/pilipinas/base.rb', line 142

def assoc_collection(code:, dir:)
  # Pre-warm the canonical index outside the Cache.fetch block.
  # If the index is not yet cached, this call acquires and releases the
  # mutex.  Once warm, find_by inside the block hits the lock-free fast
  # path, preventing recursive locking (deadlock) on the same Mutex.
  index
  Cache.fetch("assoc:#{name}:#{code}") do
    file = File.join(Pilipinas::DATA_DIR, dir.to_s, "#{code}.yml")
    load_yaml(file).filter_map { |h| find_by(code: h[:code]) }.freeze
  end
end

.countInteger

Total number of records.

Returns:

  • (Integer)


91
92
93
# File 'lib/pilipinas/base.rb', line 91

def count
  index[:records].size
end

.find_by(options) ⇒ Base?

Find a single record by one attribute.

Look-up is O(1) via a pre-built hash index and is case-insensitive.

Examples:

Pilipinas::Region.find_by(code: "17744")
Pilipinas::Region.find_by(name: "REGION V (Bicol Region)")

Parameters:

  • options (Hash{Symbol => String, Integer})

    Exactly one key/value pair. Supported keys: :code, :name.

Returns:

Raises:

  • (ArgumentError)

    if options is empty

  • (UnknownAttribute)

    if the key is not :code or :name



122
123
124
125
126
127
# File 'lib/pilipinas/base.rb', line 122

def find_by(options)
  raise ArgumentError, 'options hash must not be empty' if options.empty?

  attribute, value = options.first
  find_by_attribute(attribute.to_sym, value.to_s)
end

.find_by_code(value) ⇒ Base?

Find a record whose code matches value (case-insensitive).

Parameters:

  • value (String, Integer)

Returns:



# File 'lib/pilipinas/base.rb', line 163

.find_by_name(value) ⇒ Base?

Find a record whose name matches value (case-insensitive).

Parameters:

  • value (String)

Returns:



# File 'lib/pilipinas/base.rb', line 168

.firstBase?

First record in collection order.

Returns:



98
99
100
# File 'lib/pilipinas/base.rb', line 98

def first
  index[:records].first
end

.lastBase?

Last record in collection order.

Returns:



105
106
107
# File 'lib/pilipinas/base.rb', line 105

def last
  index[:records].last
end

.method_missing(method_name, *args, **_kwargs) ⇒ Base?

Handles dynamic find_by_<attribute> methods.

Returns:

Raises:



177
178
179
180
181
182
183
184
185
186
# File 'lib/pilipinas/base.rb', line 177

def method_missing(method_name, *args, **_kwargs, &)
  match = method_name.to_s.match(/\Afind_by_(.+)\z/)
  return super unless match

  attribute = match[1].to_sym
  raise UnknownAttribute, "Invalid attribute '#{attribute}'." \
    unless %i[code name].include?(attribute)

  find_by_attribute(attribute, args.first.to_s)
end

.reset_cachevoid

This method returns an undefined value.

Clear all cached data.

Primarily useful between test examples to guarantee isolation.



159
160
161
# File 'lib/pilipinas/base.rb', line 159

def reset_cache
  Cache.clear
end

.respond_to_missing?(method_name, include_private = false) ⇒ Boolean

Parameters:

  • method_name (Symbol)
  • include_private (Boolean) (defaults to: false)

Returns:

  • (Boolean)


191
192
193
# File 'lib/pilipinas/base.rb', line 191

def respond_to_missing?(method_name, include_private = false)
  method_name.to_s.match?(/\Afind_by_(code|name)\z/) || super
end

Instance Method Details

#==(other) ⇒ Boolean Also known as: eql?

Value equality based on class and code.

Parameters:

  • other (Object)

Returns:

  • (Boolean)


64
65
66
# File 'lib/pilipinas/base.rb', line 64

def ==(other)
  other.is_a?(self.class) && other.code == @code
end

#hashInteger

Returns:

  • (Integer)


71
72
73
# File 'lib/pilipinas/base.rb', line 71

def hash
  [self.class, @code].hash
end

#inspectString

Returns:

  • (String)


56
57
58
# File 'lib/pilipinas/base.rb', line 56

def inspect
  "#<#{self.class.name} code=#{@code.inspect} name=#{@name.inspect}>"
end

#to_sString

Returns:

  • (String)


51
52
53
# File 'lib/pilipinas/base.rb', line 51

def to_s
  "#{self.class.name.split('::').last}(code: #{@code}, name: #{@name})"
end