selene 0.3.0 → 0.3.1
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/.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
|
-

|
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
|
+

|
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
|