Class: Code::Object::Ics

Inherits:
Object show all
Defined in:
lib/code/object/ics.rb

Constant Summary collapse

CLASS_DOCUMENTATION =
{
  name: "Ics",
  description:
    "parses icalendar text and exposes events as dictionaries.",
  examples: [
    "Ics.parse(\"BEGIN:VCALENDAR\\nBEGIN:VEVENT\\nSUMMARY:meet\\nEND:VEVENT\\nEND:VCALENDAR\")",
    "Ics.parse(\"BEGIN:VCALENDAR\\nVERSION:2.0\\nEND:VCALENDAR\")",
    "Ics.parse(\"\")"
  ]
}.freeze
CLASS_FUNCTIONS =
{
  "parse" => {
    name: "parse",
    description: "returns event dictionaries parsed from icalendar text.",
    examples: [
      "Ics.parse(\"BEGIN:VCALENDAR\\nBEGIN:VEVENT\\nUID:1\\nSUMMARY:demo\\nEND:VEVENT\\nEND:VCALENDAR\")",
      "Ics.parse(\"BEGIN:VCALENDAR\\nEND:VCALENDAR\")",
      "Ics.parse(\"not calendar data\")"
    ]
  }
}.freeze
EVENT_ATTRIBUTES =
%i[
  uid
  summary
  description
  location
  url
  status
  organizer
  categories
  attendees
  geo
].freeze
MISSING_ATTRIBUTE =
Object.new.freeze

Class Method Summary collapse

Class Method Details

.call(**args) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/code/object/ics.rb', line 48

def self.call(**args)
  code_operator = args.fetch(:operator, nil).to_code
  code_arguments = args.fetch(:arguments, []).to_code
  code_value = code_arguments.code_first

  case code_operator.to_s
  when "parse"
    sig(args) { String }
    code_parse(code_value)
  else
    super
  end
end

.code_parse(value) ⇒ Object



62
63
64
65
66
67
68
69
70
71
72
# File 'lib/code/object/ics.rb', line 62

def self.code_parse(value)
  source = value.to_code.raw
  calendars = ::Icalendar::Calendar.parse(source)
  events = calendars.flat_map(&:events)

  events.map { |event| serialize_event(event) }.to_code
rescue ::Code::Error
  raise
rescue StandardError
  [].to_code
end

.event_attribute(event, attribute) ⇒ Object



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/code/object/ics.rb', line 100

def self.event_attribute(event, attribute)
  case attribute
  when :uid
    event.uid
  when :summary
    event.summary
  when :description
    event.description
  when :location
    event.location
  when :url
    event.url
  when :status
    event.status
  when :organizer
    event.organizer
  when :categories
    event.categories
  when :attendees
    event.attendees
  when :geo
    event.geo
  end
rescue NoMethodError
  MISSING_ATTRIBUTE
end

.function_documentation(scope) ⇒ Object



28
29
30
31
32
# File 'lib/code/object/ics.rb', line 28

def self.function_documentation(scope)
  return CLASS_FUNCTIONS if scope == :class

  {}
end

.normalize_string(value) ⇒ Object



155
156
157
158
159
160
# File 'lib/code/object/ics.rb', line 155

def self.normalize_string(value)
  value
    .dup
    .force_encoding(::Encoding::UTF_8)
    .encode(::Encoding::UTF_8, invalid: :replace, undef: :replace)
end

.scalar_event_attribute?(attribute) ⇒ Boolean

Returns:



151
152
153
# File 'lib/code/object/ics.rb', line 151

def self.scalar_event_attribute?(attribute)
  !%i[categories attendees geo].include?(attribute)
end

.serialize_date_like(value) ⇒ Object



162
163
164
165
166
167
168
169
170
171
# File 'lib/code/object/ics.rb', line 162

def self.serialize_date_like(value)
  case value
  when ::Time, ::Date, ::ActiveSupport::TimeWithZone
    value
  when ::DateTime
    value.to_time
  when ::Icalendar::Value
    serialize_date_like(value.value)
  end
end

.serialize_event(event) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/code/object/ics.rb', line 74

def self.serialize_event(event)
  EVENT_ATTRIBUTES
    .each_with_object({}) do |attribute, result|
      serialized = serialize_value(event_attribute(event, attribute))
      next if serialized == MISSING_ATTRIBUTE

      serialized =
        if attribute == :categories && serialized.is_a?(::Array)
          serialized.flatten(1)
        elsif scalar_event_attribute?(attribute) &&
              serialized.is_a?(::Array)
          serialized.join(",")
        else
          serialized
        end
      result[attribute] = serialized unless serialized.nil?
    end
    .merge(
      timestamp: serialize_value(event.dtstamp),
      starts_at: serialize_value(event.dtstart),
      ends_at: serialize_value(event.dtend),
      all_day: !!serialize_date_like(event.dtstart).is_a?(::Date)
    )
    .compact
end

.serialize_value(value) ⇒ Object



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/code/object/ics.rb', line 127

def self.serialize_value(value)
  case value
  when nil
    nil
  when ::String
    normalize_string(value)
  when ::Symbol, ::Integer, ::Float, ::BigDecimal, true, false
    value
  when ::Array
    value.map { |item| serialize_value(item) }
  when ::Hash
    value.transform_values { |item| serialize_value(item) }
  else
    serialized_date = serialize_date_like(value)
    return serialized_date unless serialized_date.nil?

    if value.is_a?(::Icalendar::Value)
      serialize_value(value.value)
    else
      normalize_string(value.to_s)
    end
  end
end