icalendar 2.7.1 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +33 -0
  3. data/History.txt +14 -0
  4. data/icalendar.gemspec +1 -0
  5. data/lib/icalendar/alarm.rb +3 -0
  6. data/lib/icalendar/calendar.rb +48 -0
  7. data/lib/icalendar/component.rb +8 -2
  8. data/lib/icalendar/downcased_hash.rb +2 -0
  9. data/lib/icalendar/event.rb +6 -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 +20 -14
  13. data/lib/icalendar/journal.rb +4 -0
  14. data/lib/icalendar/logger.rb +2 -0
  15. data/lib/icalendar/marshable.rb +2 -0
  16. data/lib/icalendar/parser.rb +39 -15
  17. data/lib/icalendar/timezone.rb +2 -0
  18. data/lib/icalendar/timezone_store.rb +2 -0
  19. data/lib/icalendar/todo.rb +6 -1
  20. data/lib/icalendar/tzinfo.rb +2 -0
  21. data/lib/icalendar/value.rb +10 -2
  22. data/lib/icalendar/values/active_support_time_with_zone_adapter.rb +2 -0
  23. data/lib/icalendar/values/array.rb +2 -1
  24. data/lib/icalendar/values/binary.rb +5 -1
  25. data/lib/icalendar/values/boolean.rb +2 -0
  26. data/lib/icalendar/values/cal_address.rb +2 -0
  27. data/lib/icalendar/values/date.rb +2 -0
  28. data/lib/icalendar/values/date_time.rb +2 -0
  29. data/lib/icalendar/values/duration.rb +15 -6
  30. data/lib/icalendar/values/float.rb +2 -0
  31. data/lib/icalendar/values/integer.rb +2 -0
  32. data/lib/icalendar/values/period.rb +5 -1
  33. data/lib/icalendar/values/recur.rb +31 -14
  34. data/lib/icalendar/values/text.rb +5 -1
  35. data/lib/icalendar/values/time.rb +2 -0
  36. data/lib/icalendar/values/time_with_zone.rb +8 -0
  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/alarm_spec.rb +7 -2
  42. data/spec/calendar_spec.rb +68 -0
  43. data/spec/event_spec.rb +4 -2
  44. metadata +8 -5
  45. data/.travis.yml +0 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fbeadc87a1316894e02896fd63e7f7644a1ab5bf3a542b0d5a02c59aabf2d603
4
- data.tar.gz: 3aaf09d7984fed57644d091fa76285084d9fbf3568e6078939e7887d6f44f50a
3
+ metadata.gz: 0f083428b61c781e197d459b5ba375139056f75e95b1b99e596169106a57ed20
4
+ data.tar.gz: acb3cdb07b0dc052664fc67619fa6301951daf70ac83a0430a839a0bdd3a5923
5
5
  SHA512:
6
- metadata.gz: e6134ff7d12c820d7ee10a316108c1d7a9db274cccd366b93a49fa491931038edd78eb37789a24962cc6370c83fd34d67508dd24b574e8d1fa235e815c1fa22d
7
- data.tar.gz: 0bad8ab7548493a133686dab3a6d36a58aeecddb8642da83cef5a2184d0d08e97dc59307fc6fdd9574935319044ccb5ffa50f9c936aa60e5637a2430e78eb446
6
+ metadata.gz: 595032415b8465be6f662f136a56dd6f4dd8af27e888c7a7b27ad1c688b3963466e34a949daa0111a65ea89037b33ea01d8f2ea24991f5d9a3869647521b6c7c
7
+ data.tar.gz: ac464077d0c421d27b39270f1d12af23b59383e3e8351b68ec41a8385cc26670ca31b2f8dba72212e8db0996ea4a9e7c4a1b7b6a9b1b34d632169fae5a715725
@@ -0,0 +1,33 @@
1
+ name: Ruby
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - master
8
+ pull_request:
9
+ branches:
10
+ - main
11
+ - master
12
+
13
+ jobs:
14
+ build:
15
+ runs-on: ubuntu-latest
16
+
17
+ strategy:
18
+ matrix:
19
+ ruby:
20
+ - 2.7.6
21
+ - 3.0.4
22
+ - 3.1.2
23
+ - 3.2.2
24
+
25
+ steps:
26
+ - uses: actions/checkout@v2
27
+ - name: Set up Ruby
28
+ uses: ruby/setup-ruby@v1
29
+ with:
30
+ ruby-version: ${{ matrix.ruby }}
31
+ bundler-cache: true
32
+ - name: Run rspec tests
33
+ run: bundle exec rake spec
data/History.txt CHANGED
@@ -1,3 +1,17 @@
1
+ === Unreleased
2
+
3
+ === 2.9.0 2023-08-11
4
+ * Always include the VALUE of Event URLs for improved compatibility - Sean Kelley
5
+ * Improved parse performance - Thomas Cannon
6
+ * Add helper methods for other Calendar method verbs
7
+ * bugfix: Require stringio before use in Parser - vwyu
8
+
9
+ === 2.8.0 2022-07-10
10
+ * Fix compatibility with ActiveSupport 7 - Pat Allan
11
+ * Set default action of "DISPLAY" on alarms - Rikson
12
+ * Add license information to gemspec - Robert Reiz
13
+ * Support RFC7986 properties - Daniele Frisanco
14
+
1
15
  === 2.7.1 2021-03-14
