icalendar 2.8.0 → 2.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +4 -4
  3. data/History.txt +13 -0
  4. data/icalendar.gemspec +4 -0
  5. data/lib/icalendar/alarm.rb +2 -0
  6. data/lib/icalendar/calendar.rb +38 -0
  7. data/lib/icalendar/component.rb +8 -2
  8. data/lib/icalendar/downcased_hash.rb +2 -0
  9. data/lib/icalendar/event.rb +3 -1
  10. data/lib/icalendar/freebusy.rb +2 -0
  11. data/lib/icalendar/has_components.rb +7 -2
  12. data/lib/icalendar/has_properties.rb +4 -2
  13. data/lib/icalendar/journal.rb +2 -0
  14. data/lib/icalendar/logger.rb +2 -0
  15. data/lib/icalendar/marshable.rb +2 -0
  16. data/lib/icalendar/parser.rb +38 -14
  17. data/lib/icalendar/timezone.rb +2 -0
  18. data/lib/icalendar/timezone_store.rb +2 -0
  19. data/lib/icalendar/todo.rb +2 -0
  20. data/lib/icalendar/tzinfo.rb +2 -0
  21. data/lib/icalendar/value.rb +11 -3
  22. data/lib/icalendar/values/binary.rb +5 -1
  23. data/lib/icalendar/values/boolean.rb +2 -0
  24. data/lib/icalendar/values/cal_address.rb +2 -0
  25. data/lib/icalendar/values/date.rb +2 -0
  26. data/lib/icalendar/values/date_time.rb +4 -2
  27. data/lib/icalendar/values/duration.rb +15 -6
  28. data/lib/icalendar/values/float.rb +2 -0
  29. data/lib/icalendar/values/helpers/active_support_time_with_zone_adapter.rb +16 -0
  30. data/lib/icalendar/values/helpers/array.rb +62 -0
  31. data/lib/icalendar/values/helpers/time_with_zone.rb +61 -0
  32. data/lib/icalendar/values/integer.rb +2 -0
  33. data/lib/icalendar/values/period.rb +5 -1
  34. data/lib/icalendar/values/recur.rb +31 -14
  35. data/lib/icalendar/values/text.rb +5 -1
  36. data/lib/icalendar/values/time.rb +4 -2
  37. data/lib/icalendar/values/uri.rb +2 -0
  38. data/lib/icalendar/values/utc_offset.rb +6 -1
  39. data/lib/icalendar/version.rb +3 -1
  40. data/lib/icalendar.rb +2 -0
  41. data/spec/calendar_spec.rb +49 -0
  42. data/spec/event_spec.rb +4 -2
  43. data/spec/roundtrip_spec.rb +1 -1
  44. data/spec/values/date_time_spec.rb +16 -0
  45. metadata +8 -7
  46. data/lib/icalendar/values/active_support_time_with_zone_adapter.rb +0 -12
  47. data/lib/icalendar/values/array.rb +0 -58
  48. data/lib/icalendar/values/time_with_zone.rb +0 -53
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: becd9ceba3342d0b602f1bb8a33858fb2176ceafd457a0fbca76d24ff7800dc7
4
- data.tar.gz: f11054752ca18d026b1339e936ab30d4f1ff61219ecfd94d3f913b3de6b14106
3
+ metadata.gz: 4a4c654b36ffe5a09f521ad3c012686f12e582ba5d380242c646334f47153360
4
+ data.tar.gz: 184399a11e6e8a39b18ceeef59de8a9b9f7cd581f0c7169cce5c7dca63575e3b
5
5
  SHA512:
6
- metadata.gz: 8262dd28d571778ecc7dd638d6df2a44e4c26bea2221183d82a644c777d100a75027c83ac83fd53c801d43cd75b9521be104111900d6653119a0593a281b8203
7
- data.tar.gz: 1dea941db6e27188b9ab0d769a0df2335fdc2bc492a4ca6339a5e4a90e0d91f9821282aaed13b2cc4f8a4921e24a3d20f14aa34fea97c50c6326d3a1dc29f3cc
6
+ metadata.gz: 61837b5d19193d80d72f37edff89290fc2e5ffa0c6b9ee500036de58b0fbd9aacb5e0e9bc9f1a0a4e1ce7a3ad494ed0147e87bf697b54523d8b799e9eaa275fa
7
+ data.tar.gz: 00d54cc0ab4b7fc6acf616a537b426d110ddbe65dda1d6ab8ec938ba2df64e87def99023916c6193ab25d1885a2a60e8a1706617ed5a6e0adb5cb34671dae1ec
@@ -17,12 +17,12 @@ jobs:
17
17
  strategy:
18
18
  matrix:
19
19
  ruby:
20
- - 2.7.6
21
- - 3.0.4
22
- - 3.1.2
20
+ - 3.0.6
21
+ - 3.1.4
22
+ - 3.2.2
23
23
 
24
24
  steps:
25
- - uses: actions/checkout@v2
25
+ - uses: actions/checkout@v4
26
26
  - name: Set up Ruby
27
27
  uses: ruby/setup-ruby@v1
28
28
  with:
data/History.txt CHANGED
@@ -1,3 +1,16 @@
1
+ === Unreleased
2
+
3
+ === 2.10.0 2023-11-01
4
+ * Add changelog metadata to gemspec - Juri Hahn
5
+ * Attempt to rescue timezone info when given a nonstandard tzid with no VTIMEZONE
6
+ * Move Values classes that shouldn't be directly used into Helpers module
7
+
8
+ === 2.9.0 2023-08-11
9
+ * Always include the VALUE of Event URLs for improved compatibility - Sean Kelley
10
+ * Improved parse performance - Thomas Cannon
11
+ * Add helper methods for other Calendar method verbs
12
+ * bugfix: Require stringio before use in Parser - vwyu
13
+
1
14
  === 2.8.0 2022-07-10
2
15
  * Fix compatibility with ActiveSupport 7 - Pat Allan
3
16
  * Set default action of "DISPLAY" on alarms - Rikson
data/icalendar.gemspec CHANGED
@@ -20,6 +20,10 @@ variety of calendaring applications.
20
20
  ActiveSupport is required for TimeWithZone support, but not required for general use.
21
21
  EOM
22
22
 
