icalendar 2.7.1 → 2.9.0

Sign up to get free protection for your applications and to get access to all the features.
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