Class: Vehicles::Dataset

Inherits:
Object
  • Object
show all
Defined in:
lib/vehicles/dataset.rb

Overview

Loads the bundled snapshot once, builds in-memory indexes, and answers every query. No HTTP, no SQLite, no ActiveRecord on the read path — the first call builds the index, every call after is a hash lookup.

Instances are memoized per data path (see .load), so the JSON is parsed once per process.

Constant Summary collapse

BUILTIN_ALIASES =

Built-in make aliases (normalized key => make slug). Common abbreviations and nicknames so whatever a user types tends to land. Diacritics and case are already handled by Vehicles.normalize, so “škoda”/“citroën” need no entry.

{
  "vw" => "volkswagen", "vdub" => "volkswagen",
  "merc" => "mercedes-benz", "mercedes" => "mercedes-benz", "benz" => "mercedes-benz",
  "mb" => "mercedes-benz",
  "chevy" => "chevrolet",
  "beemer" => "bmw", "bimmer" => "bmw",
  "alfa" => "alfa-romeo",
  "landrover" => "land-rover", "range rover" => "land-rover", "rangerover" => "land-rover",
  "vauxhall" => "opel" # GB badge-engineered Opel; map to the EU make we ship
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(raw) ⇒ Dataset

Returns a new instance of Dataset.



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/vehicles/dataset.rb', line 41

def initialize(raw)
  @version        = raw["version"]
  @schema_version = raw["schema_version"]
  @region         = raw["region"]

  @makes    = (raw["makes"] || []).map { |attrs| Make.new(attrs) }
  @by_slug  = {}   # raw slug => Make
  @index    = {}   # normalized name/slug/alias => Make
  @makes.each do |make|
    @by_slug[make.slug] = make
    index(make.name, make)
    index(make.slug, make)
    make.aliases.each { |a| index(a, make) }
  end
end

Instance Attribute Details

#regionObject (readonly)

Returns the value of attribute region.



39
40
41
# File 'lib/vehicles/dataset.rb', line 39

def region
  @region
end

#schema_versionObject (readonly)

Returns the value of attribute schema_version.



39
40
41
# File 'lib/vehicles/dataset.rb', line 39

def schema_version
  @schema_version
end

#versionObject (readonly)

Returns the value of attribute version.



39
40
41
# File 'lib/vehicles/dataset.rb', line 39

def version
  @version
end

Class Method Details

.load(path = Vehicles.data_path) ⇒ Object

Memoized per path so the bundled JSON is parsed only once per process.



29
30
31
# File 'lib/vehicles/dataset.rb', line 29

def load(path = Vehicles.data_path)
  (@instances ||= {})[path] ||= new(JSON.parse(File.read(path)))
end

.reset!Object

Drop the cache (used by the test suite between runs).



34
35
36
# File 'lib/vehicles/dataset.rb', line 34

def reset!
  @instances = {}
end

Instance Method Details

#all_modelsObject

Flat list of every Model (memoized + frozen — shared, so don’t let callers mutate it). Backs ‘search`.



128
129
130
# File 'lib/vehicles/dataset.rb', line 128

def all_models
  @all_models ||= @makes.flat_map(&:models).freeze
end

#find_make(query) ⇒ Object

Resolve a make from a String/Symbol/Make via aliases, slug, or name.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/vehicles/dataset.rb', line 68

def find_make(query)
  return query if query.is_a?(Make)

  q = Vehicles.normalize(query)
  return nil if q.empty?

  # 1. user-supplied aliases win
  if (canonical = Vehicles.configuration.aliases[q])
    return @by_slug[Vehicles.slugify(canonical)] || @index[Vehicles.normalize(canonical)]
  end
  # 2. built-in aliases
  if (slug = BUILTIN_ALIASES[q])
    return @by_slug[slug]
  end

  # 3. direct slug / normalized name / make alias
  @by_slug[q] || @index[q]
end

#find_model(query) ⇒ Object

Resolve a free-text “make + model” string into one Model. Tries the longest leading make prefix first (“land rover defender”), then the remainder as the model. Returns nil if nothing matches.



90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/vehicles/dataset.rb', line 90

def find_model(query)
  q = Vehicles.normalize(query)
  tokens = q.split
  return nil if tokens.empty?

  (tokens.length - 1).downto(1) do |i|
    make = find_make(tokens[0, i].join(" "))
    next unless make

    model = make.model(tokens[i..].join(" "))
    return model if model
  end
  nil
end

#makes(kind: nil, region: nil) ⇒ Object

All makes, optionally filtered by kind/region. Unknown region => [] (honest: we don’t ship that pack yet), so callers never get wrong-region data.



59
60
61
62
63
64
65
# File 'lib/vehicles/dataset.rb', line 59

def makes(kind: nil, region: nil)
  return [] if region && !region_match?(region)

  list = @makes
  list = list.select { |m| m.kinds.include?(kind.to_sym) } if kind
  list
end

#region?(region) ⇒ Boolean

Does this snapshot cover the given region? (Today only :eu.)

Returns:

  • (Boolean)


133
134
135
# File 'lib/vehicles/dataset.rb', line 133

def region?(region)
  region_match?(region)
end

#search(query) ⇒ Object

Every model whose name (or full name) matches the query, ranked: exact name, then prefix, then substring, then full-name substring. Shorter names first.



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/vehicles/dataset.rb', line 107

def search(query)
  q = Vehicles.normalize(query)
  return [] if q.empty?

  scored = []
  all_models.each do |m|
    name_n = Vehicles.normalize(m.name)
    score =
      if    name_n == q              then 0
      elsif name_n.start_with?(q)    then 1
      elsif name_n.include?(q)       then 2
      elsif Vehicles.normalize(m.full_name).include?(q) then 3
      else next
      end
    scored << [score, m.name.length, m]
  end
  scored.sort_by { |score, len, _m| [score, len] }.map { |_s, _l, m| m }
end