icalendar2 0.1.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.
- data/CHANGELOG.md +2 -0
- data/README.md +89 -0
- data/lib/icalendar2/calendar.rb +85 -0
- data/lib/icalendar2/calendar_property/calscale.rb +9 -0
- data/lib/icalendar2/calendar_property/method.rb +9 -0
- data/lib/icalendar2/calendar_property/prodid.rb +9 -0
- data/lib/icalendar2/calendar_property/version.rb +9 -0
- data/lib/icalendar2/calendar_property.rb +12 -0
- data/lib/icalendar2/component/base.rb +90 -0
- data/lib/icalendar2/component/event.rb +32 -0
- data/lib/icalendar2/component.rb +8 -0
- data/lib/icalendar2/parameter/altrep.rb +8 -0
- data/lib/icalendar2/parameter/base.rb +34 -0
- data/lib/icalendar2/parameter/cn.rb +8 -0
- data/lib/icalendar2/parameter/cutype.rb +8 -0
- data/lib/icalendar2/parameter/delegated_from.rb +8 -0
- data/lib/icalendar2/parameter/delegated_to.rb +8 -0
- data/lib/icalendar2/parameter/dir.rb +8 -0
- data/lib/icalendar2/parameter/encoding.rb +8 -0
- data/lib/icalendar2/parameter/fmttype.rb +10 -0
- data/lib/icalendar2/parameter.rb +4 -0
- data/lib/icalendar2/parser.rb +136 -0
- data/lib/icalendar2/property/attach.rb +10 -0
- data/lib/icalendar2/property/attendee.rb +8 -0
- data/lib/icalendar2/property/base.rb +80 -0
- data/lib/icalendar2/property/categories.rb +10 -0
- data/lib/icalendar2/property/class.rb +9 -0
- data/lib/icalendar2/property/comment.rb +8 -0
- data/lib/icalendar2/property/contact.rb +8 -0
- data/lib/icalendar2/property/description.rb +9 -0
- data/lib/icalendar2/property/dtend.rb +9 -0
- data/lib/icalendar2/property/dtstamp.rb +9 -0
- data/lib/icalendar2/property/dtstart.rb +9 -0
- data/lib/icalendar2/property/exdate.rb +10 -0
- data/lib/icalendar2/property/geo.rb +9 -0
- data/lib/icalendar2/property/last_mod.rb +9 -0
- data/lib/icalendar2/property/location.rb +9 -0
- data/lib/icalendar2/property/nil.rb +13 -0
- data/lib/icalendar2/property/organizer.rb +9 -0
- data/lib/icalendar2/property/priority.rb +9 -0
- data/lib/icalendar2/property/rdate.rb +8 -0
- data/lib/icalendar2/property/related_to.rb +8 -0
- data/lib/icalendar2/property/resources.rb +8 -0
- data/lib/icalendar2/property/rrule.rb +10 -0
- data/lib/icalendar2/property/rstatus.rb +8 -0
- data/lib/icalendar2/property/sequence.rb +9 -0
- data/lib/icalendar2/property/summary.rb +9 -0
- data/lib/icalendar2/property/uid.rb +9 -0
- data/lib/icalendar2/property.rb +9 -0
- data/lib/icalendar2/tokens.rb +32 -0
- data/lib/icalendar2/value/binary_value.rb +8 -0
- data/lib/icalendar2/value/boolean_value.rb +6 -0
- data/lib/icalendar2/value/cal_address_value.rb +6 -0
- data/lib/icalendar2/value/date_time_value.rb +14 -0
- data/lib/icalendar2/value/date_value.rb +14 -0
- data/lib/icalendar2/value/duration_value.rb +14 -0
- data/lib/icalendar2/value/float_value.rb +7 -0
- data/lib/icalendar2/value/integer_value.rb +6 -0
- data/lib/icalendar2/value/lat_lon_value.rb +6 -0
- data/lib/icalendar2/value/period_value.rb +8 -0
- data/lib/icalendar2/value/recur_value.rb +39 -0
- data/lib/icalendar2/value/text_value.rb +17 -0
- data/lib/icalendar2/value/time_value.rb +6 -0
- data/lib/icalendar2/value/uri_value.rb +6 -0
- data/lib/icalendar2/value.rb +45 -0
- data/lib/icalendar2/version.rb +3 -0
- data/lib/icalendar2.rb +74 -0
- metadata +131 -0
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# icalendar2 - generate, consume and validate iCalendar feeds
|
2
|
+
|
3
|
+
## Warning
|
4
|
+
|
5
|
+
This is NOT production ready yet. It has not been extensively tested, and you
|
6
|
+
use it at your own risk.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
If you want to jump right in and try it out, here's how you install it:
|
11
|
+
|
12
|
+
``` bash
|
13
|
+
gem install icalendar2
|
14
|
+
```
|
15
|
+
|
16
|
+
Website: https://github.com/ericcf/icalendar2
|
17
|
+
|
18
|
+
## Introduction
|
19
|
+
|
20
|
+
icalendar2 is a Ruby library for parsing, manipulating and generating iCalendar
|
21
|
+
objects as described in RFC 5545. Its API is intended to be mostly compatible
|
22
|
+
with that of the icalendar gem (https://github.com/sdague/icalendar), at least
|
23
|
+
initially.
|
24
|
+
|
25
|
+
For example:
|
26
|
+
|
27
|
+
``` ruby
|
28
|
+
require 'rubygems'
|
29
|
+
require 'icalendar2'
|
30
|
+
include Icalendar2
|
31
|
+
|
32
|
+
calendar = Calendar.new
|
33
|
+
calendar.event do
|
34
|
+
dtstart Date.new(2012, 12, 25)
|
35
|
+
dtend Date.new(2012, 1, 5)
|
36
|
+
summary "12 days of Christmas."
|
37
|
+
description "Eat lots of cookies."
|
38
|
+
end
|
39
|
+
calendar.valid? # true
|
40
|
+
puts calendar.to_ical
|
41
|
+
# BEGIN:VCALENDAR
|
42
|
+
# BEGIN:VEVENT
|
43
|
+
# UID:2012-12-12T10:12:45-06:00_253060006@example.com
|
44
|
+
# DTSTAMP:20121212T101245
|
45
|
+
# DTSTART:20121225
|
46
|
+
# DTEND:20120105
|
47
|
+
# SUMMARY:12 days of Christmas.
|
48
|
+
# DESCRIPTION:Eat lots of cookies.
|
49
|
+
# END:VEVENT
|
50
|
+
# END:VCALENDAR
|
51
|
+
|
52
|
+
calendars = Parser.new(calendar.to_ical).parse
|
53
|
+
calendars.size # 1
|
54
|
+
calendars.first.valid? # true
|
55
|
+
```
|
56
|
+
|
57
|
+
## Improvements over/differences with icalendar gem
|
58
|
+
|
59
|
+
While this gem is based on (and borrows some code from) the icalendar gem, it
|
60
|
+
also improves on it in several key areas:
|
61
|
+
* No monkey patching of core Ruby classes such as String for conversions
|
62
|
+
* Better separation of concerns makes it easier to test and extend
|
63
|
+
* Validates calendars and pinpoints errors
|
64
|
+
* Uses RSpec for tests
|
65
|
+
|
66
|
+
## Testing
|
67
|
+
|
68
|
+
To run the icalendar2 tests ensure that the rspec gem is installed, and run:
|
69
|
+
|
70
|
+
``` bash
|
71
|
+
rake test
|
72
|
+
```
|
73
|
+
|
74
|
+
## Building the gem
|
75
|
+
|
76
|
+
``` bash
|
77
|
+
gem build icalendar2.gemspec
|
78
|
+
```
|
79
|
+
|
80
|
+
## Contributions
|
81
|
+
|
82
|
+
I need your help to make this library better. Please use the GitHub issue
|
83
|
+
tracker for feature requests and bug reports. The more detail you can provide
|
84
|
+
the better.
|
85
|
+
|
86
|
+
## License
|
87
|
+
|
88
|
+
As this is based on the icalendar gem, which employs the Ruby licence, this too
|
89
|
+
falls under that license: http://www.ruby-lang.org/en/about/license.txt
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Icalendar2
|
2
|
+
# See http://tools.ietf.org/html/rfc5545#section-3.4
|
3
|
+
class Calendar
|
4
|
+
VALUE = "VCALENDAR"
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@components = {
|
8
|
+
Event::VALUE => []
|
9
|
+
}
|
10
|
+
@properties = {
|
11
|
+
:calscale => Property::Nil.new,
|
12
|
+
:method => Property::Nil.new,
|
13
|
+
:prodid => Property::Nil.new,
|
14
|
+
:version => Property::Nil.new
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def calscale(value = nil)
|
19
|
+
set_property(:calscale, value)
|
20
|
+
end
|
21
|
+
|
22
|
+
def method_property(value = nil)
|
23
|
+
set_property(:method, value)
|
24
|
+
end
|
25
|
+
|
26
|
+
def prodid(value = nil)
|
27
|
+
set_property(:prodid, value)
|
28
|
+
end
|
29
|
+
|
30
|
+
def version(value = nil)
|
31
|
+
set_property(:version, value)
|
32
|
+
end
|
33
|
+
|
34
|
+
def set_property(property_name, value, parameters = {})
|
35
|
+
property = property_name.downcase.to_sym
|
36
|
+
if value.nil?
|
37
|
+
@properties[property].value.to_s
|
38
|
+
elsif (factory = CalendarProperty.get_factory(property_name))
|
39
|
+
if value.is_a? factory
|
40
|
+
@properties[property] = value
|
41
|
+
else
|
42
|
+
@properties[property] = factory.new(value)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def events
|
48
|
+
@components[Event::VALUE]
|
49
|
+
end
|
50
|
+
|
51
|
+
def event(&block)
|
52
|
+
e = Event.new
|
53
|
+
e.instance_eval(&block)
|
54
|
+
events << e
|
55
|
+
|
56
|
+
e
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_event(event)
|
60
|
+
events << event
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_ical
|
64
|
+
str = "#{Tokens::COMPONENT_BEGIN}:#{VALUE}#{Tokens::CRLF}"
|
65
|
+
str << body_to_ical
|
66
|
+
str << "#{Tokens::COMPONENT_END}:#{VALUE}#{Tokens::CRLF}"
|
67
|
+
str.encode("UTF-8")
|
68
|
+
end
|
69
|
+
|
70
|
+
def add_component(component)
|
71
|
+
@components[component.class::VALUE] << component
|
72
|
+
end
|
73
|
+
|
74
|
+
def valid?
|
75
|
+
events.all?(&:valid?)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def body_to_ical
|
81
|
+
str = @properties.values.map(&:to_ical).join
|
82
|
+
str << events.map(&:to_ical).join
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Icalendar2
|
2
|
+
module Component
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
class Base
|
6
|
+
|
7
|
+
class << self; attr_reader :property_names end
|
8
|
+
class << self; attr_reader :required_property_names end
|
9
|
+
|
10
|
+
def self.requires(properties = {})
|
11
|
+
add_properties(properties)
|
12
|
+
@required_property_names = properties.values.flatten
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.accepts(properties = {})
|
16
|
+
add_properties(properties)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@properties = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def new_uid
|
24
|
+
"#{DateTime.now}_#{rand(999999999)}@#{Socket.gethostname}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def new_timestamp
|
28
|
+
DateTime.now
|
29
|
+
end
|
30
|
+
|
31
|
+
def valid?
|
32
|
+
present_property_names = @properties.keys
|
33
|
+
@properties.values.flatten.all?(&:valid?) &&
|
34
|
+
self.class.required_property_names.all? { |p| present_property_names.include?(p) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_property(property_name, value, parameters = {})
|
38
|
+
name = (property_name == "class" ? "klass" : property_name)
|
39
|
+
unless self.class.property_names.include? name.to_sym
|
40
|
+
raise "#{self.class} component property #{name} not defined"
|
41
|
+
end
|
42
|
+
self.send(name, value, parameters)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def self.add_properties(properties)
|
48
|
+
single_properties = properties[:exactly_one] || []
|
49
|
+
single_properties.each do |property_sym|
|
50
|
+
define_accessor(property_sym)
|
51
|
+
end
|
52
|
+
multiple_properties = properties[:one_or_more] || []
|
53
|
+
multiple_properties.each do |property_sym|
|
54
|
+
define_multiple_accessor(property_sym)
|
55
|
+
end
|
56
|
+
@property_names ||= []
|
57
|
+
@property_names += single_properties + multiple_properties
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.define_accessor(property_sym)
|
61
|
+
define_method(property_sym) do |*args|
|
62
|
+
value, parameters = *args
|
63
|
+
if value.nil?
|
64
|
+
@properties[property_sym]
|
65
|
+
else
|
66
|
+
@properties[property_sym] = Property.get_factory(property_sym).new(value, parameters)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
alias_method "#{property_sym}=", property_sym
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.define_multiple_accessor(property_sym)
|
73
|
+
property_factory = Property.get_factory(property_sym)
|
74
|
+
define_method(property_factory::PLURAL) do
|
75
|
+
@properties[property_sym]
|
76
|
+
end
|
77
|
+
define_method(property_sym) do |*args|
|
78
|
+
value, parameters = *args
|
79
|
+
@properties[property_sym] ||= []
|
80
|
+
@properties[property_sym] << property_factory.new(value, parameters)
|
81
|
+
end
|
82
|
+
alias_method "#{property_sym}=", property_sym
|
83
|
+
end
|
84
|
+
|
85
|
+
def properties_to_ical
|
86
|
+
@properties.values.flatten.map(&:to_ical).join
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Icalendar2
|
2
|
+
# See http://tools.ietf.org/html/rfc5545#section-3.6.1
|
3
|
+
class Event < Component::Base
|
4
|
+
VALUE = "VEVENT"
|
5
|
+
|
6
|
+
requires :exactly_one => [:dtstamp, :uid]
|
7
|
+
accepts :exactly_one => [:dtstart, :klass, :created, :description, :geo,
|
8
|
+
:last_mod, :location, :organizer, :priority, :sequence, :status, :summary,
|
9
|
+
:transp, :url, :recurid, :dtend, :duration],
|
10
|
+
:one_or_more => [:rrule, :attach, :attendee, :categories, :comment,
|
11
|
+
:contact, :exdate, :rstatus, :related_to, :resources, :rdate]
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
super
|
15
|
+
self.uid = new_uid
|
16
|
+
self.dtstamp = new_timestamp
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_ical
|
20
|
+
str = "#{Tokens::COMPONENT_BEGIN}:#{VALUE}#{Tokens::CRLF}"
|
21
|
+
str << properties_to_ical
|
22
|
+
#str << alarm_to_ical
|
23
|
+
str << "#{Tokens::COMPONENT_END}:#{VALUE}#{Tokens::CRLF}"
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def alarm_to_ical
|
29
|
+
#alarm.print if alarm
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Icalendar2
|
2
|
+
module Parameter
|
3
|
+
class Base
|
4
|
+
|
5
|
+
class << self; attr_reader :format_matcher end
|
6
|
+
|
7
|
+
def self.format(matcher)
|
8
|
+
@format_matcher = matcher
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(value)
|
12
|
+
@valid = true
|
13
|
+
@value = value
|
14
|
+
validate
|
15
|
+
end
|
16
|
+
|
17
|
+
def valid?
|
18
|
+
@valid
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def validate
|
24
|
+
unless self.class.format_matcher.nil?
|
25
|
+
if @value.respond_to?(:to_s) && (val = @value.to_s).respond_to?(:match)
|
26
|
+
@valid = !!val.match(self.class.format_matcher)
|
27
|
+
else
|
28
|
+
@valid = false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Icalendar2
|
2
|
+
module Parameter
|
3
|
+
# See http://tools.ietf.org/html/rfc5545#section-3.2.8
|
4
|
+
class Fmttype < Base
|
5
|
+
TYPE_NAME = "(?:#{Tokens::ALPHA}|#{Tokens::DIGIT}|[-!#$&.+^_])+"
|
6
|
+
SUBTYPE_NAME = TYPE_NAME
|
7
|
+
format /^FMTTYPE=#{TYPE_NAME}\/#{SUBTYPE_NAME}$/
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module Icalendar2
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
class Parser
|
5
|
+
|
6
|
+
def initialize(source)
|
7
|
+
if source.respond_to?(:gets)
|
8
|
+
@file = source
|
9
|
+
elsif !source.nil? && source.respond_to?(:to_s)
|
10
|
+
@file = StringIO.new(source.to_s, 'r')
|
11
|
+
else
|
12
|
+
raise ArgumentError, "#{self.class}.new must be called with an IO-like object or a path"
|
13
|
+
end
|
14
|
+
|
15
|
+
@previous_line = next_line_of_file
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns an iCalendar stream (collection of iCalendar objects).
|
19
|
+
def parse
|
20
|
+
calendars = []
|
21
|
+
|
22
|
+
while (line = next_multiline)
|
23
|
+
fields = parse_line(line)
|
24
|
+
if fields[:name] == Tokens::COMPONENT_BEGIN && fields[:value] == Calendar::VALUE
|
25
|
+
calendar = parse_calendar
|
26
|
+
calendars << calendar
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
calendars
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def parse_calendar
|
36
|
+
calendar = Calendar.new
|
37
|
+
|
38
|
+
while (line = next_multiline)
|
39
|
+
fields = parse_line(line)
|
40
|
+
|
41
|
+
name, value = fields[:name].upcase, fields[:value]
|
42
|
+
if name == Tokens::COMPONENT_BEGIN
|
43
|
+
component_factory = Component.get_factory(value)
|
44
|
+
if component_factory.nil?
|
45
|
+
raise "Unable to find component: #{value}"
|
46
|
+
end
|
47
|
+
calendar.add_component parse_component(component_factory.new)
|
48
|
+
else
|
49
|
+
calendar.set_property(name, value, fields[:params])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
calendar
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse_component(component)
|
57
|
+
while (line = next_multiline)
|
58
|
+
fields = parse_line(line)
|
59
|
+
name = fields[:name].downcase
|
60
|
+
break if name.upcase == Tokens::COMPONENT_END
|
61
|
+
params, value = fields[:params], fields[:value]
|
62
|
+
component.set_property(name, value, params)
|
63
|
+
end
|
64
|
+
|
65
|
+
component
|
66
|
+
end
|
67
|
+
|
68
|
+
CONTINUED_LINE = /^[ \t]/
|
69
|
+
NAME = "(?:#{Tokens::IANA_TOKEN}|#{Tokens::X_NAME})"
|
70
|
+
PARAM_NAME = "(?:#{Tokens::IANA_TOKEN}|#{Tokens::X_NAME})"
|
71
|
+
PARAM = "(?:#{PARAM_NAME}=#{Tokens::PARAM_VALUE}(?:,#{Tokens::PARAM_VALUE})*)"
|
72
|
+
PARAM_CAPTURE = "(#{PARAM_NAME})=(#{Tokens::PARAM_VALUE}(?:,#{Tokens::PARAM_VALUE})*)"
|
73
|
+
CONTENT_LINE = /^(#{NAME})(;#{PARAM})*:(#{Tokens::VALUE_CHAR}*)/
|
74
|
+
|
75
|
+
def next_line_of_file
|
76
|
+
line = @file.gets
|
77
|
+
line.chomp! unless line.nil?
|
78
|
+
end
|
79
|
+
|
80
|
+
def next_multiline
|
81
|
+
line = @previous_line
|
82
|
+
|
83
|
+
if line.nil?
|
84
|
+
return nil
|
85
|
+
end
|
86
|
+
|
87
|
+
loop do
|
88
|
+
next_line = next_line_of_file
|
89
|
+
|
90
|
+
if next_line =~ CONTINUED_LINE
|
91
|
+
line << next_line[1, next_line.size]
|
92
|
+
elsif next_line.nil? || next_line.size > 0
|
93
|
+
@previous_line = next_line
|
94
|
+
break
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
line
|
99
|
+
end
|
100
|
+
|
101
|
+
def parse_line(line)
|
102
|
+
unless line =~ %r{#{CONTENT_LINE}}i
|
103
|
+
raise "Invalid line in calendar string: #{line}"
|
104
|
+
end
|
105
|
+
|
106
|
+
name = $1.upcase
|
107
|
+
value = unescape($3)
|
108
|
+
params = parse_params_str($2)
|
109
|
+
|
110
|
+
{ :name => name, :value => value, :params => params }
|
111
|
+
end
|
112
|
+
|
113
|
+
def unescape(str)
|
114
|
+
str.gsub("\\;", ";").gsub("\\,", ",").gsub("\\n", "\n").gsub("\\\\", "\\")
|
115
|
+
end
|
116
|
+
|
117
|
+
def parse_params_str(params_str)
|
118
|
+
params = {}
|
119
|
+
if !params_str.nil? && params_str.size > 1
|
120
|
+
params_str.scan(/#{PARAM_CAPTURE}/) do
|
121
|
+
name = $1
|
122
|
+
values = $2
|
123
|
+
|
124
|
+
params[name] ||= []
|
125
|
+
values.scan(/(#{Tokens::PARAM_VALUE})/) do
|
126
|
+
if $1.size > 0
|
127
|
+
params[name] << $1
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
params
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|