2
16
  * Recover from bad line-wrapping code that splits in the middle of Unicode code points
3
17
  * Add a verbose option to the Parser to quiet some of the chattier log entries
data/icalendar.gemspec CHANGED
@@ -6,6 +6,7 @@ Gem::Specification.new do |s|
6
6
 
7
7
  s.name = "icalendar"
8
8
  s.version = Icalendar::VERSION
9
+ s.licenses = ['BSD-2-Clause', 'GPL-3.0-only', 'icalendar']
9
10
 
10
11
  s.homepage = "https://github.com/icalendar/icalendar"
11
12
  s.platform = Gem::Platform::RUBY
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Alarm < Component
@@ -22,6 +24,7 @@ module Icalendar
22
24
 
23
25
  def initialize
24
26
  super 'alarm'
27
+ self.action = 'DISPLAY'
25
28
  end
26
29
 
27
30
  def valid?(strict = false)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Calendar < Component
@@ -5,6 +7,16 @@ module Icalendar
5
7
  required_property :prodid
6
8
  optional_single_property :calscale
7
9
  optional_single_property :ip_method
10
+ optional_property :ip_name
11
+ optional_property :description
12
+ optional_single_property :uid
13
+ optional_single_property :last_modified, Icalendar::Values::DateTime, true
14
+ optional_single_property :url, Icalendar::Values::Uri, true
15
+ optional_property :categories
16
+ optional_single_property :refresh_interval, Icalendar::Values::Duration, true
17
+ optional_single_property :source, Icalendar::Values::Uri, true
18
+ optional_single_property :color
19
+ optional_property :image, Icalendar::Values::Uri, false, true
8
20
 
9
21
  component :timezone, :tzid
10
22
  component :event
@@ -21,6 +33,42 @@ module Icalendar
21
33
 
22
34
  def publish
23
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
24
72
  end
25
73
 
26
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
@@ -12,6 +14,7 @@ module Icalendar
12
14
  mutually_exclusive_properties :dtend, :duration
13
15
 
14
16
  optional_single_property :ip_class
17
+ optional_single_property :color
15
18
  optional_single_property :created, Icalendar::Values::DateTime
16
19
  optional_single_property :description
17
20
  optional_single_property :geo, Icalendar::Values::Float
@@ -23,7 +26,7 @@ module Icalendar
23
26
  optional_single_property :status
24
27
  optional_single_property :summary
25
28
  optional_single_property :transp
26
- optional_single_property :url, Icalendar::Values::Uri
29
+ optional_single_property :url, Icalendar::Values::Uri, true
27
30
  optional_single_property :recurrence_id, Icalendar::Values::DateTime
28
31
 
29
32
  optional_property :rrule, Icalendar::Values::Recur, true
@@ -37,6 +40,8 @@ module Icalendar
37
40
  optional_property :related_to
38
41
  optional_property :resources
39
42
  optional_property :rdate, Icalendar::Values::DateTime
43
+ optional_property :conference, Icalendar::Values::Uri, false, true
44
+ optional_property :image, Icalendar::Values::Uri, false, true
40
45
 
