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