icalendar 2.5.2 → 2.7.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d8af55548918f31a965cd6bb855b7a85ed23ebf64a3387ea10f7ef83c4df0b5
4
- data.tar.gz: 85a6ce28d96b1e7c830091b0782e15e515a41cea1556af30d0d29cadac215ec2
3
+ metadata.gz: fbeadc87a1316894e02896fd63e7f7644a1ab5bf3a542b0d5a02c59aabf2d603
4
+ data.tar.gz: 3aaf09d7984fed57644d091fa76285084d9fbf3568e6078939e7887d6f44f50a
5
5
  SHA512:
6
- metadata.gz: 37c2b1f00e479da82a795b52fe82e635ef46f3ea5e62552860107f0d61ec0f5866871334c31b14034e59f29498086eb338f6198313366877bc9b01cd7e9206f3
7
- data.tar.gz: 7338852a7e3dd937c01547b6ed431bfc9ac142deb8238d1cded8d5ae37f23b1dcb61b412e21308bd40788fa497ddd13bc0bb89d36ae56587c9cbd0f6cf3b107c
6
+ metadata.gz: e6134ff7d12c820d7ee10a316108c1d7a9db274cccd366b93a49fa491931038edd78eb37789a24962cc6370c83fd34d67508dd24b574e8d1fa235e815c1fa22d
7
+ data.tar.gz: 0bad8ab7548493a133686dab3a6d36a58aeecddb8642da83cef5a2184d0d08e97dc59307fc6fdd9574935319044ccb5ffa50f9c936aa60e5637a2430e78eb446
data/.gitignore CHANGED
@@ -6,3 +6,4 @@ pkg/
6
6
  .bundle
7
7
  coverage/
8
8
  tags
9
+ .idea
data/.travis.yml CHANGED
@@ -3,17 +3,15 @@ before_install:
3
3
  - gem install bundler
4
4
  language: ruby
5
5
  rvm:
6
+ - 2.7
7
+ - 2.6
6
8
  - 2.5
7
- - 2.4
8
- - 2.3
9
- - 2.2
10
9
  - jruby-19mode
11
- - rbx-3
12
10
  - ruby-head
13
11
  - jruby-head
12
+ - truffleruby-head
14
13
  matrix:
15
14
  allow_failures:
16
- - rvm: 2.2
17
15
  - rvm: ruby-head
18
16
  - rvm: jruby-head
19
17
  script: bundle exec rake spec
data/History.txt CHANGED
@@ -1,3 +1,24 @@
1
+ === 2.7.1 2021-03-14
2
+ * Recover from bad line-wrapping code that splits in the middle of Unicode code points
3
+ * Add a verbose option to the Parser to quiet some of the chattier log entries
4
+
5
+ === 2.7.0 2020-09-12
6
+ * Handle custom component names, with and without X- prefix
7
+ * Fix Component lookup to avoid namespace collisions
8
+
9
+ === 2.6.1 2019-12-07
10
+ * Improve performance when generating large ICS files - Alex Balhatchet
11
+
12
+ === 2.6.0 2019-11-26
13
+ * Improve performance for calculating timezone offsets - Justin Howard
14
+ * Make it possible to de/serialize with Marshal - Pawel Niewiadomski
15
+ * Avoid FrozenError when running with frozen_string_literal
16
+ * Update minimum Ruby version to supported versions
17
+
18
+ === 2.5.3 2019-03-04
19
+ * Improve parsing performance - nehresma
20
+ * Support tzinfo 2.0 - Misty De Meo
21
+
1
22
  === 2.5.2 2018-12-08
2
23
  * Remove usage of the global TimezoneStore instance, in favor of a local variable in the parser
3
24
  * Deprecate TimezoneStore class methods
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  iCalendar -- Internet calendaring, Ruby style
2
2
  ===
3
3
 