41
46
  component :alarm, false
42
47
 
@@ -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
@@ -104,29 +106,29 @@ module Icalendar
104
106
  def required_property(prop, klass = Icalendar::Values::Text, validator = nil)
105
107
  validator ||= ->(component, value) { !value.nil? }
106
108
  self.required_properties[prop] = validator
107
- single_property prop, klass
109
+ single_property prop, klass, false
108
110
  end
109
111
 
110
112
  def required_multi_property(prop, klass = Icalendar::Values::Text, validator = nil)
111
113
  validator ||= ->(component, value) { !value.compact.empty? }
112
114
  self.required_properties[prop] = validator
113
- multi_property prop, klass
115
+ multi_property prop, klass, false
114
116
  end
115
117
 
116
- def optional_single_property(prop, klass = Icalendar::Values::Text)
117
- single_property prop, klass
118
+ def optional_single_property(prop, klass = Icalendar::Values::Text, new_property = false)
119
+ single_property prop, klass, new_property
118
120
  end
119
121
 
120
122
  def mutually_exclusive_properties(*properties)
121
123
  self.mutex_properties << properties
122
124
  end
123
125
 
124
- def optional_property(prop, klass = Icalendar::Values::Text, suggested_single = false)
126
+ def optional_property(prop, klass = Icalendar::Values::Text, suggested_single = false, new_property = false)
125
127
  self.suggested_single_properties << prop if suggested_single
126
- multi_property prop, klass
128
+ multi_property prop, klass, new_property
127
129
  end
128
130
 
129
- def single_property(prop, klass)
131
+ def single_property(prop, klass, new_property)
130
132
  self.single_properties << prop.to_s
131
133
  self.default_property_types[prop.to_s] = klass
132
134
 
@@ -135,17 +137,17 @@ module Icalendar
135
137
  end
136
138
 
137
139
  define_method "#{prop}=" do |value|
138
- instance_variable_set "@#{prop}", map_property_value(value, klass, false)
140
+ instance_variable_set "@#{prop}", map_property_value(value, klass, false, new_property)
139
141
  end
140
142
  end
141
143
 
142
- def multi_property(prop, klass)
144
+ def multi_property(prop, klass, new_property)
143
145
  self.multiple_properties << prop.to_s
144
146
  self.default_property_types[prop.to_s] = klass
145
147
  property_var = "@#{prop}"
146
148
 
147
149
  define_method "#{prop}=" do |value|
148
- mapped = map_property_value value, klass, true
150
+ mapped = map_property_value value, klass, true, new_property
149
151
  if mapped.is_a? Icalendar::Values::Array
150
152
  instance_variable_set property_var, mapped.to_a.compact
151
153
  else
@@ -162,20 +164,24 @@ module Icalendar
162
164
  end
163
165
 
164
166
  define_method "append_#{prop}" do |value|
165
- send(prop) << map_property_value(value, klass, true)
167
+ send(prop) << map_property_value(value, klass, true, new_property)
166
168
  end
167
169
  end
168
170
  end
169
171
 
170
172
  private
171
173
 
172
- def map_property_value(value, klass, multi_valued)
174
+ def map_property_value(value, klass, multi_valued, new_property)
175
+ params = {}
176
+ if new_property
177
+ params.merge!('VALUE': klass.value_type)
178
+ end
173
179
  if value.nil? || value.is_a?(Icalendar::Value)
174
180
  value
175
181
  elsif value.is_a? ::Array
176
- Icalendar::Values::Array.new value, klass, {}, {delimiter: (multi_valued ? ',' : ';')}
182
+ Icalendar::Values::Array.new value, klass, params, {delimiter: (multi_valued ? ',' : ';')}
177
183
  else
178
- klass.new value
184
+ klass.new value, params
179
185
  end
180
186
  end
181
187
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Journal < Component
@@ -6,6 +8,7 @@ module Icalendar
6
8
  required_property :uid
7
9
 
8
10
  optional_single_property :ip_class
11
+ optional_single_property :color
9
12
  optional_single_property :created, Icalendar::Values::DateTime
10
13
  optional_single_property :dtstart, Icalendar::Values::DateTime
11
14
  optional_single_property :last_modified, Icalendar::Values::DateTime