23
+ s.metadata = {
24
+ 'changelog_uri' => 'https://github.com/icalendar/icalendar/blob/main/History.txt'
25
+ }
26
+
23
27
  s.files = `git ls-files`.split "\n"
24
28
  s.test_files = `git ls-files -- {test,spec,features}/*`.split "\n"
25
29
  s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename f }
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Alarm < Component
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Calendar < Component
@@ -31,6 +33,42 @@ module Icalendar
31
33
 
32
34
  def publish
33
35
  self.ip_method = 'PUBLISH'
36
+ self
37
+ end
38
+
39
+ def request
40
+ self.ip_method = 'REQUEST'
41
+ self
42
+ end
43
+
44
+ def reply
45
+ self.ip_method = 'REPLY'
46
+ self
47
+ end
48
+
49
+ def add
50
+ self.ip_method = 'ADD'
51
+ self
52
+ end
53
+
54
+ def cancel
55
+ self.ip_method = 'CANCEL'
56
+ self
57
+ end
58
+
59
+ def refresh
60
+ self.ip_method = 'REFRESH'
61
+ self
62
+ end
63
+
64
+ def counter
65
+ self.ip_method = 'COUNTER'
66
+ self
67
+ end
68
+
69
+ def decline_counter
70
+ self.ip_method = 'DECLINECOUNTER'
71
+ self
34
72
  end
35
73
 
36
74
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'securerandom'
2
4
 
3
5
  module Icalendar
@@ -53,10 +55,14 @@ module Icalendar
53
55
  end.compact.join "\r\n"
54
56
  end
55
57
 
58
+ ICAL_PROP_NAME_GSUB_REGEX = /\Aip_/.freeze
59
+
56
60
  def ical_prop_name(prop_name)
57
- prop_name.gsub(/\Aip_/, '').gsub('_', '-').upcase
61
+ prop_name.gsub(ICAL_PROP_NAME_GSUB_REGEX, '').gsub('_', '-').upcase
58
62
  end
59
63
 
64
+ ICAL_FOLD_LONG_LINE_SCAN_REGEX = /\P{M}\p{M}*/u.freeze
65
+
60
66
  def ical_fold(long_line, indent = "\x20")
61
67
  # rfc2445 says:
62
68
  # Lines of text SHOULD NOT be longer than 75 octets, excluding the line
@@ -74,7 +80,7 @@ module Icalendar
74
80
 
75
81
  return long_line if long_line.bytesize <= Icalendar::MAX_LINE_LENGTH
76
82
 
77
- chars = long_line.scan(/\P{M}\p{M}*/u) # split in graphenes
83
+ chars = long_line.scan(ICAL_FOLD_LONG_LINE_SCAN_REGEX) # split in graphenes
78
84
  folded = ['']
79
85
  bytes = 0
80
86
  while chars.count > 0
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'delegate'
2
4
 
3
5
  module Icalendar
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Event < Component
@@ -24,7 +26,7 @@ module Icalendar
24
26
  optional_single_property :status
25
27
  optional_single_property :summary
26
28
  optional_single_property :transp
27
- optional_single_property :url, Icalendar::Values::Uri
29
+ optional_single_property :url, Icalendar::Values::Uri, true
28
30
  optional_single_property :recurrence_id, Icalendar::Values::DateTime
29
31
 
30
32
  optional_property :rrule, Icalendar::Values::Recur, true
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Freebusy < Component
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  module HasComponents
@@ -32,13 +34,16 @@ module Icalendar
32
34
  custom_components[component_name.downcase.gsub("-", "_")] || []
33
35
  end
34
36
 
37
+ METHOD_MISSING_ADD_REGEX = /^add_(x_\w+)$/.freeze
38
+ METHOD_MISSING_X_FLAG_REGEX = /^x_/.freeze
39
+
35
40
  def method_missing(method, *args, &block)
36
41
  method_name = method.to_s
37
- if method_name =~ /^add_(x_\w+)$/
42
+ if method_name =~ METHOD_MISSING_ADD_REGEX
38
43
  component_name = $1
39
44
  custom = args.first || Component.new(component_name, component_name.upcase)
40
45
  add_custom_component(component_name, custom, &block)
41
- elsif method_name =~ /^x_/ && custom_component(method_name).size > 0
46
+ elsif method_name =~ METHOD_MISSING_X_FLAG_REGEX && custom_component(method_name).size > 0
42
47
  custom_component method_name
43
48
  else
44
49
  super
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  module HasProperties
@@ -146,7 +148,7 @@ module Icalendar
146
148
 
147
149
  define_method "#{prop}=" do |value|
148
150
  mapped = map_property_value value, klass, true, new_property
149
- if mapped.is_a? Icalendar::Values::Array
151
+ if mapped.is_a? Icalendar::Values::Helpers::Array
150
152
  instance_variable_set property_var, mapped.to_a.compact
151
153
  else
152
154
  instance_variable_set property_var, [mapped].compact
@@ -177,7 +179,7 @@ module Icalendar
177
179
  if value.nil? || value.is_a?(Icalendar::Value)
178
180
  value
179
181
  elsif value.is_a? ::Array
180
- Icalendar::Values::Array.new value, klass, params, {delimiter: (multi_valued ? ',' : ';')}
182
+ Icalendar::Values::Helpers::Array.new value, klass, params, {delimiter: (multi_valued ? ',' : ';')}
181
183
  else
182
184
  klass.new value, params
183
185
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Journal < Component
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'delegate'
2
4
  require 'logger'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Marshable
3
5
  def self.included(base)
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'icalendar/timezone_store'
4
+ require 'stringio'
2
5
 
3
6
  module Icalendar
4
7
 
@@ -6,6 +9,8 @@ module Icalendar
6
9
  attr_writer :component_class
7
10
  attr_reader :source, :strict, :timezone_store, :verbose
8
11
 
12
+ CLEAN_BAD_WRAPPING_GSUB_REGEX = /\r?\n[ \t]/.freeze
13
+
9
14
  def self.clean_bad_wrapping(source)
10
15
  content = if source.respond_to? :read
11
16
  source.read
@@ -18,7 +23,7 @@ module Icalendar
18
23
  end
19
24
  encoding = content.encoding