4
- [![Build Status](https://travis-ci.org/icalendar/icalendar.svg?branch=master)](https://travis-ci.org/icalendar/icalendar)
4
+ [![Build Status](https://travis-ci.com/icalendar/icalendar.svg?branch=master)](https://travis-ci.com/icalendar/icalendar)
5
5
  [![Code Climate](https://codeclimate.com/github/icalendar/icalendar.png)](https://codeclimate.com/github/icalendar/icalendar)
6
6
 
7
7
  <http://github.com/icalendar/icalendar>
@@ -115,7 +115,7 @@ cal.event do |e|
115
115
  a.attendee = %w(mailto:me@my-domain.com mailto:me-too@my-domain.com) # one or more email recipients (required)
116
116
  a.append_attendee "mailto:me-three@my-domain.com"
117
117
  a.trigger = "-PT15M" # 15 minutes before
118
- a.append_attach Icalendar::Values::Uri.new "ftp://host.com/novo-procs/felizano.exe", "fmttype" => "application/binary" # email attachments (optional)
118
+ a.append_attach Icalendar::Values::Uri.new("ftp://host.com/novo-procs/felizano.exe", "fmttype" => "application/binary") # email attachments (optional)
119
119
  end
120
120
 
121
121
  e.alarm do |a|
@@ -220,7 +220,8 @@ end
220
220
  iCalendar has some basic support for creating VTIMEZONE blocks from timezone information pulled from `tzinfo`.
221
221
  You must require `tzinfo` support manually to take advantage.
222
222
 
223
- iCalendar has been tested and works with `tzinfo` versions 0.3 and 1.x
223
+ iCalendar has been tested and works with `tzinfo` versions 0.3, 1.x, and 2.x. The `tzinfo-data` gem may also
224
+ be required depending on your version of `tzinfo` and potentially your operating system.
224
225
 
225
226
  #### Example ####
226
227
 
data/icalendar.gemspec CHANGED
@@ -24,22 +24,23 @@ ActiveSupport is required for TimeWithZone support, but not required for general
24
24
  s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename f }
25
25
  s.require_paths = ['lib']
26
26
 
27
- s.required_ruby_version = '>= 2.2.0'
27
+ s.required_ruby_version = '>= 2.4.0'
28
28
 
29
29
  s.add_dependency 'ice_cube', '~> 0.16'
30
30
 
31
- s.add_development_dependency 'rake', '~> 12.0'
32
- s.add_development_dependency 'bundler', '~> 1.16'
31
+ s.add_development_dependency 'rake', '~> 13.0'
32
+ s.add_development_dependency 'bundler', '~> 2.0'
33
33
 
34
- # test with both groups of tzinfo dependencies
34
+ # test with all groups of tzinfo dependencies
35
+ # tzinfo 2.x
36
+ # s.add_development_dependency 'tzinfo', '~> 2.0'
37
+ # s.add_development_dependency 'tzinfo-data', '~> 1.2020'
35
38
  # tzinfo 1.x
36
- s.add_development_dependency 'activesupport', '~> 5.2'
37
- s.add_development_dependency 'i18n', '~> 1.1'
39
+ s.add_development_dependency 'activesupport', '~> 6.0'
40
+ s.add_development_dependency 'i18n', '~> 1.8'
38
41
  s.add_development_dependency 'tzinfo', '~> 1.2'
39
- s.add_development_dependency 'tzinfo-data', '~> 1.2018'
42
+ s.add_development_dependency 'tzinfo-data', '~> 1.2020'
40
43
  # tzinfo 0.x
41
- # s.add_development_dependency 'activesupport', '~> 4.0'
42
- # s.add_development_dependency 'i18n', '~> 0.7'
43
44
  # s.add_development_dependency 'tzinfo', '~> 0.3'
44
45
  # end tzinfo
45
46
 
data/lib/icalendar.rb CHANGED
@@ -22,6 +22,7 @@ end
22
22
 
23
23
  require 'icalendar/has_properties'
24
24
  require 'icalendar/has_components'
25
+ require 'icalendar/marshable'
25
26
  require 'icalendar/component'
26
27
  require 'icalendar/value'
27
28
  require 'icalendar/alarm'
@@ -32,3 +33,4 @@ require 'icalendar/freebusy'
32
33
  require 'icalendar/timezone'
33
34
  require 'icalendar/calendar'
34
35
  require 'icalendar/parser'
36
+ require 'icalendar/version'
@@ -11,9 +11,10 @@ module Icalendar
11
11
  attr_accessor :parent
12
12
 
13
13
  def self.parse(source)
14
- parser = Parser.new(source)
15
- parser.component_class = self
16
- parser.parse
14
+ _parse source
15
+ rescue ArgumentError
16
+ source.rewind if source.respond_to?(:rewind)
17
+ _parse Parser.clean_bad_wrapping(source)
17
18
  end
18
19
 
19
20
  def initialize(name, ical_name = nil)
@@ -71,6 +72,8 @@ module Icalendar
71
72
  # than 75 octets, but you need to split between characters, not bytes.
72
73
  # This is challanging with Unicode composing accents, for example.
73
74
 
75
+ return long_line if long_line.bytesize <= Icalendar::MAX_LINE_LENGTH
76
+
74
77
  chars = long_line.scan(/\P{M}\p{M}*/u) # split in graphenes
75
78
  folded = ['']
76
79
  bytes = 0
@@ -99,6 +102,14 @@ module Icalendar
99
102
  end
100
103
  collection.empty? ? nil : collection.join.chomp("\r\n")
101
104
  end
105
+
106
+ class << self
107
+ private def _parse(source)
108
+ parser = Parser.new(source)
109
+ parser.component_class = self
110
+ parser.parse
111
+ end
112
+ end
102
113
  end
103
114
 
104
115
  end
@@ -10,7 +10,7 @@ module Icalendar
10
10
  end
11
11
 
12
12
  def initialize(*args)
13
- @custom_components = Hash.new { |h, k| h[k] = [] }
13
+ @custom_components = Hash.new
14
14
  super
15
15
  end
16
16
 
@@ -21,21 +21,33 @@ module Icalendar
21
21
  c
22
22
  end
23
23
 
24
+ def add_custom_component(component_name, c)
25
+ c.parent = self
26
+ yield c if block_given?
27
+ (custom_components[component_name.downcase.gsub("-", "_")] ||= []) << c
28
+ c
29
+ end
30
+
31
+ def custom_component(component_name)
32
+ custom_components[component_name.downcase.gsub("-", "_")] || []
33
+ end
34
+
24
35
  def method_missing(method, *args, &block)
25
36
  method_name = method.to_s
26
37
  if method_name =~ /^add_(x_\w+)$/
27
38
  component_name = $1
28
39
  custom = args.first || Component.new(component_name, component_name.upcase)
29
- custom_components[component_name] << custom
30
- yield custom if block_given?
31
- custom
40
+ add_custom_component(component_name, custom, &block)
41
+ elsif method_name =~ /^x_/ && custom_component(method_name).size > 0
42
+ custom_component method_name
32
43
  else
33
44
  super
34
45
  end
35
46
  end
36
47
 
37
48
  def respond_to_missing?(method_name, include_private = false)
38
- method_name.to_s.start_with?('add_x_') || super
49
+ string_method = method_name.to_s
50
+ string_method.start_with?('add_x_') || custom_component(string_method).size > 0 || super
39
51
  end
40
52
 
41
53
  module ClassMethods
@@ -10,7 +10,7 @@ module Icalendar
10
10
  end
11
11
 
12
12
  def initialize(*args)
13
- @custom_properties = Hash.new { |h, k| h[k] = [] }
13
+ @custom_properties = Hash.new
14
14
  super
15
15
  end
16
16
 
@@ -39,7 +39,7 @@ module Icalendar
39
39
  end
40
40
 
41
41
  def custom_property(property_name)
42
- custom_properties[property_name.downcase]
42
+ custom_properties[property_name.downcase] || []
43
43
  end
44
44
 
45
45
  def append_custom_property(property_name, value)
@@ -49,9 +49,9 @@ module Icalendar
49
49
  elsif self.class.multiple_properties.include? property_name
50
50
  send "append_#{property_name}", value
51
51
  elsif value.is_a? Icalendar::Value
52
- custom_properties[property_name] << value
52
+ (custom_properties[property_name] ||= []) << value
53
53
  else
54
- custom_properties[property_name] << Icalendar::Values::Text.new(value)
54
+ (custom_properties[property_name] ||= []) << Icalendar::Values::Text.new(value)
55
55
  end
56
56
  end
57
57
 
@@ -0,0 +1,34 @@
1
+ module Icalendar
2
+ module Marshable
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ def marshal_dump
8
+ instance_variables
9
+ .reject { |ivar| self.class.transient_variables.include?(ivar) }
10
+ .each_with_object({}) do |ivar, serialized|
11
+
12
+ serialized[ivar] = instance_variable_get(ivar)
13
+ end
14
+ end
15
+
16
+ def marshal_load(serialized)
17
+ serialized.each do |ivar, value|
18
+ unless self.class.transient_variables.include?(ivar)
19
+ instance_variable_set(ivar, value)
20
+ end
21
+ end
22
+ end
23
+
24
+ module ClassMethods
25
+ def transient_variables
26
+ @transient_variables ||= [:@transient_variables]
27
+ end
28
+
29
+ def transient_variable(name)
30
+ transient_variables.push(name.to_sym)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -4,9 +4,24 @@ module Icalendar
4
4
 
5
5
  class Parser
6
6
  attr_writer :component_class
7
- attr_reader :source, :strict, :timezone_store
7
+ attr_reader :source, :strict, :timezone_store, :verbose
8
8
 
9
- def initialize(source, strict = false)
9
+ def self.clean_bad_wrapping(source)
10
+ content = if source.respond_to? :read
11
+ source.read
12
+ elsif source.respond_to? :to_s
13
+ source.to_s
14
+ else
15
+ msg = 'Icalendar::Parser.clean_bad_wrapping must be called with a String or IO object'
16
+ Icalendar.fatal msg
17
+ fail ArgumentError, msg
18
+ end
19
+ encoding = content.encoding
20
+ content.force_encoding(Encoding::ASCII_8BIT)
21
+ content.gsub(/\r?\n[ \t]/, "").force_encoding(encoding)
22
+ end
23
+
24
+ def initialize(source, strict = false, verbose = false)
10
25
  if source.respond_to? :gets
11
26
  @source = source
12
27
  elsif source.respond_to? :to_s
@@ -18,6 +33,7 @@ module Icalendar
18
33
  end
19
34
  read_in_data
20
35
  @strict = strict
36
+ @verbose = verbose
21
37
  @timezone_store = TimezoneStore.new
22
38
  end
23
39
 
@@ -49,7 +65,7 @@ module Icalendar
49
65
  Icalendar.logger.error "No method \"#{method_name}\" for component #{component}"
50
66
  raise nme
51
67
  else
52
- Icalendar.logger.warn "No method \"#{method_name}\" for component #{component}. Appending to custom."
68
+ Icalendar.logger.warn "No method \"#{method_name}\" for component #{component}. Appending to custom." if verbose?
53
69
  component.append_custom_property prop_name, prop_value
54
70
  end
55
71
  end
@@ -82,8 +98,8 @@ module Icalendar
82
98
  if !fields[:params]['value'].nil?
83
99
  klass_name = fields[:params].delete('value').first
84
100
  unless klass_name.upcase == klass.value_type
85
- klass_name = klass_name.downcase.gsub(/(?:\A|-)(.)/) { |m| m[-1].upcase }
86
- klass = Icalendar::Values.const_get klass_name if Icalendar::Values.const_defined?(klass_name)
101
+ klass_name = "Icalendar::Values::#{klass_name.downcase.gsub(/(?:\A|-)(.)/) { |m| m[-1].upcase }}"
102
+ klass = Object.const_get klass_name if Object.const_defined?(klass_name)
87
103
  end
88
104
  end
89
105
  klass
@@ -93,6 +109,10 @@ module Icalendar
93
109
  !!@strict
94
110
  end
95
111
 
112
+ def verbose?
113
+ @verbose
114
+ end
115
+
96
116
  private
97
117
 
98
118
  def component_class
@@ -106,14 +126,14 @@ module Icalendar
106
126
  timezone_store.store(component) if klass_name == 'Timezone'
107
127
  break
108
128
  elsif fields[:name] == 'begin'
109
- klass_name = fields[:value].gsub(/\AV/, '').downcase.capitalize
129
+ klass_name = fields[:value].gsub(/\AV/, '').gsub("-", "_").downcase.capitalize
110
130
  Icalendar.logger.debug "Adding component #{klass_name}"
111
- if Icalendar.const_defined? klass_name
112
- component.add_component parse_component(Icalendar.const_get(klass_name).new)
113
- elsif Icalendar::Timezone.const_defined? klass_name
114
- component.add_component parse_component(Icalendar::Timezone.const_get(klass_name).new)
131
+ if Object.const_defined? "Icalendar::#{klass_name}"
132
+ component.add_component parse_component(Object.const_get("Icalendar::#{klass_name}").new)
133
+ elsif Object.const_defined? "Icalendar::Timezone::#{klass_name}"
134
+ component.add_component parse_component(Object.const_get("Icalendar::Timezone::#{klass_name}").new)
115
135
  else
116
- component.add_component parse_component(Component.new klass_name.downcase, fields[:value])
136
+ component.add_custom_component klass_name, parse_component(Component.new klass_name.downcase, fields[:value])
117
137
  end
118
138
  else
119
139
  parse_property component, fields
@@ -172,7 +192,11 @@ module Icalendar
172
192
  end
173
193
  end
174
194
  end
175
- Icalendar.logger.debug "Found fields: #{parts.inspect} with params: #{params.inspect}"
195
+ # Building the string to send to the logger is expensive.
196
+ # Only do it if the logger is at the right log level.
197
+ if ::Logger::DEBUG >= Icalendar.logger.level
198
+ Icalendar.logger.debug "Found fields: #{parts.inspect} with params: #{params.inspect}"
199
+ end
176
200
  {
177
201
  name: parts[:name].downcase.gsub('-', '_'),
178
202
  params: params,
@@ -14,10 +14,40 @@ module Icalendar
14
14
  optional_property :comment
15
15
  optional_property :rdate, Icalendar::Values::DateTime
16
16
  optional_property :tzname
17
+
18
+ transient_variable :@cached_occurrences
19
+ transient_variable :@occurrences
17
20
  end
18
21
  end
22
+
23
+ def occurrences
24
+ @occurrences ||= IceCube::Schedule.new(dtstart.to_time) do |s|
25
+ rrule.each do |rule|
26
+ s.add_recurrence_rule IceCube::Rule.from_ical(rule.value_ical)
27
+ end
28
+ rdate.each do |date|
29
+ s.add_recurrence_time date.to_time
30
+ end
31
+ end.all_occurrences_enumerator
32
+ end
33
+
34
+ def previous_occurrence(from)
35
+ from = IceCube::TimeUtil.match_zone(from, dtstart.to_time)
36
+
37
+ @cached_occurrences ||= []
38
+ while @cached_occurrences.empty? || @cached_occurrences.last <= from
39
+ begin
40
+ @cached_occurrences << occurrences.next
41
+ rescue StopIteration
42
+ break
43
+ end
44
+ end
45
+
46
+ @cached_occurrences.reverse_each.find { |occurrence| occurrence < from }
47
+ end
19
48
  end
20
49
  class Daylight < Component
50
+ include Marshable
21
51
  include TzProperties
22
52
 
23
53
  def initialize
@@ -25,6 +55,7 @@ module Icalendar
25
55
  end
26
56
  end
27
57
  class Standard < Component
58
+ include Marshable
28
59
  include TzProperties
29
60
 
30
61
  def initialize
@@ -75,30 +106,14 @@ module Icalendar
75
106
 
76
107
  def standard_for(local)
77
108
  possible = standards.map do |std|
78
- schedule = IceCube::Schedule.new(std.dtstart.to_time) do |s|
79
- std.rrule.each do |rule|
80
- s.add_recurrence_rule IceCube::Rule.from_ical(rule.value_ical)
81
- end
82
- std.rdate.each do |date|
83
- s.add_recurrence_time date.to_time
84
- end
85
- end
86
- [schedule.previous_occurrence(local.to_time), std]
109
+ [std.previous_occurrence(local.to_time), std]
87
110
  end
88
111
  possible.sort_by(&:first).last
89
112
  end
90
113
 
91
114
  def daylight_for(local)
92
115
  possible = daylights.map do |day|
93
- schedule = IceCube::Schedule.new(day.dtstart.to_time) do |s|
94
- day.rrule.each do |rule|
95
- s.add_recurrence_rule IceCube::Rule.from_ical(rule.value_ical)
96
- end
97
- day.rdate.each do |date|
98
- s.add_recurrence_time date.to_time
99
- end
100
- end
101
- [schedule.previous_occurrence(local.to_time), day]
116
+ [day.previous_occurrence(local.to_time), day]
102
117
  end
103
118
  possible.sort_by(&:first).last
104
119
  end
@@ -61,7 +61,7 @@ module Icalendar
61
61
  end
62
62
 
63
63
  def rrule
64
- start = local_start.to_datetime
64
+ start = (respond_to?(:local_start_at) ? local_start_at : local_start).to_datetime
65
65
  # this is somewhat of a hack, but seems to work ok
66
66
  # assumes that no timezone transition is in law as "4th X of the month"
67
67
  # but only as 1st X, 2nd X, 3rd X, or Last X
@@ -76,7 +76,7 @@ module Icalendar
76
76
  end
77
77
 
78
78
  def dtstart
79
- local_start.to_datetime.strftime '%Y%m%dT%H%M%S'
79
+ (respond_to?(:local_start_at) ? local_start_at : local_start).to_datetime.strftime '%Y%m%dT%H%M%S'
80
80
  end
81
81
  end
82
82
 
@@ -37,8 +37,7 @@ module Icalendar
37
37
  end
38
38
 
39
39
  def parse_fields(value)
40
- value.gsub!(/\s+/, '')
41
- md = /\A(?<behind>[+-])(?<hours>\d{2})(?<minutes>\d{2})(?<seconds>\d{2})?\z/.match value
40
+ md = /\A(?<behind>[+-])(?<hours>\d{2})(?<minutes>\d{2})(?<seconds>\d{2})?\z/.match value.gsub(/\s+/, '')
42
41
  {
43
42
  behind: (md[:behind] == '-'),
44
43
  hours: md[:hours].to_i,
@@ -1,5 +1,5 @@
1
1
  module Icalendar
2
2
 
3
- VERSION = '2.5.2'
3
+ VERSION = '2.7.1'
4
4
 
5
5
  end
@@ -2,6 +2,12 @@ require 'spec_helper'
2
2
 
3
3
  describe Icalendar::Calendar do
4
4
 
5
+ context 'marshalling' do
6
+ it 'can be de/serialized' do
7
+ Marshal.load(Marshal.dump(subject))
8
+ end
9
+ end
10
+
5
11
  context 'values' do
6
12
  let(:property) { 'my-value' }
7
13
 
@@ -164,4 +170,13 @@ END:VCALENDAR
164
170
  expect(subject.ip_method).to eq 'PUBLISH'
165
171
  end
166
172
  end
173
+
174
+ describe '.parse' do
175
+ let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'bad_wrapping.ics') }
176
+
177
+ it 'correctly parses a bad file' do
178
+ actual = described_class.parse(source)
179
+ expect(actual[0]).to be_a(described_class)
180
+ end
181
+ end
167
182
  end
data/spec/event_spec.rb CHANGED
@@ -113,6 +113,13 @@ describe Icalendar::Event do
113
113
  end
114
114
  end
115
115
 
116
+ describe "#custom_property" do
117
+ it "returns a default for missing properties" do
118
+ expect(subject.x_doesnt_exist).to eq([])
119
+ expect(subject.custom_property "x_doesnt_exist").to eq([])
120
+ end
121
+ end
122
+
116
123
  describe '.parse' do
117
124
  let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', fn) }
118
125
  let(:fn) { 'event.ics' }
@@ -0,0 +1,14 @@
1
+ BEGIN:VCALENDAR
2
+ VERSION:2.0
3
+ PRODID:manual
4
+ CALSCALE:GREGORIAN
5
+ BEGIN:VEVENT
6
+ DTSTAMP:20200902T223352Z
7
+ UID:6e7d7fe5-6735-4cdd-bfe4-761dfcecd7a7
8
+ DTSTART;VALUE=DATE:20200902
9
+ DTEND;VALUE=DATE:20200903
10
+ DESCRIPTION:Event description that puts a UTF-8 multi-octet sequence right�
11
+ �here.
12
+ SUMMARY:UTF-8 multi-octet sequence test
13
+ END:VEVENT
14
+ END:VCALENDAR
@@ -0,0 +1,158 @@
1
+ BEGIN:VCALENDAR
2
+ PRODID:-//Atlassian Confluence//Calendar Plugin 1.0//EN
3
+ VERSION:2.0
4
+ CALSCALE:GREGORIAN
5
+ X-WR-CALNAME:Grimsell testkalender
6
+ X-WR-CALDESC:
7
+ X-WR-TIMEZONE:Europe/Stockholm
8
+ X-MIGRATED-FOR-USER-KEY:true
9
+ METHOD:PUBLISH
10
+ BEGIN:VTIMEZONE
11
+ TZID:Europe/Stockholm
12
+ TZURL:http://tzurl.org/zoneinfo/Europe/Stockholm
13
+ SEQUENCE:9206
14
+ BEGIN:DAYLIGHT
15
+ TZOFFSETFROM:+0100
16
+ TZOFFSETTO:+0200
17
+ TZNAME:CEST
18
+ DTSTART:19810329T020000
19
+ RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
20
+ END:DAYLIGHT
21
+ BEGIN:STANDARD
22
+ TZOFFSETFROM:+0200
23
+ TZOFFSETTO:+0100
24
+ TZNAME:CET
25
+ DTSTART:19961027T030000
26
+ RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
27
+ END:STANDARD
28
+ BEGIN:STANDARD
29
+ TZOFFSETFROM:+011212
30
+ TZOFFSETTO:+010014
31
+ TZNAME:SET
32
+ DTSTART:18790101T000000
33
+ RDATE:18790101T000000
34
+ END:STANDARD
35
+ BEGIN:STANDARD
36
+ TZOFFSETFROM:+010014
37
+ TZOFFSETTO:+0100
38
+ TZNAME:CET
39
+ DTSTART:19000101T000000
40
+ RDATE:19000101T000000
41
+ END:STANDARD
42
+ BEGIN:DAYLIGHT
43
+ TZOFFSETFROM:+0100
44
+ TZOFFSETTO:+0200
45
+ TZNAME:CEST
46
+ DTSTART:19160514T230000
47
+ RDATE:19160514T230000
48
+ RDATE:19800406T020000
49
+ END:DAYLIGHT
50
+ BEGIN:STANDARD
51
+ TZOFFSETFROM:+0200
52
+ TZOFFSETTO:+0100
53
+ TZNAME:CET
54
+ DTSTART:19161001T010000
55
+ RDATE:19161001T010000
56
+ RDATE:19800928T030000
57
+ RDATE:19810927T030000
58
+ RDATE:19820926T030000
59
+ RDATE:19830925T030000
60
+ RDATE:19840930T030000
61
+ RDATE:19850929T030000
62
+ RDATE:19860928T030000
63
+ RDATE:19870927T030000
64
+ RDATE:19880925T030000
65
+ RDATE:19890924T030000
66
+ RDATE:19900930T030000
67
+ RDATE:19910929T030000
68
+ RDATE:19920927T030000
69
+ RDATE:19930926T030000
70
+ RDATE:19940925T030000
71
+ RDATE:19950924T030000
72
+ END:STANDARD
73
+ BEGIN:STANDARD
74
+ TZOFFSETFROM:+0100
75
+ TZOFFSETTO:+0100
76
+ TZNAME:CET
77
+ DTSTART:19800101T000000
78
+ RDATE:19800101T000000
79
+ END:STANDARD
80
+ END:VTIMEZONE
81
+ BEGIN:VEVENT
82
+ DTSTAMP:20151203T104347Z
83
+ SUMMARY:Kaffepaus
84
+ UID:20151203T101856Z-1543254179@intranet.idainfront.se
85
+ DESCRIPTION:
86
+ ORGANIZER;X-CONFLUENCE-USER-KEY=ff80818141e4d3b60141e4d4b75700a2;CN=Magnu
87
+ s Grimsell;CUTYPE=INDIVIDUAL:mailto:magnus.grimsell@idainfront.se
88
+ CREATED:20151203T101856Z
89
+ LAST-MODIFIED:20151203T101856Z
90
+ SEQUENCE:1
91
+ X-CONFLUENCE-SUBCALENDAR-TYPE:other
92
+ TRANSP:OPAQUE
93
+ STATUS:CONFIRMED
94
+ DTSTART:20151203T090000Z
95
+ DTEND:20151203T091500Z
96
+ END:VEVENT
97
+ BEGIN:X-EVENT-SERIES
98
+ SUMMARY:iipax - Sprints
99
+ DESCRIPTION:
100
+ X-CONFLUENCE-SUBCALENDAR-TYPE:jira-agile-sprint
101
+ URL:jira://8112ef9a-343e-30e6-b469-4f993bf0371d?projectKey=II&dateFieldNa
102
+ me=sprint
103
+ END:X-EVENT-SERIES
104
+ BEGIN:VEVENT
105
+ DTSTAMP:20151203T104348Z
106
+ DTSTART;TZID=Europe/Stockholm:20130115T170800
107
+ DTEND;TZID=Europe/Stockholm:20130204T170800
108
+ UID:3cb4df4b-eb18-43fd-934a-16c15acfb4b1-3@jira.idainfront.se
109
+ X-GREENHOPPER-SPRINT-CLOSED:false
110
+ X-GREENHOPPER-SPRINT-EDITABLE:true
111
+ X-JIRA-PROJECT;X-JIRA-PROJECT-KEY=II:iipax
112
+ X-GREENHOPPER-SPRINT-VIEWBOARDS-URL:https://jira.idainfront.se/secure/GHG
113
+ oToBoard.jspa?sprintId=3
114
+ SEQUENCE:0
115
+ X-CONFLUENCE-SUBCALENDAR-TYPE:jira-agile-sprint
116
+ TRANSP:OPAQUE
117
+ STATUS:CONFIRMED
118
+ DESCRIPTION:https://jira.idainfront.se/secure/GHGoToBoard.jspa?sprintId=3
119
+
120
+ SUMMARY:iipax - Callisto-6
121
+ END:VEVENT
122
+ BEGIN:VEVENT
123
+ DTSTAMP:20151203T104348Z
124
+ DTSTART;TZID=Europe/Stockholm:20130219T080000
125
+ DTEND;TZID=Europe/Stockholm:20130319T095900
126
+ UID:3cb4df4b-eb18-43fd-934a-16c15acfb4b1-5@jira.idainfront.se
127
+ X-GREENHOPPER-SPRINT-CLOSED:false
128
+ X-GREENHOPPER-SPRINT-EDITABLE:true
129
+ X-JIRA-PROJECT;X-JIRA-PROJECT-KEY=II:iipax
130
+ X-GREENHOPPER-SPRINT-VIEWBOARDS-URL:https://jira.idainfront.se/secure/GHG
131
+ oToBoard.jspa?sprintId=5
132
+ SEQUENCE:0
133
+ X-CONFLUENCE-SUBCALENDAR-TYPE:jira-agile-sprint
134
+ TRANSP:OPAQUE
135
+ STATUS:CONFIRMED
136
+ DESCRIPTION:https://jira.idainfront.se/secure/GHGoToBoard.jspa?sprintId=5
137
+
138
+ SUMMARY:iipax - Bulbasaur
139
+ END:VEVENT
140
+ BEGIN:VEVENT
141
+ DTSTAMP:20151203T104348Z
142
+ DTSTART;TZID=Europe/Stockholm:20130326T105900
143
+ DTEND;TZID=Europe/Stockholm:20130419T105900
144
+ UID:3cb4df4b-eb18-43fd-934a-16c15acfb4b1-7@jira.idainfront.se
145
+ X-GREENHOPPER-SPRINT-CLOSED:true
146
+ X-GREENHOPPER-SPRINT-EDITABLE:true
147
+ X-JIRA-PROJECT;X-JIRA-PROJECT-KEY=II:iipax
148
+ X-GREENHOPPER-SPRINT-VIEWBOARDS-URL:https://jira.idainfront.se/secure/GHG
149
+ oToBoard.jspa?sprintId=7
150
+ SEQUENCE:0
151
+ X-CONFLUENCE-SUBCALENDAR-TYPE:jira-agile-sprint
152
+ TRANSP:OPAQUE
153
+ STATUS:CONFIRMED
154
+ DESCRIPTION:https://jira.idainfront.se/secure/GHGoToBoard.jspa?sprintId=7
155
+
156
+ SUMMARY:iipax - Ivysaur
157
+ END:VEVENT
158
+ END:VCALENDAR
data/spec/parser_spec.rb CHANGED
@@ -66,6 +66,16 @@ describe Icalendar::Parser do
66
66
  expect(event.dtstart.utc).to eq Time.parse("20180104T150000Z")
67
67
  end
68
68
  end
69
+ context 'custom_component.ics' do
70
+ let(:fn) { 'custom_component.ics' }
71
+
72
+ it 'correctly handles custom named components' do
73
+ parsed = subject.parse
74
+ calendar = parsed.first
75
+ expect(calendar.custom_component('x_event_series').size).to eq 1
76
+ expect(calendar.custom_component('X-EVENT-SERIES').size).to eq 1
77
+ end
78
+ end
69
79
  end
70
80
 
71
81
  describe '#parse with bad line' do
@@ -135,6 +135,15 @@ describe Icalendar do
135
135
  ical = subject.parse.first.to_ical
136
136
  expect(ical).to include 'CUSTOMFIELD:Not properly noted as custom with X- prefix.'
137
137
  end
138
+
139
+ context 'custom components' do
140
+ let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'custom_component.ics') }
141
+
142
+ it 'can output the custom component' do
143
+ ical = subject.parse.first.to_ical
144
+ expect(ical).to include 'BEGIN:X-EVENT-SERIES'
145
+ end
146
+ end
138
147
  end
139
148
  end
140
149
  end
@@ -28,4 +28,52 @@ describe Icalendar::Timezone do
28
28
  it { should_not be_valid }
29
29
  end
30
30
  end
31
+
32
+ context 'marshalling' do
33
+ context 'with standard/daylight components' do
34
+ before do
35
+ subject.standard do |standard|
36
+ standard.rrule = Icalendar::Values::Recur.new("FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10")
37
+ standard.dtstart = Icalendar::Values::DateTime.new("16010101T030000")
38
+ standard.tzoffsetfrom = Icalendar::Values::UtcOffset.new("+0200")
39
+ standard.tzoffsetto = Icalendar::Values::UtcOffset.new("+0100")
40
+ end
41
+
42
+ subject.daylight do |daylight|
43
+ daylight.rrule = Icalendar::Values::Recur.new("FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3")
44
+ daylight.dtstart = Icalendar::Values::DateTime.new("16010101T020000")
45
+ daylight.tzoffsetfrom = Icalendar::Values::UtcOffset.new("+0100")
46
+ daylight.tzoffsetto = Icalendar::Values::UtcOffset.new("+0200")
47
+ end
48
+ end
49
+
50
+ it 'can be de/serialized' do
51
+ first_standard = subject.standards.first
52
+ first_daylight = subject.daylights.first
53
+
54
+ expect(first_standard.valid?).to be_truthy
55
+ expect(first_daylight.valid?).to be_truthy
56
+
57
+ # calling previous_occurrence intializes @cached_occurrences with a time that's not handled by ruby marshaller
58
+ first_occurence_for = Time.new(1601, 10, 31)
59
+
60
+ standard_previous_occurrence = first_standard.previous_occurrence(first_occurence_for)
61
+ expect(standard_previous_occurrence).not_to be_nil
62
+
63
+ daylight_previous_occurrence = first_daylight.previous_occurrence(first_occurence_for)
64
+ expect(daylight_previous_occurrence).not_to be_nil
65
+
66
+ deserialized = nil
67
+
68
+ expect { deserialized = Marshal.load(Marshal.dump(subject)) }.not_to raise_exception
69
+
70
+ expect(deserialized.standards.first.previous_occurrence(first_occurence_for)).to eq(standard_previous_occurrence)
71
+ expect(deserialized.daylights.first.previous_occurrence(first_occurence_for)).to eq(daylight_previous_occurrence)
72
+ end
73
+ end
74
+
75
+ it 'can be de/serialized' do
76
+ expect { Marshal.load(Marshal.dump(subject)) }.not_to raise_exception
77
+ end
78
+ end
31
79
  end
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.5.2
4
+ version: 2.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Ahearn
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-12-08 00:00:00.000000000 Z
11
+ date: 2021-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ice_cube
@@ -30,56 +30,56 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '12.0'
33
+ version: '13.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '12.0'
40
+ version: '13.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '1.16'
47
+ version: '2.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '1.16'
54
+ version: '2.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: activesupport
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '5.2'
61
+ version: '6.0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '5.2'
68
+ version: '6.0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: i18n
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '1.1'
75
+ version: '1.8'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '1.1'
82
+ version: '1.8'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: tzinfo
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -100,14 +100,14 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '1.2018'
103
+ version: '1.2020'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '1.2018'
110
+ version: '1.2020'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: timecop
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -180,6 +180,7 @@ files:
180
180
  - lib/icalendar/has_properties.rb
181
181
  - lib/icalendar/journal.rb
182
182
  - lib/icalendar/logger.rb
183
+ - lib/icalendar/marshable.rb
183
184
  - lib/icalendar/parser.rb
184
185
  - lib/icalendar/timezone.rb
185
186
  - lib/icalendar/timezone_store.rb
@@ -209,6 +210,8 @@ files:
209
210
  - spec/calendar_spec.rb
210
211
  - spec/downcased_hash_spec.rb
211
212
  - spec/event_spec.rb
213
+ - spec/fixtures/bad_wrapping.ics
214
+ - spec/fixtures/custom_component.ics
212
215
  - spec/fixtures/event.ics
213
216
  - spec/fixtures/nondefault_values.ics
214
217
  - spec/fixtures/nonstandard.ics
@@ -245,7 +248,7 @@ metadata: {}
245
248
  post_install_message: 'ActiveSupport is required for TimeWithZone support, but not
246
249
  required for general use.
247
250
 
248
- '
251
+ '
249
252
  rdoc_options: []
250
253
  require_paths:
251
254
  - lib
@@ -253,16 +256,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
253
256
  requirements:
254
257
  - - ">="
255
258
  - !ruby/object:Gem::Version
256
- version: 2.2.0
259
+ version: 2.4.0
257
260
  required_rubygems_version: !ruby/object:Gem::Requirement
258
261
  requirements:
259
262
  - - ">="
260
263
  - !ruby/object:Gem::Version
261
264
  version: '0'
262
265
  requirements: []
263
- rubyforge_project:
264
- rubygems_version: 2.7.6
265
- signing_key:
266
+ rubygems_version: 3.2.11
267
+ signing_key:
266
268
  specification_version: 4
267
269
  summary: A ruby implementation of the iCalendar specification (RFC-5545).
268
270
  test_files:
@@ -270,6 +272,8 @@ test_files:
270
272
  - spec/calendar_spec.rb
271
273
  - spec/downcased_hash_spec.rb
272
274
  - spec/event_spec.rb
275
+ - spec/fixtures/bad_wrapping.ics
276
+ - spec/fixtures/custom_component.ics
273
277
  - spec/fixtures/event.ics
274
278
  - spec/fixtures/nondefault_values.ics
275
279
  - spec/fixtures/nonstandard.ics