@@ -27,6 +30,7 @@ module Icalendar
27
30
  optional_property :request_status
28
31
  optional_property :related_to
29
32
  optional_property :rdate, Icalendar::Values::DateTime
33
+ optional_property :image, Icalendar::Values::Uri, false, true
30
34
 
31
35
  def initialize
32
36
  super 'journal'
@@ -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)
@@ -50,7 +55,7 @@ module Icalendar
50
55
 
51
56
  def parse_property(component, fields = nil)
52
57
  fields = next_fields if fields.nil?
53
- prop_name = %w(class method).include?(fields[:name]) ? "ip_#{fields[:name]}" : fields[:name]
58
+ prop_name = %w(class method name).include?(fields[:name]) ? "ip_#{fields[:name]}" : fields[:name]
54
59
  multi_property = component.class.multiple_properties.include? prop_name
55
60
  prop_value = wrap_property_value component, fields, multi_property
56
61
  begin
@@ -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::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
@@ -12,6 +14,7 @@ module Icalendar
12
14
  mutually_exclusive_properties :due, :duration
13
15
 
14
16
  optional_single_property :ip_class
17
+ optional_single_property :color
15
18
  optional_single_property :completed, Icalendar::Values::DateTime
16
19
  optional_single_property :created, Icalendar::Values::DateTime
17
20
  optional_single_property :description
@@ -38,6 +41,8 @@ module Icalendar
38
41
  optional_property :related_to
39
42
  optional_property :resources
40
43
  optional_property :rdate, Icalendar::Values::DateTime
44
+ optional_property :conference, Icalendar::Values::Uri, false, true
45
+ optional_property :image, Icalendar::Values::Uri, false, true
41
46
 
42
47
  component :alarm, false
43
48
 
@@ -49,4 +54,4 @@ module Icalendar
49
54
 
50
55
  end
51
56
 
52
- end
57
+ end
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
  class ActiveSupportTimeWithZoneAdapter < ActiveSupport::TimeWithZone
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
 
@@ -52,7 +54,6 @@ module Icalendar
52
54
  def needs_value_type?(default_type)
53
55
  value.first.class != default_type
54
56
  end
55
-
56
57
  end
57
58
 
58
59
  end
@@ -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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
4
  require_relative 'time_with_zone'
3
5
 
@@ -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
 
@@ -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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
4
  require_relative 'time_with_zone'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'icalendar/timezone_store'
2
4
 
3
5
  begin
@@ -6,6 +8,12 @@ begin
6
8
  if defined?(ActiveSupport::TimeWithZone)
7
9
  require 'icalendar/values/active_support_time_with_zone_adapter'
8
10
  end
11
+ rescue NameError
12
+ # ActiveSupport v7+ needs the base require to be run first before loading
13
+ # specific parts of it.
14
+ # https://guides.rubyonrails.org/active_support_core_extensions.html#stand-alone-active-support
15
+ require 'active_support'
16
+ retry
9
17
  rescue LoadError
10
18
  # tis ok, just a bit less fancy
11
19
  end
@@ -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.7.1'
5
+ VERSION = '2.9.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
data/spec/alarm_spec.rb CHANGED
@@ -6,14 +6,15 @@ describe Icalendar::Alarm do
6
6
  describe '#valid?' do
7
7
  subject do
8
8
  described_class.new.tap do |a|
9
- a.action = 'AUDIO'
10
9
  a.trigger = Icalendar::Values::DateTime.new(Time.now.utc)
11
10
  end
12
11
  end
13
12
  context 'neither duration or repeat is set' do
13
+ before(:each) { subject.action = 'AUDIO' }
14
14
  it { should be_valid }
15
15
  end
16
16
  context 'both duration and repeat are set' do
17
+ before(:each) { subject.action = 'AUDIO' }
17
18
  before(:each) do
18
19
  subject.duration = 'PT15M'
19
20
  subject.repeat = 4
@@ -29,8 +30,12 @@ describe Icalendar::Alarm do
29
30
  it { should_not be_valid }
30
31
  end
31
32
 
33
+ context 'DISPLAY is set as default action' do
34
+ it 'must be DISPLAY' do
35
+ expect(subject.action).to eq 'DISPLAY'
36
+ end
37
+ end
32
38
  context 'display action' do