20
25
  content.force_encoding(Encoding::ASCII_8BIT)
21
- content.gsub(/\r?\n[ \t]/, "").force_encoding(encoding)
26
+ content.gsub(CLEAN_BAD_WRAPPING_GSUB_REGEX, "").force_encoding(encoding)
22
27
  end
23
28
 
24
29
  def initialize(source, strict = false, verbose = false)
@@ -71,11 +76,15 @@ module Icalendar
71
76
  end
72
77
  end
73
78
 
79
+ WRAP_PROPERTY_VALUE_DELIMETER_REGEX = /(?<!\\)([,;])/.freeze
80
+ WRAP_PROPERTY_VALUE_SPLIT_REGEX = /(?<!\\)[;,]/.freeze
81
+
82
+
74
83
  def wrap_property_value(component, fields, multi_property)
75
84
  klass = get_wrapper_class component, fields
76
85
  if wrap_in_array? klass, fields[:value], multi_property
77
- delimiter = fields[:value].match(/(?<!\\)([,;])/)[1]
78
- Icalendar::Values::Array.new fields[:value].split(/(?<!\\)[;,]/),
86
+ delimiter = fields[:value].match(WRAP_PROPERTY_VALUE_DELIMETER_REGEX)[1]
87
+ Icalendar::Values::Helpers::Array.new fields[:value].split(WRAP_PROPERTY_VALUE_SPLIT_REGEX),
79
88
  klass,
80
89
  fields[:params],
81
90
  delimiter: delimiter
@@ -88,17 +97,22 @@ module Icalendar
88
97
  retry
89
98
  end
90
99
 
100
+ WRAP_IN_ARRAY_REGEX_1 = /(?<!\\)[,;]/.freeze
101
+ WRAP_IN_ARRAY_REGEX_2 = /(?<!\\);/.freeze
102
+
91
103
  def wrap_in_array?(klass, value, multi_property)
92
104
  klass.value_type != 'RECUR' &&
93
- ((multi_property && value =~ /(?<!\\)[,;]/) || value =~ /(?<!\\);/)
105
+ ((multi_property && value =~ WRAP_IN_ARRAY_REGEX_1) || value =~ WRAP_IN_ARRAY_REGEX_2)
94
106
  end
95
107
 
108
+ GET_WRAPPER_CLASS_GSUB_REGEX = /(?:\A|-)(.)/.freeze
109
+
96
110
  def get_wrapper_class(component, fields)
97
111
  klass = component.class.default_property_types[fields[:name]]
98
112
  if !fields[:params]['value'].nil?
99
113
  klass_name = fields[:params].delete('value').first
100
114
  unless klass_name.upcase == klass.value_type
101
- klass_name = "Icalendar::Values::#{klass_name.downcase.gsub(/(?:\A|-)(.)/) { |m| m[-1].upcase }}"
115
+ klass_name = "Icalendar::Values::#{klass_name.downcase.gsub(GET_WRAPPER_CLASS_GSUB_REGEX) { |m| m[-1].upcase }}"
102
116
  klass = Object.const_get klass_name if Object.const_defined?(klass_name)
103
117
  end
104
118
  end
@@ -119,14 +133,16 @@ module Icalendar
119
133
  @component_class ||= Icalendar::Calendar
120
134
  end
121
135
 
136
+ PARSE_COMPONENT_KLASS_NAME_GSUB_REGEX = /\AV/.freeze
137
+
122
138
  def parse_component(component)
123
139
  while (fields = next_fields)
124
140
  if fields[:name] == 'end'
125
- klass_name = fields[:value].gsub(/\AV/, '').downcase.capitalize
141
+ klass_name = fields[:value].gsub(PARSE_COMPONENT_KLASS_NAME_GSUB_REGEX, '').downcase.capitalize
126
142
  timezone_store.store(component) if klass_name == 'Timezone'
127
143
  break
128
144
  elsif fields[:name] == 'begin'
129
- klass_name = fields[:value].gsub(/\AV/, '').gsub("-", "_").downcase.capitalize
145
+ klass_name = fields[:value].gsub(PARSE_COMPONENT_KLASS_NAME_GSUB_REGEX, '').gsub("-", "_").downcase.capitalize
130
146
  Icalendar.logger.debug "Adding component #{klass_name}"
131
147
  if Object.const_defined? "Icalendar::#{klass_name}"
132
148
  component.add_component parse_component(Object.const_get("Icalendar::#{klass_name}").new)
@@ -146,13 +162,16 @@ module Icalendar
146
162
  @data = source.gets and @data.chomp!
147
163
  end
148
164
 
165
+ NEXT_FIELDS_TAB_REGEX = /\A[ \t].+\z/.freeze
166
+ NEXT_FIELDS_WHITESPACE_REGEX = /\A\s*\z/.freeze
167
+
149
168
  def next_fields
150
169
  line = @data or return nil
151
170
  loop do
152
171
  read_in_data
153
- if @data =~ /\A[ \t].+\z/
172
+ if @data =~ NEXT_FIELDS_TAB_REGEX
154
173
  line << @data[1, @data.size]
155
- elsif @data !~ /\A\s*\z/
174
+ elsif @data !~ NEXT_FIELDS_WHITESPACE_REGEX
156
175
  break
157
176
  end
158
177
  end
@@ -167,24 +186,29 @@ module Icalendar
167
186
  VALUE = '.*'
168
187
  LINE = "(?<name>#{NAME})(?<params>(?:;#{PARAM})*):(?<value>#{VALUE})"
169
188
  BAD_LINE = "(?<name>#{NAME})(?<params>(?:;#{PARAM})*)"
