Class: Code::Object::Ics

Inherits:
Code::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
MAX_EVENTS =
10_000
MAX_NESTING =
50

Constants inherited from Code::Object

INSTANCE_FUNCTIONS, NUMBER_CLASSES

Constants included from Concerns::Shared

Concerns::Shared::COMPOUND_ASSIGNMENT_OPERATORS, Concerns::Shared::OPERATOR_METHOD_ALIASES, Concerns::Shared::SHARED_OPERATORS

Instance Attribute Summary

Attributes included from Concerns::Shared

#functions, #raw

Class Method Summary collapse

Methods inherited from Code::Object

class_documentation, class_functions, code_new, #code_new, documentation, documentation_for, documented_functions_for, function_documentation_for, function_documentation_registry_for, functions, inherited_function_documentation_for, #initialize, instance_functions, maybe, #name, repeat, sorted_dictionary, |

Methods included from Concerns::Shared

#<=>, #==, #as_json, #blank?, #call, #code_and, #code_as_json, #code_blank?, #code_class_functions, #code_compare, #code_deep_duplicate, #code_different, #code_documentable_functions, #code_documentation, #code_duplicate, #code_dynamic_call, #code_equal, #code_exclamation_mark, #code_exclusive_range, #code_false?, #code_falsy?, code_fetch, #code_fetch, #code_functions, code_get, #code_get, #code_greater, #code_greater_or_equal, #code_has_key?, #code_inclusive_range, #code_inspect, #code_instance_functions, #code_instance_of?, #code_is_a?, #code_itself, #code_less, #code_less_or_equal, #code_name, #code_nothing?, #code_or, #code_presence, #code_presence_in, #code_present?, #code_respond_to?, #code_same_object?, #code_self, #code_send, code_set, #code_set, #code_something?, #code_strict_different, #code_strict_equal, #code_tap, #code_then, #code_to_boolean, #code_to_class, #code_to_date, #code_to_decimal, #code_to_dictionary, #code_to_duration, #code_to_integer, #code_to_json, #code_to_list, #code_to_nothing, #code_to_parameter, #code_to_range, #code_to_string, #code_to_time, #code_true?, #code_truthy?, #eql?, #falsy?, #hash, #inspect, #multi_fetch, #nothing?, #present?, #sig, #something?, #succ, #to_code, #to_i, #to_json, #to_s, #truthy?

Constructor Details

This class inherits a constructor from Code::Object

Class Method Details

.call(**args) ⇒ Object



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

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



64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/code/object/ics.rb', line 64

def self.code_parse(value)
  source = value.to_code.raw
  ::Code.ensure_input_size!(source, label: "ics")
  calendars = ::Icalendar::Calendar.parse(source)
  events = calendars.flat_map(&:events)
  raise Error, "ics has too many events" if events.size > MAX_EVENTS

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

.event_attribute(event, attribute) ⇒ Object



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

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



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

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:



159
160
161
# File 'lib/code/object/ics.rb', line 159

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

.serialize_date_like(value) ⇒ Object



170
171
172
173
174
175
176
177
# File 'lib/code/object/ics.rb', line 170

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

.serialize_event(event) ⇒ Object



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

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, depth: 0) ⇒ Object

Raises:



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/code/object/ics.rb', line 131

def self.serialize_value(value, depth: 0)
  raise Error, "ics is too deeply nested" if depth > MAX_NESTING

  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, depth: depth + 1) }
  when ::Hash
    value.transform_values do |item|
      serialize_value(item, depth: depth + 1)
    end
  else
    serialized_date = serialize_date_like(value)
    return serialized_date unless serialized_date.nil?

    if value.is_a?(::Icalendar::Value)
      serialize_value(value.value, depth: depth + 1)
    else
      normalize_string(value.to_s)
    end
  end
end