33
- before(:each) { subject.action = 'DISPLAY' }
34
39
  it 'requires description' do
35
40
  expect(subject).to_not be_valid
36
41
  subject.description = 'Display Text'
@@ -123,6 +123,16 @@ describe Icalendar::Calendar do
123
123
  describe '#to_ical' do
124
124
  before(:each) do
125
125
  Timecop.freeze DateTime.new(2013, 12, 26, 5, 0, 0, '+0000')
126
+ subject.ip_name = 'Company Vacation Days'
127
+ subject.description = 'The description'
128
+ subject.last_modified = "20140101T000000Z"
129
+ subject.url = 'https://example.com'
130
+ subject.color = 'red'
131
+ subject.image = 'https://example.com/image.png'
132
+ subject.uid = '5FC53010-1267-4F8E-BC28-1D7AE55A7C99'
133
+ subject.categories = 'MEETING'
134
+ subject.refresh_interval = 'P1W'
135
+ subject.source = 'https://example.com/holidays.ics'
126
136
  subject.event do |e|
127
137
  e.summary = 'An event'
128
138
  e.dtstart = "20140101T000000Z"
@@ -145,6 +155,15 @@ BEGIN:VCALENDAR
145
155
  VERSION:2.0
146
156
  PRODID:icalendar-ruby
147
157
  CALSCALE:GREGORIAN
158
+ LAST-MODIFIED;VALUE=DATE-TIME:20140101T000000Z
159
+ URL;VALUE=URI:https://example.com
160
+ REFRESH-INTERVAL;VALUE=DURATION:P1W
161
+ SOURCE;VALUE=URI:https://example.com/holidays.ics
162
+ COLOR:red
163
+ NAME:Company Vacation Days
164
+ DESCRIPTION:The description
165
+ CATEGORIES:MEETING
166
+ IMAGE;VALUE=URI:https://example.com/image.png
148
167
  BEGIN:VEVENT
149
168
  DTSTAMP:20131226T050000Z
150
169
  DTSTART:20140101T000000Z
@@ -171,6 +190,55 @@ END:VCALENDAR
171
190
  end
172
191
  end
173
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
+
174
242
  describe '.parse' do
175
243
  let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'bad_wrapping.ics') }
176
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
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.7.1
4
+ version: 2.9.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: 2021-03-15 00:00:00.000000000 Z
11
+ date: 2023-08-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ice_cube
@@ -160,9 +160,9 @@ executables: []
160
160
  extensions: []
161
161
  extra_rdoc_files: []
162
162
  files:
163
+ - ".github/workflows/main.yml"
163
164
  - ".gitignore"
164
165
  - ".rspec"
165
- - ".travis.yml"
166
166
  - Gemfile
167
167
  - History.txt
168
168
  - LICENSE
@@ -243,7 +243,10 @@ files:
243
243
  - spec/values/text_spec.rb
244
244
  - spec/values/utc_offset_spec.rb
245
245
  homepage: https://github.com/icalendar/icalendar
246
- licenses: []
246
+ licenses:
247
+ - BSD-2-Clause
248
+ - GPL-3.0-only
249
+ - icalendar
247
250
  metadata: {}
248
251
  post_install_message: 'ActiveSupport is required for TimeWithZone support, but not
249
252
  required for general use.
@@ -263,7 +266,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
263
266
  - !ruby/object:Gem::Version
264
267
  version: '0'
265
268
  requirements: []
266
- rubygems_version: 3.2.11
269
+ rubygems_version: 3.3.7
267
270
  signing_key:
268
271
  specification_version: 4
269
272
  summary: A ruby implementation of the iCalendar specification (RFC-5545).
data/.travis.yml DELETED
@@ -1,17 +0,0 @@
1
- sudo: false
2
- before_install:
3
- - gem install bundler
4
- language: ruby
5
- rvm:
6
- - 2.7
7
- - 2.6
8
- - 2.5
9
- - jruby-19mode
10
- - ruby-head
11
- - jruby-head
12
- - truffleruby-head
13
- matrix:
14
- allow_failures:
15
- - rvm: ruby-head
16
- - rvm: jruby-head
17
- script: bundle exec rake spec