1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
require "date"
require "icalendar"
require "json"
require "net/http"

# Converts a MealViewer school menu into an iCalendar feed.
class MenuFeed
  API = URI("https://api.mealviewer.com/api/v4/school/")

  # Item types that name the meal, in the order they should appear in a title.
  PRIMARY_TYPES = ["Entree", "Grab & Go", "Breakfast"]
  # Stands in for the entree on days without one, e.g. a soup-and-stew day.
  ENTREE_FALLBACK = "Pre-Pack"
  SKIPPED_TYPES = ["Condiment"]

  Item = Data.define(:type, :name)

  Meal = Data.define(:date, :block_id, :block_name, :items) do
    def self.from_block(block)
      new(
        date: Date.strptime(block.fetch("scheduledDate"), "%Y-%m-%dT%H:%M:%S"),
        block_id: block.fetch("id"),
        block_name: block.fetch("blockName"),
        items: block.fetch("cafeteriaLineList").fetch("data")
          .flat_map { it.fetch("foodItemList").fetch("data") }
          .map { Item.new(type: it.fetch("item_Type"), name: it.fetch("item_Name")) }
          .reject { SKIPPED_TYPES.include?(it.type) },
      )
    end

    def summary
      types = PRIMARY_TYPES
      types = types.map { it == "Entree" ? ENTREE_FALLBACK : it } if items.none? { it.type == "Entree" }
      primary = items
        .select { types.include?(it.type) }
        .sort_by.with_index { |item, i| [types.index(item.type), i] }
      return block_name if primary.empty?

      primary.map(&:name).join(" / ")
    end

    def description = items.map { "#{it.type}: #{it.name}" }.join("\n")
  end

  def self.fetch(school, from, to)
    uri = API + [URI.encode_uri_component(school), from.strftime("%m-%d-%Y"), to.strftime("%m-%d-%Y"), "0"].join("/")
    response = Net::HTTP.get_response(uri)
    raise "GET #{uri} returned #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)

    new(JSON.parse(response.body))
  end

  def initialize(menu)
    location = menu.fetch("physicalLocation")
    @school = location.fetch("name")
    @lookup = location.fetch("physicalLocationLookup")
    @meals = menu.fetch("menuSchedules")
      .flat_map { it.fetch("menuBlocks") }
      .reject { it.fetch("blackedOut") }
      .map { Meal.from_block(it) }
  end

  attr_reader :school, :meals

  def to_ical(block_name)
    calendar = Icalendar::Calendar.new
    calendar.x_wr_calname = "#{school} #{block_name.downcase}"
    meals.select { it.block_name == block_name }.each do |meal|
      calendar.event do |event|
        event.uid = "#{meal.date.iso8601}-#{meal.block_id}@mealviewer.#{@lookup}"
        event.dtstart = Icalendar::Values::Date.new(meal.date)
        event.dtend = Icalendar::Values::Date.new(meal.date + 1)
        event.summary = meal.summary
        event.description = meal.description
        event.transp = "TRANSPARENT"
      end
    end
    calendar.publish
    calendar.to_ical
  end
end