189
+ LINE_REGEX = %r{#{LINE}}.freeze
190
+ BAD_LINE_REGEX = %r{#{BAD_LINE}}.freeze
191
+ PARAM_REGEX = %r{#{PARAM}}.freeze
192
+ PVALUE_REGEX = %r{#{PVALUE}}.freeze
193
+ PVALUE_GSUB_REGEX = /\A"|"\z/.freeze
170
194
 
171
195
  def parse_fields(input)
172
- if parts = %r{#{LINE}}.match(input)
196
+ if parts = LINE_REGEX.match(input)
173
197
  value = parts[:value]
174
198
  else
175
- parts = %r{#{BAD_LINE}}.match(input) unless strict?
199
+ parts = BAD_LINE_REGEX.match(input) unless strict?
176
200
  parts or fail "Invalid iCalendar input line: #{input}"
177
201
  # Non-strict and bad line so use a value of empty string
178
202
  value = ''
179
203
  end
180
204
 
181
205
  params = {}
182
- parts[:params].scan %r{#{PARAM}} do |match|
206
+ parts[:params].scan PARAM_REGEX do |match|
183
207
  param_name = match[0].downcase
184
208
  params[param_name] ||= []
185
- match[1].scan %r{#{PVALUE}} do |param_value|
209
+ match[1].scan PVALUE_REGEX do |param_value|
186
210
  if param_value.size > 0
187
- param_value = param_value.gsub(/\A"|"\z/, '')
211
+ param_value = param_value.gsub(PVALUE_GSUB_REGEX, '')
188
212
  params[param_name] << param_value
189
213
  if param_name == 'tzid'
190
214
  params['x-tz-info'] = timezone_store.retrieve param_value
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ice_cube'
2
4
 
3
5
  module Icalendar
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'delegate'
2
4
  require 'icalendar/downcased_hash'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Todo < Component
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  =begin
2
4
  Copyright (C) 2008 Sean Dague
3
5
 
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+
1
4
  require 'delegate'
2
5
  require 'icalendar/downcased_hash'
3
6
 
@@ -31,8 +34,11 @@ module Icalendar
31
34
  end
32
35
  end
33
36
 
37
+ VALUE_TYPE_GSUB_REGEX_1 = /\A.*::/.freeze
38
+ VALUE_TYPE_GSUB_REGEX_2 = /(?<!\A)[A-Z]/.freeze
39
+
34
40
  def self.value_type
35
- name.gsub(/\A.*::/, '').gsub(/(?<!\A)[A-Z]/, '-\0').upcase
41
+ name.gsub(VALUE_TYPE_GSUB_REGEX_1, '').gsub(VALUE_TYPE_GSUB_REGEX_2, '-\0').upcase
36
42
  end
37
43
 
38
44
  def value_type
@@ -54,9 +60,11 @@ module Icalendar
54
60
  "#{name.to_s.gsub('_', '-').upcase}=#{param_value}"
55
61
  end
56
62
 
63
+ ESCAPE_PARAM_VALUE_REGEX = /[;:,]/.freeze
64
+
57
65
  def escape_param_value(value)
58
66
  v = value.to_s.gsub('"', "'")
59
- v =~ /[;:,]/ ? %("#{v}") : v
67
+ v =~ ESCAPE_PARAM_VALUE_REGEX ? %("#{v}") : v
60
68
  end
61
69
 
62
70
  end
@@ -64,7 +72,7 @@ module Icalendar
64
72
  end
65
73
 
66
74
  # helpers; not actual iCalendar value type
67
- require_relative 'values/array'
75
+ require_relative 'values/helpers/array'
68
76
  require_relative 'values/date_or_date_time'
69
77
 
70
78
  # iCalendar value types
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'base64'
2
4
 
3
5
  module Icalendar
@@ -21,9 +23,11 @@ module Icalendar
21
23
 
22
24
  private
23
25
 
26
+ BASE_64_REGEX = /\A(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{4}|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{2}==)\z/.freeze
27
+
24
28
  def base64?
25
29
  value.is_a?(String) &&
26
- value =~ /\A(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{4}|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{2}==)\z/
30
+ value =~ BASE_64_REGEX
27
31
  end
28
32
  end
29
33
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
4
 
3
5
  module Icalendar
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
- require_relative 'time_with_zone'
4
+ require_relative 'helpers/time_with_zone'
3
5
 
4
6
  module Icalendar
5
7
  module Values
6
8
 
7
9
  class DateTime < Value
8
- include TimeWithZone
10
+ include Helpers::TimeWithZone
9
11
 
10
12
  FORMAT = '%Y%m%dT%H%M%S'
11
13
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ostruct'
2
4
 
3
5
  module Icalendar
@@ -36,14 +38,21 @@ module Icalendar
36
38
  hours > 0 || minutes > 0 || seconds > 0
37
39
  end
38
40
 
41
+ DURATION_PAST_REGEX = /\A([+-])P/.freeze
42
+ DURATION_WEEKS_REGEX = /(\d+)W/.freeze
43
+ DURATION_DAYS_REGEX = /(\d+)D/.freeze
44
+ DURATION_HOURS_REGEX = /(\d+)H/.freeze
45
+ DURATION_MINUTES_REGEX = /(\d+)M/.freeze
46
+ DURATION_SECONDS_REGEX = /(\d+)S/.freeze
47
+
39
48
  def parse_fields(value)
40
49
  {
41
- past: (value =~ /\A([+-])P/ ? $1 == '-' : false),
42
- weeks: (value =~ /(\d+)W/ ? $1.to_i : 0),
43
- days: (value =~ /(\d+)D/ ? $1.to_i : 0),
44
- hours: (value =~ /(\d+)H/ ? $1.to_i : 0),
45
- minutes: (value =~ /(\d+)M/ ? $1.to_i : 0),
46
- seconds: (value =~ /(\d+)S/ ? $1.to_i : 0)
50
+ past: (value =~ DURATION_PAST_REGEX ? $1 == '-' : false),
51
+ weeks: (value =~ DURATION_WEEKS_REGEX ? $1.to_i : 0),
52
+ days: (value =~ DURATION_DAYS_REGEX ? $1.to_i : 0),
53
+ hours: (value =~ DURATION_HOURS_REGEX ? $1.to_i : 0),
54
+ minutes: (value =~ DURATION_MINUTES_REGEX ? $1.to_i : 0),
55
+ seconds: (value =~ DURATION_SECONDS_REGEX ? $1.to_i : 0)
47
56
  }
48
57
  end
49
58
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
 
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Icalendar
4
+ module Values
5
+ module Helpers
6
+ class ActiveSupportTimeWithZoneAdapter < ActiveSupport::TimeWithZone
7
+ # ActiveSupport::TimeWithZone implements a #to_a method that will cause
8
+ # unexpected behavior in components with multi_property DateTime
9
+ # properties when the setters for those properties are invoked with an
10
+ # Icalendar::Values::DateTime that is delegating for an
11
+ # ActiveSupport::TimeWithZone. To avoid this behavior, undefine #to_a.
12
+ undef_method :to_a
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Icalendar
4
+ module Values
5
+ module Helpers
6
+
7
+ class Array < Value
8
+
9
+ attr_reader :value_delimiter
10
+
11
+ def initialize(value, klass, params = {}, options = {})
12
+ @value_delimiter = options[:delimiter] || ','
13
+ mapped = if value.is_a? ::Array
14
+ value.map do |v|
15
+ if v.is_a? Icalendar::Values::Helpers::Array
16
+ Icalendar::Values::Helpers::Array.new v.value, klass, v.ical_params, delimiter: v.value_delimiter
17
+ elsif v.is_a? ::Array
18
+ Icalendar::Values::Helpers::Array.new v, klass, params, delimiter: value_delimiter
19
+ elsif v.is_a? Icalendar::Value
20
+ v
21
+ else
22
+ klass.new v, params
23
+ end
24
+ end
25
+ else
26
+ [klass.new(value, params)]
27
+ end
28
+ super mapped
29
+ end
30
+
31
+ def params_ical
32
+ value.each do |v|
33
+ ical_params.merge! v.ical_params
34
+ end
35
+ super
36
+ end
37
+
38
+ def value_ical
39
+ value.map do |v|
40
+ v.value_ical
41
+ end.join value_delimiter
42
+ end
43
+
44
+ def valid?
45
+ klass = value.first.class
46
+ !value.all? { |v| v.class == klass }
47
+ end
48
+
49
+ def value_type
50
+ value.first.value_type
51
+ end
52
+
53
+ private
54
+
55
+ def needs_value_type?(default_type)
56
+ value.first.class != default_type
57
+ end
58
+ end
59
+
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'active_support/time'
5
+
6
+ if defined?(ActiveSupport::TimeWithZone)
7
+ require_relative 'active_support_time_with_zone_adapter'
8
+ end
9
+ rescue NameError
10
+ # ActiveSupport v7+ needs the base require to be run first before loading
11
+ # specific parts of it.
12
+ # https://guides.rubyonrails.org/active_support_core_extensions.html#stand-alone-active-support
13
+ require 'active_support'
14
+ retry
15
+ rescue LoadError
16
+ # tis ok, just a bit less fancy
17
+ end
18
+
19
+ module Icalendar
20
+ module Values
21
+ module Helpers
22
+ module TimeWithZone
23
+ attr_reader :tz_utc
24
+
25
+ def initialize(value, params = {})
26
+ params = Icalendar::DowncasedHash(params)
27
+ @tz_utc = params['tzid'] == 'UTC'
28
+ x_tz_info = params.delete 'x-tz-info'
29
+
30
+ offset_value = unless params['tzid'].nil?
31
+ tzid = params['tzid'].is_a?(::Array) ? params['tzid'].first : params['tzid']
32
+ support_classes_defined = defined?(ActiveSupport::TimeZone) && defined?(ActiveSupportTimeWithZoneAdapter)
33
+ if support_classes_defined && (tz = ActiveSupport::TimeZone[tzid])
34
+ # plan a - use ActiveSupport::TimeWithZone
35
+ ActiveSupportTimeWithZoneAdapter.new(nil, tz, value)
36
+ elsif !x_tz_info.nil?
37
+ # plan b - use definition from provided `VTIMEZONE`
38
+ offset = x_tz_info.offset_for_local(value).to_s
39
+ if value.respond_to?(:change)
40
+ value.change offset: offset
41
+ else
42
+ ::Time.new value.year, value.month, value.day, value.hour, value.min, value.sec, offset
43
+ end
44
+ elsif support_classes_defined && (tz = ActiveSupport::TimeZone[tzid.split.first])
45
+ # plan c - try to find an ActiveSupport::TimeWithZone based on the first word of the tzid
46
+ params['tzid'] = [tz.tzinfo.name]
47
+ ActiveSupportTimeWithZoneAdapter.new(nil, tz, value)
48
+ end
49
+ # plan d - just ignore the tzid
50
+ end
51
+ super((offset_value || value), params)
52
+ end
53
+
54
+ def params_ical
55
+ ical_params.delete 'tzid' if tz_utc
56
+ super
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
 
@@ -1,12 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
 
4
6
  class Period < Value
5
7
 
8
+ PERIOD_LAST_PART_REGEX = /\A[+-]?P.+\z/.freeze
9
+
6
10
  def initialize(value, params = {})
7
11
  parts = value.split '/'
8
12
  period_start = Icalendar::Values::DateTime.new parts.first
9
- if parts.last =~ /\A[+-]?P.+\z/
13
+ if parts.last =~ PERIOD_LAST_PART_REGEX
10
14
  period_end = Icalendar::Values::Duration.new parts.last
11
15
  else
12
16
  period_end = Icalendar::Values::DateTime.new parts.last
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ostruct'
2
4
 
3
5
  module Icalendar
@@ -44,22 +46,37 @@ module Icalendar
44
46
 
45
47
  private
46
48
 
49
+ PARSE_FIELDS_FREQUENCY_REGEX = /FREQ=(SECONDLY|MINUTELY|HOURLY|DAILY|WEEKLY|MONTHLY|YEARLY)/i.freeze
50
+ PARSE_FIELDS_UNTIL_REGEX = /UNTIL=([^;]*)/i.freeze
51
+ PARSE_FIELDS_COUNT_REGEX = /COUNT=(\d+)/i.freeze
52
+ PARSE_FIELDS_INTERVAL_REGEX = /INTERVAL=(\d+)/i.freeze
53
+ PARSE_FIELDS_BY_SECOND_REGEX = /BYSECOND=(#{NUM_LIST})(?:;|\z)/i.freeze
54
+ PARSE_FIELDS_BY_MINUTE_REGEX = /BYMINUTE=(#{NUM_LIST})(?:;|\z)/i.freeze
55
+ PARSE_FIELDS_BY_HOUR_REGEX = /BYHOUR=(#{NUM_LIST})(?:;|\z)/i.freeze
56
+ PARSE_FIELDS_BY_DAY_REGEX = /BYDAY=(#{WEEKDAY}(?:,#{WEEKDAY})*)(?:;|\z)/i.freeze
57
+ PARSE_FIELDS_BY_MONTH_DAY_REGEX = /BYMONTHDAY=(#{MONTHDAY}(?:,#{MONTHDAY})*)(?:;|\z)/i.freeze
58
+ PARSE_FIELDS_BY_YEAR_DAY_REGEX = /BYYEARDAY=(#{YEARDAY}(?:,#{YEARDAY})*)(?:;|\z)/i.freeze
59
+ PARSE_FIELDS_BY_WEEK_NUMBER_REGEX = /BYWEEKNO=(#{MONTHDAY}(?:,#{MONTHDAY})*)(?:;|\z)/i.freeze
60
+ PARSE_FIELDS_BY_MONTH_REGEX = /BYMONTH=(#{NUM_LIST})(?:;|\z)/i.freeze
61
+ PARSE_FIELDS_BY_SET_POSITON_REGEX = /BYSETPOS=(#{YEARDAY}(?:,#{YEARDAY})*)(?:;|\z)/i.freeze
62
+ PARSE_FIELDS_BY_WEEK_START_REGEX = /WKST=(#{DAYNAME})/i.freeze
63
+
47
64
  def parse_fields(value)
48
65
  {
49
- frequency: (value =~ /FREQ=(SECONDLY|MINUTELY|HOURLY|DAILY|WEEKLY|MONTHLY|YEARLY)/i ? $1.upcase : nil),
50
- until: (value =~ /UNTIL=([^;]*)/i ? $1 : nil),
51
- count: (value =~ /COUNT=(\d+)/i ? $1.to_i : nil),
52
- interval: (value =~ /INTERVAL=(\d+)/i ? $1.to_i : nil),
53
- by_second: (value =~ /BYSECOND=(#{NUM_LIST})(?:;|\z)/i ? $1.split(',').map { |i| i.to_i } : nil),
54
- by_minute: (value =~ /BYMINUTE=(#{NUM_LIST})(?:;|\z)/i ? $1.split(',').map { |i| i.to_i } : nil),
55
- by_hour: (value =~ /BYHOUR=(#{NUM_LIST})(?:;|\z)/i ? $1.split(',').map { |i| i.to_i } : nil),
56
- by_day: (value =~ /BYDAY=(#{WEEKDAY}(?:,#{WEEKDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
57
- by_month_day: (value =~ /BYMONTHDAY=(#{MONTHDAY}(?:,#{MONTHDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
58
- by_year_day: (value =~ /BYYEARDAY=(#{YEARDAY}(?:,#{YEARDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
59
- by_week_number: (value =~ /BYWEEKNO=(#{MONTHDAY}(?:,#{MONTHDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
60
- by_month: (value =~ /BYMONTH=(#{NUM_LIST})(?:;|\z)/i ? $1.split(',').map { |i| i.to_i } : nil),
61
- by_set_position: (value =~ /BYSETPOS=(#{YEARDAY}(?:,#{YEARDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
62
- week_start: (value =~ /WKST=(#{DAYNAME})/i ? $1.upcase : nil)
66
+ frequency: (value =~ PARSE_FIELDS_FREQUENCY_REGEX ? $1.upcase : nil),
67
+ until: (value =~ PARSE_FIELDS_UNTIL_REGEX ? $1 : nil),
68
+ count: (value =~ PARSE_FIELDS_COUNT_REGEX ? $1.to_i : nil),
69
+ interval: (value =~ PARSE_FIELDS_INTERVAL_REGEX ? $1.to_i : nil),
70
+ by_second: (value =~ PARSE_FIELDS_BY_SECOND_REGEX ? $1.split(',').map { |i| i.to_i } : nil),
71
+ by_minute: (value =~ PARSE_FIELDS_BY_MINUTE_REGEX ? $1.split(',').map { |i| i.to_i } : nil),
72
+ by_hour: (value =~ PARSE_FIELDS_BY_HOUR_REGEX ? $1.split(',').map { |i| i.to_i } : nil),
73
+ by_day: (value =~ PARSE_FIELDS_BY_DAY_REGEX ? $1.split(',') : nil),
74
+ by_month_day: (value =~ PARSE_FIELDS_BY_MONTH_DAY_REGEX ? $1.split(',') : nil),
75
+ by_year_day: (value =~ PARSE_FIELDS_BY_YEAR_DAY_REGEX ? $1.split(',') : nil),
76
+ by_week_number: (value =~ PARSE_FIELDS_BY_WEEK_NUMBER_REGEX ? $1.split(',') : nil),
77
+ by_month: (value =~ PARSE_FIELDS_BY_MONTH_REGEX ? $1.split(',').map { |i| i.to_i } : nil),
78
+ by_set_position: (value =~ PARSE_FIELDS_BY_SET_POSITON_REGEX ? $1.split(',') : nil),
79
+ week_start: (value =~ PARSE_FIELDS_BY_WEEK_START_REGEX ? $1.upcase : nil)
63
80
  }
64
81
  end
65
82
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
  class Text < Value
@@ -9,12 +11,14 @@ module Icalendar
9
11
  super value, params
10
12
  end
11
13
 
14
+ VALUE_ICAL_CARRIAGE_RETURN_GSUB_REGEX = /\r?\n/.freeze
15
+
12
16
  def value_ical
13
17
  value.dup.tap do |v|
14
18
  v.gsub!('\\') { '\\\\' }
15
19
  v.gsub!(';', '\;')
16
20
  v.gsub!(',', '\,')
17
- v.gsub!(/\r?\n/, '\n')
21
+ v.gsub!(VALUE_ICAL_CARRIAGE_RETURN_GSUB_REGEX, '\n')
18
22
  end
19
23
  end
20
24
  end
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
- require_relative 'time_with_zone'
4
+ require_relative 'helpers/time_with_zone'
3
5
 
4
6
  module Icalendar
5
7
  module Values
6
8
 
7
9
  class Time < Value
8
- include TimeWithZone
10
+ include Helpers::TimeWithZone
9
11
 
10
12
  FORMAT = '%H%M%S'
11
13
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
4
 
3
5
  module Icalendar
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ostruct'
2
4
 
3
5
  module Icalendar
@@ -36,8 +38,11 @@ module Icalendar
36
38
  hours == 0 && minutes == 0 && seconds == 0
37
39
  end
38
40
 
41
+ PARSE_FIELDS_MD_REGEX = /\A(?<behind>[+-])(?<hours>\d{2})(?<minutes>\d{2})(?<seconds>\d{2})?\z/.freeze
42
+ PARSE_FIELDS_WHITESPACE_GSUB_REGEX = /\s+/.freeze
43
+
39
44
  def parse_fields(value)
40
- md = /\A(?<behind>[+-])(?<hours>\d{2})(?<minutes>\d{2})(?<seconds>\d{2})?\z/.match value.gsub(/\s+/, '')
45
+ md = PARSE_FIELDS_MD_REGEX.match value.gsub(PARSE_FIELDS_WHITESPACE_GSUB_REGEX, '')
41
46
  {
42
47
  behind: (md[:behind] == '-'),
43
48
  hours: md[:hours].to_i,
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
- VERSION = '2.8.0'
5
+ VERSION = '2.10.0'
4
6
 
5
7
  end
data/lib/icalendar.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'icalendar/logger'
2
4
 
3
5
  module Icalendar
@@ -190,6 +190,55 @@ END:VCALENDAR
190
190
  end
191
191
  end
192
192
 
193
+ describe '#request' do
194
+ it 'sets ip_method to "REQUEST"' do
195
+ subject.request
196
+ expect(subject.ip_method).to eq 'REQUEST'
197
+ end
198
+ end
199
+
200
+ describe '#reply' do
201
+ it 'sets ip_method to "REPLY"' do
202
+ subject.reply
203
+ expect(subject.ip_method).to eq 'REPLY'
204
+ end
205
+ end
206
+
207
+ describe '#add' do
208
+ it 'sets ip_method to "ADD"' do
209
+ subject.add
210
+ expect(subject.ip_method).to eq 'ADD'
211
+ end
212
+ end
213
+
214
+ describe '#cancel' do
215
+ it 'sets ip_method to "CANCEL"' do
216
+ subject.cancel
217
+ expect(subject.ip_method).to eq 'CANCEL'
218
+ end
219
+ end
220
+
221
+ describe '#refresh' do
222
+ it 'sets ip_method to "REFRESH"' do
223
+ subject.refresh
224
+ expect(subject.ip_method).to eq 'REFRESH'
225
+ end
226
+ end
227
+
228
+ describe '#counter' do
229
+ it 'sets ip_method to "COUNTER"' do
230
+ subject.counter
231
+ expect(subject.ip_method).to eq 'COUNTER'
232
+ end
233
+ end
234
+
235
+ describe '#decline_counter' do
236
+ it 'sets ip_method to "DECLINECOUNTER"' do
237
+ subject.decline_counter
238
+ expect(subject.ip_method).to eq 'DECLINECOUNTER'
239
+ end
240
+ end
241
+
193
242
  describe '.parse' do
194
243
  let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'bad_wrapping.ics') }
195
244
 
data/spec/event_spec.rb CHANGED
@@ -51,8 +51,8 @@ describe Icalendar::Event do
51
51
  context 'suggested single values' do
52
52
  before(:each) do
53
53
  subject.dtstart = DateTime.now
54
- subject.append_rrule double('RRule').as_null_object
55
- subject.append_rrule double('RRule').as_null_object
54
+ subject.append_rrule 'RRule'
55
+ subject.append_rrule 'RRule'
56
56
  end
57
57
 
58
58
  it 'is valid by default' do
@@ -156,6 +156,7 @@ describe Icalendar::Event do
156
156
  subject.dtend = "20131227T033000Z"
157
157
  subject.summary = 'My event, my ical, my test'
158
158
  subject.geo = [41.230896,-74.411774]
159
+ subject.url = 'https://example.com'
159
160
  subject.x_custom_property = 'customize'
160
161
  end
161
162
 
@@ -163,6 +164,7 @@ describe Icalendar::Event do
163
164
  it { expect(subject.to_ical).to include 'DTEND:20131227T033000Z' }
164
165
  it { expect(subject.to_ical).to include 'SUMMARY:My event\, my ical\, my test' }
165
166
  it { expect(subject.to_ical).to include 'X-CUSTOM-PROPERTY:customize' }
167
+ it { expect(subject.to_ical).to include 'URL;VALUE=URI:https://example.com' }
166
168
  it { expect(subject.to_ical).to include 'GEO:41.230896;-74.411774' }
167
169
 
168
170
  context 'simple organizer' do
@@ -14,7 +14,7 @@ describe Icalendar do
14
14
  event = Icalendar::Event.new
15
15
  parsed = Icalendar::Calendar.parse(source).first
16
16
  event.rdate = parsed.events.first.rdate
17
- expect(event.rdate.first).to be_kind_of Icalendar::Values::Array
17
+ expect(event.rdate.first).to be_kind_of Icalendar::Values::Helpers::Array
18
18
  expect(event.rdate.first.params_ical).to eq ";TZID=US-Mountain"
19
19
  end
20
20
  end
@@ -27,6 +27,22 @@ describe Icalendar::Values::DateTime do
27
27
  expect(subject.value.utc.hour).to eq 23
28
28
  end
29
29
  end
30
+
31
+ context 'nonstandard format tzid local time' do
32
+ let(:value) { '20230901T230404' }
33
+ let(:params) { {'tzid' => 'Singapore Standard Time'} }
34
+
35
+ it 'parses the value as local time' do
36
+ expect(subject.value.hour).to eq 23
37
+ expect(subject.value.utc_offset).to eq 28800
38
+ expect(subject.value.utc.hour).to eq 15
39
+ end
40
+
41
+ it 'updates the tzid' do
42
+ # use an array because that's how output from the parser will end up
43
+ expect(subject.ical_params['tzid']).to eq ['Asia/Singapore']
44
+ end
45
+ end
30
46
  end
31
47
 
32
48
  else
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: icalendar
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.8.0
4
+ version: 2.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Ahearn
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-11 00:00:00.000000000 Z
11
+ date: 2023-11-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ice_cube
@@ -187,8 +187,6 @@ files:
187
187
  - lib/icalendar/todo.rb
188
188
  - lib/icalendar/tzinfo.rb
189
189
  - lib/icalendar/value.rb
190
- - lib/icalendar/values/active_support_time_with_zone_adapter.rb
191
- - lib/icalendar/values/array.rb
192
190
  - lib/icalendar/values/binary.rb
193
191
  - lib/icalendar/values/boolean.rb
194
192
  - lib/icalendar/values/cal_address.rb
@@ -197,12 +195,14 @@ files:
197
195
  - lib/icalendar/values/date_time.rb
198
196
  - lib/icalendar/values/duration.rb
199
197
  - lib/icalendar/values/float.rb
198
+ - lib/icalendar/values/helpers/active_support_time_with_zone_adapter.rb
199
+ - lib/icalendar/values/helpers/array.rb
200
+ - lib/icalendar/values/helpers/time_with_zone.rb
200
201
  - lib/icalendar/values/integer.rb
201
202
  - lib/icalendar/values/period.rb
202
203
  - lib/icalendar/values/recur.rb
203
204
  - lib/icalendar/values/text.rb
204
205
  - lib/icalendar/values/time.rb
205
- - lib/icalendar/values/time_with_zone.rb
206
206
  - lib/icalendar/values/uri.rb
207
207
  - lib/icalendar/values/utc_offset.rb
208
208
  - lib/icalendar/version.rb
@@ -247,7 +247,8 @@ licenses:
247
247
  - BSD-2-Clause
248
248
  - GPL-3.0-only
249
249
  - icalendar
250
- metadata: {}
250
+ metadata:
251
+ changelog_uri: https://github.com/icalendar/icalendar/blob/main/History.txt
251
252
  post_install_message: 'ActiveSupport is required for TimeWithZone support, but not
252
253
  required for general use.
253
254
 
@@ -266,7 +267,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
266
267
  - !ruby/object:Gem::Version
267
268
  version: '0'
268
269
  requirements: []
269
- rubygems_version: 3.3.7
270
+ rubygems_version: 3.4.10
270
271
  signing_key:
271
272
  specification_version: 4
272
273
  summary: A ruby implementation of the iCalendar specification (RFC-5545).
@@ -1,12 +0,0 @@
1
- module Icalendar
2
- module Values
3
- class ActiveSupportTimeWithZoneAdapter < ActiveSupport::TimeWithZone
4
- # ActiveSupport::TimeWithZone implements a #to_a method that will cause
5
- # unexpected behavior in components with multi_property DateTime
6
- # properties when the setters for those properties are invoked with an
7
- # Icalendar::Values::DateTime that is delegating for an
8
- # ActiveSupport::TimeWithZone. To avoid this behavior, undefine #to_a.
9
- undef_method :to_a
10
- end
11
- end
12
- end
@@ -1,58 +0,0 @@
1
- module Icalendar
2
- module Values
3
-
4
- class Array < Value
5
-
6
- attr_reader :value_delimiter
7
-
8
- def initialize(value, klass, params = {}, options = {})
9
- @value_delimiter = options[:delimiter] || ','
10
- mapped = if value.is_a? ::Array
11
- value.map do |v|
12
- if v.is_a? Icalendar::Values::Array
13
- Icalendar::Values::Array.new v.value, klass, v.ical_params, delimiter: v.value_delimiter
14
- elsif v.is_a? ::Array
15
- Icalendar::Values::Array.new v, klass, params, delimiter: value_delimiter
16
- elsif v.is_a? Icalendar::Value
17
- v
18
- else
19
- klass.new v, params
20
- end
21
- end
22
- else
23
- [klass.new(value, params)]
24
- end
25
- super mapped
26
- end
27
-
28
- def params_ical
29
- value.each do |v|
30
- ical_params.merge! v.ical_params
31
- end
32
- super
33
- end
34
-
35
- def value_ical
36
- value.map do |v|
37
- v.value_ical
38
- end.join value_delimiter
39
- end
40
-
41
- def valid?
42
- klass = value.first.class
43
- !value.all? { |v| v.class == klass }
44
- end
45
-
46
- def value_type
47
- value.first.value_type
48
- end
49
-
50
- private
51
-
52
- def needs_value_type?(default_type)
53
- value.first.class != default_type
54
- end
55
- end
56
-
57
- end
58
- end
@@ -1,53 +0,0 @@
1
- require 'icalendar/timezone_store'
2
-
3
- begin
4
- require 'active_support/time'
5
-
6
- if defined?(ActiveSupport::TimeWithZone)
7
- require 'icalendar/values/active_support_time_with_zone_adapter'
8
- end
9
- rescue NameError
10
- # ActiveSupport v7+ needs the base require to be run first before loading
11
- # specific parts of it.
12
- # https://guides.rubyonrails.org/active_support_core_extensions.html#stand-alone-active-support
13
- require 'active_support'
14
- retry
15
- rescue LoadError
16
- # tis ok, just a bit less fancy
17
- end
18
-
19
- module Icalendar
20
- module Values
21
- module TimeWithZone
22
- attr_reader :tz_utc
23
-
24
- def initialize(value, params = {})
25
- params = Icalendar::DowncasedHash(params)
26
- @tz_utc = params['tzid'] == 'UTC'
27
- x_tz_info = params.delete 'x-tz-info'
28
-
29
- offset_value = unless params['tzid'].nil?
30
- tzid = params['tzid'].is_a?(::Array) ? params['tzid'].first : params['tzid']
31
- if defined?(ActiveSupport::TimeZone) &&
32
- defined?(ActiveSupportTimeWithZoneAdapter) &&
33
- (tz = ActiveSupport::TimeZone[tzid])
34
- ActiveSupportTimeWithZoneAdapter.new(nil, tz, value)
35
- elsif !x_tz_info.nil?
36
- offset = x_tz_info.offset_for_local(value).to_s
37
- if value.respond_to?(:change)
38
- value.change offset: offset
39
- else
40
- ::Time.new value.year, value.month, value.day, value.hour, value.min, value.sec, offset
41
- end
42
- end
43
- end
44
- super((offset_value || value), params)
45
- end
46
-
47
- def params_ical
48
- ical_params.delete 'tzid' if tz_utc
49
- super
50
- end
51
- end
52
- end
53
- end