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