selene 0.3.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -13
- data/README.md +13 -3
- data/Rakefile +0 -5
- data/lib/selene/alarm_builder.rb +1 -14
- data/lib/selene/calendar_builder.rb +3 -28
- data/lib/selene/component_builder.rb +46 -0
- data/lib/selene/component_rules.rb +38 -0
- data/lib/selene/daylight_savings_time_builder.rb +7 -14
- data/lib/selene/event_builder.rb +33 -17
- data/lib/selene/feed_builder.rb +13 -0
- data/lib/selene/line.rb +10 -7
- data/lib/selene/parser.rb +24 -24
- data/lib/selene/standard_time_builder.rb +1 -13
- data/lib/selene/time_zone_builder.rb +1 -21
- data/lib/selene/version.rb +1 -1
- data/test/fixtures/meetup.json +12 -3
- data/test/selene/alarm_builder_test.rb +5 -1
- data/test/selene/calendar_builder_test.rb +26 -10
- data/test/selene/daylight_savings_time_builder_test.rb +3 -2
- data/test/selene/event_builder_test.rb +50 -17
- data/test/selene/line_test.rb +6 -6
- data/test/selene/parser_test.rb +7 -5
- data/test/selene/time_zone_builder_test.rb +1 -1
- data/test/test_helper.rb +41 -17
- metadata +7 -5
- data/.rvmrc +0 -1
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# Selene
|
2
2
|
|
3
|
-
Selene is an iCalendar parser for Ruby. It takes a string in iCalendar format (RFC 5545) and outputs a hash.
|
4
|
-
|
5
|
-
![Selene](http://corykaufman.com/images/selene.png)
|
3
|
+
Selene is an iCalendar parser for Ruby. It takes a string in iCalendar format (RFC 5545) and outputs a serializable hash.
|
6
4
|
|
7
5
|
## Installation
|
8
6
|
|
@@ -24,6 +22,12 @@ Or install it yourself as:
|
|
24
22
|
ical = Selene.parse(File.read('calendar.ics'))
|
25
23
|
```
|
26
24
|
|
25
|
+
## Demo
|
26
|
+
|
27
|
+
I wrote a quick little ical to json Heroku app:
|
28
|
+
|
29
|
+
http://ical-to-json.herokuapp.com/
|
30
|
+
|
27
31
|
## Contributing
|
28
32
|
|
29
33
|
1. Fork it
|
@@ -31,3 +35,9 @@ ical = Selene.parse(File.read('calendar.ics'))
|
|
31
35
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
32
36
|
4. Push to the branch (`git push origin my-new-feature`)
|
33
37
|
5. Create new Pull Request
|
38
|
+
|
39
|
+
## Namesake
|
40
|
+
|
41
|
+
Selene is the Greek goddess of the moon, and also happens to be a vampire from the excellent film Underworld:
|
42
|
+
|
43
|
+
![Selene](http://corykaufman.com/images/selene.png)
|
data/Rakefile
CHANGED
@@ -11,11 +11,6 @@ Rake::TestTask.new do |t|
|
|
11
11
|
end
|
12
12
|
|
13
13
|
task :meetup do
|
14
|
-
require 'selene'
|
15
|
-
require 'json'
|
16
|
-
require 'pp'
|
17
|
-
require 'debugger'
|
18
|
-
|
19
14
|
File.open('test/fixtures/meetup.json', 'wb') do |file|
|
20
15
|
feed = Selene.parse(File.read('test/fixtures/meetup.ics'))
|
21
16
|
pp feed
|
data/lib/selene/alarm_builder.rb
CHANGED
@@ -1,17 +1,4 @@
|
|
1
1
|
module Selene
|
2
|
-
class AlarmBuilder
|
3
|
-
|
4
|
-
def initialize
|
5
|
-
@component = {}
|
6
|
-
end
|
7
|
-
|
8
|
-
def component
|
9
|
-
@component
|
10
|
-
end
|
11
|
-
|
12
|
-
def parse(line)
|
13
|
-
@component[line.name.downcase] = line.value
|
14
|
-
end
|
15
|
-
|
2
|
+
class AlarmBuilder < ComponentBuilder
|
16
3
|
end
|
17
4
|
end
|
@@ -1,34 +1,9 @@
|
|
1
1
|
module Selene
|
2
|
-
class CalendarBuilder
|
2
|
+
class CalendarBuilder < ComponentBuilder
|
3
3
|
|
4
|
-
|
5
|
-
@errors = []
|
6
|
-
@component = Hash.new { |component, property| component[property] = [] }
|
7
|
-
end
|
4
|
+
REQUIRED_PROPERTIES = %w(prodid version)
|
8
5
|
|
9
|
-
|
10
|
-
set_property line.name, line.value
|
11
|
-
end
|
6
|
+
DISTINCT_PROPERTIES = %w(prodid version calscale method)
|
12
7
|
|
13
|
-
def set_property(name, value)
|
14
|
-
@component[name.downcase] = value
|
15
|
-
end
|
16
|
-
|
17
|
-
def name(line)
|
18
|
-
line.name.downcase
|
19
|
-
end
|
20
|
-
|
21
|
-
def append(builder)
|
22
|
-
case builder
|
23
|
-
when EventBuilder
|
24
|
-
@component['events'] << builder.component
|
25
|
-
when TimeZoneBuilder
|
26
|
-
@component['time_zones'] << builder.component
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def component
|
31
|
-
@component
|
32
|
-
end
|
33
8
|
end
|
34
9
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Selene
|
2
|
+
class ComponentBuilder
|
3
|
+
include ComponentRules
|
4
|
+
|
5
|
+
attr_accessor :component, :parent, :errors
|
6
|
+
|
7
|
+
REQUIRED_PROPERTIES = []
|
8
|
+
DISTINCT_PROPERTIES = []
|
9
|
+
EXCLUSIVE_PROPERTIES = []
|
10
|
+
|
11
|
+
def add(name, builder)
|
12
|
+
@component[name.downcase] << builder.component
|
13
|
+
builder.parent = self
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@component = Hash.new { |component, property| component[property] = [] }
|
18
|
+
@errors = []
|
19
|
+
@property_rules = property_rules(self)
|
20
|
+
@component_rules = component_rules(self)
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse(line)
|
24
|
+
return unless @property_rules.all? { |message, rule| rule.call(name(line), line, message) }
|
25
|
+
@component[name(line)] = value(line)
|
26
|
+
end
|
27
|
+
|
28
|
+
def name(line)
|
29
|
+
line.name
|
30
|
+
end
|
31
|
+
|
32
|
+
def value(line)
|
33
|
+
line.value
|
34
|
+
end
|
35
|
+
|
36
|
+
def a_component_rules
|
37
|
+
@component_rules
|
38
|
+
end
|
39
|
+
|
40
|
+
def valid?
|
41
|
+
@component_rules.each { |message, rule| rule.call(message) }
|
42
|
+
@errors.empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Selene
|
2
|
+
module ComponentRules
|
3
|
+
|
4
|
+
def property_rules(component)
|
5
|
+
{}.tap do |rules|
|
6
|
+
rules["property %s must not occur more than once"] = lambda do |property, line, message|
|
7
|
+
invalid = @component.key?(property) && component.class::DISTINCT_PROPERTIES.include?(property)
|
8
|
+
!invalid.tap { @errors << { :message => message % property } if invalid }
|
9
|
+
end
|
10
|
+
|
11
|
+
rules["properties '%s' and '%s' cannot occur in the same component"] = lambda do |property, line, message|
|
12
|
+
passed = true
|
13
|
+
component.class::EXCLUSIVE_PROPERTIES.each do |properties|
|
14
|
+
other_property = properties.find { |p| @component.key?(p) && p != property }
|
15
|
+
next unless other_property
|
16
|
+
passed = false
|
17
|
+
@errors << { :message => message % [property, other_property] }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def component_rules(component)
|
24
|
+
{}.tap do |rules|
|
25
|
+
rules["missing required property '%s'"] = lambda do |message|
|
26
|
+
passed = true
|
27
|
+
component.class::REQUIRED_PROPERTIES.each do |required|
|
28
|
+
next if @component.key?(required)
|
29
|
+
passed = false
|
30
|
+
@errors << { :message => message % required }
|
31
|
+
end
|
32
|
+
passed
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -1,21 +1,14 @@
|
|
1
1
|
module Selene
|
2
|
-
class DaylightSavingsTimeBuilder
|
2
|
+
class DaylightSavingsTimeBuilder < ComponentBuilder
|
3
3
|
|
4
|
-
def
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
def component
|
9
|
-
@component
|
10
|
-
end
|
11
|
-
|
12
|
-
def parse(line)
|
13
|
-
@component[line.name.downcase] = case line.name
|
14
|
-
when 'RRULE'
|
15
|
-
Hash[line.value.split(';').map { |vs| k, v = vs.split('=', 2); [k.downcase, v] }]
|
4
|
+
def value(line)
|
5
|
+
case line.name
|
6
|
+
when 'rrule'
|
7
|
+
line.rrule
|
16
8
|
else
|
17
|
-
|
9
|
+
super
|
18
10
|
end
|
19
11
|
end
|
12
|
+
|
20
13
|
end
|
21
14
|
end
|
data/lib/selene/event_builder.rb
CHANGED
@@ -1,29 +1,45 @@
|
|
1
1
|
module Selene
|
2
|
-
class EventBuilder
|
2
|
+
class EventBuilder < ComponentBuilder
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
end
|
4
|
+
# These properties are required
|
5
|
+
REQUIRED_PROPERTIES = %w(dtstamp uid)
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
# These properties must not occur more than once
|
8
|
+
DISTINCT_PROPERTIES = %w(dtstamp uid dtstart class created description geo
|
9
|
+
last-mod location organizer priority seq status summary
|
10
|
+
transp url recurid)
|
11
|
+
|
12
|
+
# These properties must not occur together in the same component
|
13
|
+
EXCLUSIVE_PROPERTIES = [
|
14
|
+
%w(dtend duration)
|
15
|
+
]
|
11
16
|
|
12
|
-
|
13
|
-
|
14
|
-
|
17
|
+
# single_property :rrule, :except => when?
|
18
|
+
# if dtstart is a date, dtend has to be too
|
19
|
+
# multi-day durations must be 'dur-day' or 'dur-week'
|
20
|
+
|
21
|
+
def value(line)
|
22
|
+
case line.name
|
23
|
+
when 'dtstamp', 'dtstart', 'dtend'
|
15
24
|
line.value_with_params
|
16
|
-
when '
|
17
|
-
line.
|
25
|
+
when 'geo'
|
26
|
+
line.values
|
18
27
|
else
|
19
|
-
|
28
|
+
super
|
20
29
|
end
|
21
30
|
end
|
22
31
|
|
23
|
-
def
|
24
|
-
|
25
|
-
|
26
|
-
|
32
|
+
def parent=(builder)
|
33
|
+
raise Exception.new("Event components cannot be nested inside anything but a calendar component") unless builder.is_a?(CalendarBuilder)
|
34
|
+
super(builder)
|
35
|
+
end
|
36
|
+
|
37
|
+
def component_rules(component)
|
38
|
+
super(component).tap do |rules|
|
39
|
+
rules["Property 'dtstart' required if the calendar does not specify a 'method' property"] = lambda do |message|
|
40
|
+
return if @component.key?('dtstart') || parent && parent.component.key?('method')
|
41
|
+
@errors << { :message => message }
|
42
|
+
end
|
27
43
|
end
|
28
44
|
end
|
29
45
|
|
data/lib/selene/line.rb
CHANGED
@@ -29,7 +29,7 @@ module Selene
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
|
32
|
+
# Parse a param string into a hash
|
33
33
|
def self.parse_params(params_string)
|
34
34
|
{}.tap do |params|
|
35
35
|
return params unless params_string
|
@@ -42,17 +42,17 @@ module Selene
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def initialize(name, params, value)
|
45
|
-
self.name = name.
|
45
|
+
self.name = name.downcase
|
46
46
|
self.params = params || {}
|
47
47
|
self.value = value
|
48
48
|
end
|
49
49
|
|
50
50
|
def begin_component?
|
51
|
-
name == '
|
51
|
+
name == 'begin'
|
52
52
|
end
|
53
53
|
|
54
54
|
def end_component?
|
55
|
-
name == '
|
55
|
+
name == 'end'
|
56
56
|
end
|
57
57
|
|
58
58
|
def params?
|
@@ -63,9 +63,12 @@ module Selene
|
|
63
63
|
params? ? [value, params] : value
|
64
64
|
end
|
65
65
|
|
66
|
-
def
|
67
|
-
|
68
|
-
|
66
|
+
def rrule
|
67
|
+
Hash[values.map { |values| k, v = values.split('=', 2); [k.downcase, v] }]
|
68
|
+
end
|
69
|
+
|
70
|
+
def values
|
71
|
+
value.split(';')
|
69
72
|
end
|
70
73
|
|
71
74
|
end
|
data/lib/selene/parser.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
+
require 'selene/line'
|
2
|
+
require 'selene/component_rules'
|
3
|
+
require 'selene/component_builder'
|
4
|
+
|
1
5
|
require 'selene/alarm_builder'
|
2
6
|
require 'selene/calendar_builder'
|
3
7
|
require 'selene/daylight_savings_time_builder'
|
4
8
|
require 'selene/event_builder'
|
5
|
-
require 'selene/line'
|
6
9
|
require 'selene/standard_time_builder'
|
7
10
|
require 'selene/time_zone_builder'
|
8
11
|
|
@@ -10,35 +13,32 @@ module Selene
|
|
10
13
|
module Parser
|
11
14
|
|
12
15
|
def self.builder(component)
|
13
|
-
case component
|
14
|
-
when '
|
15
|
-
when '
|
16
|
-
when '
|
17
|
-
when '
|
18
|
-
when '
|
19
|
-
when '
|
20
|
-
else
|
16
|
+
case component.downcase
|
17
|
+
when 'vcalendar' then CalendarBuilder
|
18
|
+
when 'vtimezone' then TimeZoneBuilder
|
19
|
+
when 'daylight' then DaylightSavingsTimeBuilder
|
20
|
+
when 'standard' then StandardTimeBuilder
|
21
|
+
when 'vevent' then EventBuilder
|
22
|
+
when 'valarm' then AlarmBuilder
|
23
|
+
else ComponentBuilder
|
21
24
|
end
|
22
25
|
end
|
23
26
|
|
24
27
|
def self.parse(string)
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
37
|
-
else
|
38
|
-
stack[-1].parse(line)
|
39
|
-
end
|
28
|
+
stack = []
|
29
|
+
stack << builder('feed').new
|
30
|
+
Line.split(string).each do |line|
|
31
|
+
if line.begin_component?
|
32
|
+
builder = builder(line.value).new
|
33
|
+
stack[-1].add(line.value, builder) unless stack.empty?
|
34
|
+
stack << builder
|
35
|
+
elsif line.end_component?
|
36
|
+
stack.pop
|
37
|
+
else
|
38
|
+
stack[-1].parse(line)
|
40
39
|
end
|
41
40
|
end
|
41
|
+
stack[-1].component
|
42
42
|
end
|
43
43
|
|
44
44
|
end
|
@@ -1,16 +1,4 @@
|
|
1
1
|
module Selene
|
2
|
-
class StandardTimeBuilder
|
3
|
-
|
4
|
-
def initialize
|
5
|
-
@component = {}
|
6
|
-
end
|
7
|
-
|
8
|
-
def component
|
9
|
-
@component
|
10
|
-
end
|
11
|
-
|
12
|
-
def parse(line)
|
13
|
-
@component[line.name.downcase] = line.value
|
14
|
-
end
|
2
|
+
class StandardTimeBuilder < ComponentBuilder
|
15
3
|
end
|
16
4
|
end
|
@@ -1,24 +1,4 @@
|
|
1
1
|
module Selene
|
2
|
-
class TimeZoneBuilder
|
3
|
-
|
4
|
-
def initialize
|
5
|
-
@component = Hash.new { |component, property| component[property] = [] }
|
6
|
-
end
|
7
|
-
|
8
|
-
def component
|
9
|
-
@component
|
10
|
-
end
|
11
|
-
|
12
|
-
def parse(line)
|
13
|
-
@component[line.name.downcase] = line.value
|
14
|
-
end
|
15
|
-
|
16
|
-
def append(builder)
|
17
|
-
case builder
|
18
|
-
when DaylightSavingsTimeBuilder
|
19
|
-
@component['daylight'] << builder.component
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
2
|
+
class TimeZoneBuilder < ComponentBuilder
|
23
3
|
end
|
24
4
|
end
|
data/lib/selene/version.rb
CHANGED
data/test/fixtures/meetup.json
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
{
|
2
|
-
"
|
2
|
+
"vcalendar": [
|
3
3
|
{
|
4
4
|
"version": "2.0",
|
5
5
|
"prodid": "-//Meetup//RemoteApi//EN",
|
@@ -7,7 +7,7 @@
|
|
7
7
|
"method": "PUBLISH",
|
8
8
|
"x-original-url": "http://www.meetup.com/DetroitRuby/events/ical/DetroitRuby/",
|
9
9
|
"x-wr-calname": "Events - DetroitRuby",
|
10
|
-
"
|
10
|
+
"vtimezone": [
|
11
11
|
{
|
12
12
|
"tzid": "America/New_York",
|
13
13
|
"tzurl": "http://tzurl.org/zoneinfo-outlook/America/New_York",
|
@@ -24,10 +24,19 @@
|
|
24
24
|
"byday": "2SU"
|
25
25
|
}
|
26
26
|
}
|
27
|
+
],
|
28
|
+
"standard": [
|
29
|
+
{
|
30
|
+
"tzoffsetfrom": "-0400",
|
31
|
+
"tzoffsetto": "-0500",
|
32
|
+
"tzname": "EST",
|
33
|
+
"dtstart": "19701101T020000",
|
34
|
+
"rrule": "FREQ=YEARLY;BYMONTH=11;BYDAY=1SU"
|
35
|
+
}
|
27
36
|
]
|
28
37
|
}
|
29
38
|
],
|
30
|
-
"
|
39
|
+
"vevent": [
|
31
40
|
{
|
32
41
|
"dtstamp": "20121231T093631Z",
|
33
42
|
"dtstart": [
|
@@ -1,7 +1,8 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
3
|
module Selene
|
4
|
-
class CalendarBuilderTest < TestCase
|
4
|
+
class CalendarBuilderTest < MiniTest::Unit::TestCase
|
5
|
+
include BuilderTestHelper
|
5
6
|
|
6
7
|
def builder
|
7
8
|
@builder ||= CalendarBuilder.new
|
@@ -16,40 +17,55 @@ module Selene
|
|
16
17
|
end
|
17
18
|
|
18
19
|
def test_parse_prodid
|
19
|
-
parse_line('PRODID',
|
20
|
+
parse_line('PRODID', {}, '-//Meetup//RemoteApi//EN')
|
20
21
|
assert_equal builder.component['prodid'], '-//Meetup//RemoteApi//EN'
|
21
22
|
end
|
22
23
|
|
23
24
|
def test_parse_version
|
24
|
-
parse_line('VERSION',
|
25
|
+
parse_line('VERSION', {}, '2.0')
|
25
26
|
assert_equal @builder.component['version'], '2.0'
|
26
27
|
end
|
27
28
|
|
28
29
|
def test_parse_calscale
|
29
|
-
parse_line('CALSCALE',
|
30
|
+
parse_line('CALSCALE', {}, 'Gregorian')
|
30
31
|
assert_equal builder.component['calscale'], 'Gregorian'
|
31
32
|
end
|
32
33
|
|
33
34
|
def test_parse_method
|
34
|
-
parse_line('METHOD',
|
35
|
+
parse_line('METHOD', {}, 'Publish')
|
35
36
|
assert_equal builder.component['method'], 'Publish'
|
36
37
|
end
|
37
38
|
|
38
39
|
def test_parse_x_prop
|
39
|
-
parse_line('X-ORIGINAL-URL',
|
40
|
+
parse_line('X-ORIGINAL-URL', {}, 'http://www.google.com')
|
40
41
|
assert_equal builder.component['x-original-url'], 'http://www.google.com'
|
41
42
|
end
|
42
43
|
|
43
44
|
def test_append_event_builder
|
44
45
|
event_builder.stub :component, { 'summary' => "Bluth's Best Party" }
|
45
|
-
builder.
|
46
|
-
assert_equal builder.component['
|
46
|
+
builder.add('vevent', event_builder)
|
47
|
+
assert_equal builder.component['vevent'].first['summary'], "Bluth's Best Party"
|
47
48
|
end
|
48
49
|
|
49
50
|
def test_append_time_zone_builder
|
50
51
|
time_zone_builder.stub :component, { 'tzid' => 'America/Detroit' }
|
51
|
-
builder.
|
52
|
-
assert_equal builder.component['
|
52
|
+
builder.add('vtimezone', time_zone_builder)
|
53
|
+
assert_equal builder.component['vtimezone'].first, { 'tzid' => 'America/Detroit' }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Validation
|
57
|
+
|
58
|
+
%w(prodid version).each do |property|
|
59
|
+
define_method "test_#{property}_required" do
|
60
|
+
assert_required property
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
%w(prodid version calscale method).each do |property|
|
65
|
+
define_method "test_#{property}_cant_be_defined_more_than_once" do
|
66
|
+
assert_single property
|
67
|
+
assert_multiple_values_do_not_overwrite property
|
68
|
+
end
|
53
69
|
end
|
54
70
|
|
55
71
|
end
|
@@ -1,13 +1,14 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
3
|
module Selene
|
4
|
-
class DaylightSavingsTimeBuilderTest < TestCase
|
4
|
+
class DaylightSavingsTimeBuilderTest < MiniTest::Unit::TestCase
|
5
|
+
include BuilderTestHelper
|
5
6
|
|
6
7
|
def builder
|
7
8
|
@builder ||= DaylightSavingsTimeBuilder.new
|
8
9
|
end
|
9
10
|
|
10
|
-
def test_parses_rrule
|
11
|
+
def test_parses_rrule
|
11
12
|
parse_line('RRULE', '', 'FREQ=YEARLY;BYMONTH=3;BYDAY=2SU')
|
12
13
|
assert_equal builder.component['rrule'], { 'freq' => 'YEARLY', 'bymonth' => '3', 'byday' => '2SU' }
|
13
14
|
end
|
@@ -1,20 +1,25 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
3
|
module Selene
|
4
|
-
class EventBuilderTest < TestCase
|
4
|
+
class EventBuilderTest < MiniTest::Unit::TestCase
|
5
|
+
include BuilderTestHelper
|
5
6
|
|
6
7
|
def builder
|
7
8
|
@builder ||= EventBuilder.new
|
8
9
|
end
|
9
10
|
|
11
|
+
def builder_with_parent
|
12
|
+
builder.tap { |b| b.parent = CalendarBuilder.new }
|
13
|
+
end
|
14
|
+
|
10
15
|
def test_parses_dtstart
|
11
|
-
expected = ['20130110T183000', { 'tzid' => 'America/New_York' }]
|
12
16
|
parse_line('DTSTART', { 'tzid' => 'America/New_York' }, '20130110T183000')
|
13
|
-
assert_equal builder.component['dtstart'],
|
17
|
+
assert_equal builder.component['dtstart'],
|
18
|
+
['20130110T183000', { 'tzid' => 'America/New_York' }]
|
14
19
|
end
|
15
20
|
|
16
21
|
def test_parses_dtstamp
|
17
|
-
parse_line('DTSTAMP',
|
22
|
+
parse_line('DTSTAMP', {}, '20121231T093631Z')
|
18
23
|
assert_equal builder.component['dtstamp'], '20121231T093631Z'
|
19
24
|
end
|
20
25
|
|
@@ -24,65 +29,93 @@ module Selene
|
|
24
29
|
end
|
25
30
|
|
26
31
|
def test_parses_dtend
|
27
|
-
expected = ['20130110T183000', { 'tzid' => 'America/New_York' }]
|
28
32
|
parse_line('DTEND', { 'tzid' => 'America/New_York' }, '20130110T183000')
|
29
|
-
assert_equal builder.component['dtend'],
|
33
|
+
assert_equal builder.component['dtend'], ['20130110T183000', { 'tzid' => 'America/New_York' }]
|
30
34
|
end
|
31
35
|
|
32
36
|
def test_parses_dtstart_without_tzid
|
33
|
-
parse_line('DTEND',
|
37
|
+
parse_line('DTEND', {}, '20130110T183000')
|
34
38
|
assert_equal builder.component['dtend'], '20130110T183000'
|
35
39
|
end
|
36
40
|
|
37
41
|
def test_parses_status
|
38
|
-
parse_line('STATUS',
|
42
|
+
parse_line('STATUS', {}, 'CONFIRMED')
|
39
43
|
assert_equal builder.component['status'], 'CONFIRMED'
|
40
44
|
end
|
41
45
|
|
42
46
|
def test_parses_summary
|
43
|
-
parse_line('SUMMARY',
|
47
|
+
parse_line('SUMMARY', {}, 'DetroitRuby: 2012 Plan + Lightning talks')
|
44
48
|
assert_equal builder.component['summary'], 'DetroitRuby: 2012 Plan + Lightning talks'
|
45
49
|
end
|
46
50
|
|
47
51
|
def test_parses_description
|
48
|
-
parse_line('DESCRIPTION',
|
52
|
+
parse_line('DESCRIPTION', {}, 'DetroitRuby\nThursday\, January 10 at 6:30 PM\n\n')
|
49
53
|
assert_equal builder.component['description'], 'DetroitRuby\nThursday\, January 10 at 6:30 PM\n\n'
|
50
54
|
end
|
51
55
|
|
52
56
|
def test_parses_class
|
53
|
-
parse_line('CLASS',
|
57
|
+
parse_line('CLASS', {}, 'PUBLIC')
|
54
58
|
assert_equal builder.component['class'], 'PUBLIC'
|
55
59
|
end
|
56
60
|
|
57
61
|
def test_parses_created
|
58
|
-
parse_line('CREATED',
|
62
|
+
parse_line('CREATED', {}, '20120106T161509Z')
|
59
63
|
assert_equal builder.component['created'], '20120106T161509Z'
|
60
64
|
end
|
61
65
|
|
62
66
|
def test_parses_geo
|
63
|
-
parse_line('GEO',
|
67
|
+
parse_line('GEO', {}, '42.33;-83.05')
|
64
68
|
assert_equal builder.component['geo'], ['42.33', '-83.05']
|
65
69
|
end
|
66
70
|
|
67
71
|
def test_parses_location
|
68
|
-
parse_line('LOCATION',
|
72
|
+
parse_line('LOCATION', {}, 'Compuware Building (One Campus Martius\, Detroit\, MI 48226)')
|
69
73
|
assert_equal builder.component['location'], 'Compuware Building (One Campus Martius\, Detroit\, MI 48226)'
|
70
74
|
end
|
71
75
|
|
72
76
|
def test_parses_url
|
73
|
-
parse_line('URL',
|
77
|
+
parse_line('URL', {}, 'http://www.meetup.com/DetroitRuby/events/93346412/')
|
74
78
|
assert_equal builder.component['url'], 'http://www.meetup.com/DetroitRuby/events/93346412/'
|
75
79
|
end
|
76
80
|
|
77
81
|
def test_parses_last_modified
|
78
|
-
parse_line('LAST-MODIFIED',
|
82
|
+
parse_line('LAST-MODIFIED', {}, '20120106T161509Z')
|
79
83
|
assert_equal builder.component['last-modified'], '20120106T161509Z'
|
80
84
|
end
|
81
85
|
|
82
86
|
def test_parses_uid
|
83
|
-
parse_line('UID',
|
87
|
+
parse_line('UID', {}, 'event_qgkxkcyrcbnb@meetup.com')
|
84
88
|
assert_equal builder.component['uid'], 'event_qgkxkcyrcbnb@meetup.com'
|
85
89
|
end
|
86
90
|
|
91
|
+
def test_sets_parent_calendar
|
92
|
+
assert builder_with_parent.parent.is_a?(CalendarBuilder), "Must be able to set the parent calendar builder"
|
93
|
+
end
|
94
|
+
|
95
|
+
# Validation
|
96
|
+
|
97
|
+
%w(dtstamp uid).each do |property|
|
98
|
+
define_method "test_#{property}_required" do
|
99
|
+
assert_required property
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
%w(dtstamp uid dtstart class created description geo last-mod location organizer priority seq status summary transp url recurid).each do |property|
|
104
|
+
define_method "test_#{property}_cant_be_defined_more_than_once" do
|
105
|
+
assert_single property
|
106
|
+
assert_multiple_values_do_not_overwrite property
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def test_adding_to_non_calendar_raises_exception
|
111
|
+
assert_raises Exception do
|
112
|
+
builder.parent = TimeZoneBuilder.new
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def test_dtstart_required_if_no_calendar_method
|
117
|
+
builder_with_parent.valid?
|
118
|
+
assert builder.errors.any? { |e| e[:message] == "Property 'dtstart' required if the calendar does not specify a 'method' property" }
|
119
|
+
end
|
87
120
|
end
|
88
121
|
end
|
data/test/selene/line_test.rb
CHANGED
@@ -1,24 +1,24 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
3
|
module Selene
|
4
|
-
class LineTest < TestCase
|
4
|
+
class LineTest < MiniTest::Unit::TestCase
|
5
5
|
|
6
6
|
def test_lines_are_unfolded_before_splitting
|
7
7
|
assert_equal Line.split("TEST:This is a\r\n test").first.value, "This is a test"
|
8
8
|
end
|
9
9
|
|
10
10
|
def test_parses_content_line
|
11
|
-
assert_equal Line.parse('VERSION:2.0'),
|
11
|
+
assert_equal Line.parse('VERSION:2.0'), Line.new('VERSION', {}, '2.0')
|
12
12
|
end
|
13
13
|
|
14
14
|
def test_parses_url
|
15
|
-
|
16
|
-
|
15
|
+
assert_equal Line.parse('TZURL:http://www.meetup.com/DetroitRuby/events/ical/DetroitRuby/'),
|
16
|
+
Line.new('TZURL', {}, 'http://www.meetup.com/DetroitRuby/events/ical/DetroitRuby/')
|
17
17
|
end
|
18
18
|
|
19
19
|
def test_parses_params
|
20
|
-
|
21
|
-
|
20
|
+
assert_equal Line.parse('DTSTART;TZID=America/New_York:20130110T183000'),
|
21
|
+
Line.new('DTSTART', { 'tzid' => 'America/New_York' }, '20130110T183000')
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
data/test/selene/parser_test.rb
CHANGED
@@ -2,19 +2,21 @@ require 'test_helper'
|
|
2
2
|
require 'json'
|
3
3
|
|
4
4
|
module Selene
|
5
|
-
class ParserTest < TestCase
|
5
|
+
class ParserTest < MiniTest::Unit::TestCase
|
6
|
+
include FixtureHelper
|
6
7
|
|
7
8
|
def test_parses_blank_string
|
8
|
-
assert_equal Selene::Parser.parse(""), {
|
9
|
+
assert_equal Selene::Parser.parse(""), {}
|
9
10
|
end
|
10
11
|
|
11
12
|
def test_parses_simple_calendar
|
12
|
-
assert_equal Selene::Parser.parse("BEGIN:VCALENDAR\r\nSUMMARY:Meetups\r\nEND:VCALENDAR"),
|
13
|
+
assert_equal Selene::Parser.parse("BEGIN:VCALENDAR\r\nSUMMARY:Meetups\r\nEND:VCALENDAR"),
|
14
|
+
{ 'vcalendar' => [{ 'summary' => 'Meetups' }] }
|
13
15
|
end
|
14
16
|
|
15
17
|
def test_parses_meetup_calendar
|
16
|
-
|
17
|
-
|
18
|
+
assert_equal Selene::Parser.parse(fixture('meetup.ics')),
|
19
|
+
JSON.parse(fixture('meetup.json'))
|
18
20
|
end
|
19
21
|
end
|
20
22
|
end
|
data/test/test_helper.rb
CHANGED
@@ -6,36 +6,60 @@ require 'minitest/mock'
|
|
6
6
|
require 'selene'
|
7
7
|
|
8
8
|
module Selene
|
9
|
-
|
9
|
+
module Stubbable
|
10
10
|
|
11
|
+
# Allowed syntax options:
|
12
|
+
#
|
13
|
+
# stub :method => 'value'
|
14
|
+
# stub :method, 'value'
|
15
|
+
# stub :method { 'value' }
|
16
|
+
|
17
|
+
def stub(hash_or_method, value_or_block = nil)
|
18
|
+
if hash_or_method.is_a?(Hash)
|
19
|
+
hash_or_method.each { |method, value| stub method, Proc.new { value } }
|
20
|
+
elsif !value_or_block.is_a?(Proc)
|
21
|
+
stub hash_or_method, Proc.new { value_or_block }
|
22
|
+
else
|
23
|
+
define_singleton_method hash_or_method, value_or_block
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
module FixtureHelper
|
11
30
|
def fixture(filename)
|
12
31
|
File.read(File.join(File.dirname(__FILE__), 'fixtures', filename))
|
13
32
|
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module BuilderTestHelper
|
14
36
|
|
15
37
|
# This is a helper method that takes a name, params and a value, turns them into a proper line hash,
|
16
38
|
# and passes them to the builder to be parsed.
|
17
39
|
def parse_line(name, params, value)
|
18
|
-
raise "Must define builder method in test case" unless builder
|
19
40
|
builder.parse(Line.new(name, params, value))
|
20
41
|
end
|
21
42
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
43
|
+
def assert_required property
|
44
|
+
builder.valid?
|
45
|
+
message = "missing required property '#{property}'"
|
46
|
+
assert builder.errors.any? { |e| e[:message] =~ /#{message}/ }, message
|
47
|
+
end
|
26
48
|
|
27
|
-
|
28
|
-
|
29
|
-
|
49
|
+
def assert_single property
|
50
|
+
parse_line(property, {}, 'Some Value')
|
51
|
+
n = builder.errors.count
|
52
|
+
parse_line(property, {}, 'Another Value')
|
53
|
+
n = builder.errors.count - n
|
54
|
+
assert_equal n, 1, "Cannot have more than one #{property}"
|
55
|
+
end
|
30
56
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
else
|
37
|
-
define_singleton_method hash_or_method, value_or_block
|
57
|
+
def assert_multiple_values_do_not_overwrite property
|
58
|
+
parse_line(property, {}, 'Some Value')
|
59
|
+
value = builder.component[property].dup
|
60
|
+
parse_line(property, {}, 'Another Value')
|
61
|
+
assert_equal builder.component[property], value
|
38
62
|
end
|
39
|
-
end
|
40
63
|
|
64
|
+
end
|
41
65
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: selene
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-01-
|
12
|
+
date: 2013-01-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: debugger
|
@@ -69,7 +69,6 @@ files:
|
|
69
69
|
- .gitattributes
|
70
70
|
- .gitignore
|
71
71
|
- .rspec
|
72
|
-
- .rvmrc
|
73
72
|
- Gemfile
|
74
73
|
- LICENSE.txt
|
75
74
|
- README.md
|
@@ -77,8 +76,11 @@ files:
|
|
77
76
|
- lib/selene.rb
|
78
77
|
- lib/selene/alarm_builder.rb
|
79
78
|
- lib/selene/calendar_builder.rb
|
79
|
+
- lib/selene/component_builder.rb
|
80
|
+
- lib/selene/component_rules.rb
|
80
81
|
- lib/selene/daylight_savings_time_builder.rb
|
81
82
|
- lib/selene/event_builder.rb
|
83
|
+
- lib/selene/feed_builder.rb
|
82
84
|
- lib/selene/line.rb
|
83
85
|
- lib/selene/parser.rb
|
84
86
|
- lib/selene/standard_time_builder.rb
|
@@ -109,7 +111,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
109
111
|
version: '0'
|
110
112
|
segments:
|
111
113
|
- 0
|
112
|
-
hash:
|
114
|
+
hash: 500821961400754664
|
113
115
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
116
|
none: false
|
115
117
|
requirements:
|
@@ -118,7 +120,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
118
120
|
version: '0'
|
119
121
|
segments:
|
120
122
|
- 0
|
121
|
-
hash:
|
123
|
+
hash: 500821961400754664
|
122
124
|
requirements: []
|
123
125
|
rubyforge_project:
|
124
126
|
rubygems_version: 1.8.24
|
data/.rvmrc
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
rvm use 1.9.3@selene --create
|