selene 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/selene/alarm_builder.rb +2 -2
- data/lib/selene/calendar_builder.rb +10 -1
- data/lib/selene/daylight_savings_time_builder.rb +5 -5
- data/lib/selene/event_builder.rb +5 -5
- data/lib/selene/line.rb +72 -0
- data/lib/selene/parser.rb +6 -30
- data/lib/selene/standard_time_builder.rb +2 -2
- data/lib/selene/time_zone_builder.rb +2 -2
- data/lib/selene/version.rb +1 -1
- data/test/selene/alarm_builder_test.rb +16 -0
- data/test/selene/calendar_builder_test.rb +5 -5
- data/test/selene/daylight_savings_time_builder_test.rb +1 -1
- data/test/selene/event_builder_test.rb +15 -15
- data/test/selene/line_test.rb +24 -0
- data/test/selene/parser_test.rb +0 -20
- data/test/test_helper.rb +7 -0
- metadata +9 -4
data/lib/selene/alarm_builder.rb
CHANGED
@@ -2,13 +2,22 @@ module Selene
|
|
2
2
|
class CalendarBuilder
|
3
3
|
|
4
4
|
def initialize
|
5
|
+
@errors = []
|
5
6
|
@component = Hash.new { |component, property| component[property] = [] }
|
6
7
|
end
|
7
8
|
|
8
|
-
def parse(
|
9
|
+
def parse(line)
|
10
|
+
set_property line.name, line.value
|
11
|
+
end
|
12
|
+
|
13
|
+
def set_property(name, value)
|
9
14
|
@component[name.downcase] = value
|
10
15
|
end
|
11
16
|
|
17
|
+
def name(line)
|
18
|
+
line.name.downcase
|
19
|
+
end
|
20
|
+
|
12
21
|
def append(builder)
|
13
22
|
case builder
|
14
23
|
when EventBuilder
|
@@ -9,12 +9,12 @@ module Selene
|
|
9
9
|
@component
|
10
10
|
end
|
11
11
|
|
12
|
-
def parse(
|
13
|
-
@component[name.downcase] = case name
|
14
|
-
when 'RRULE'
|
15
|
-
Hash[value.split(';').map { |vs| k, v = vs.split('=', 2); [k.downcase, v] }]
|
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] }]
|
16
16
|
else
|
17
|
-
value
|
17
|
+
line.value
|
18
18
|
end
|
19
19
|
end
|
20
20
|
end
|
data/lib/selene/event_builder.rb
CHANGED
@@ -9,14 +9,14 @@ module Selene
|
|
9
9
|
@component
|
10
10
|
end
|
11
11
|
|
12
|
-
def parse(
|
13
|
-
component[name.downcase] = case name
|
12
|
+
def parse(line)
|
13
|
+
component[line.name.downcase] = case line.name
|
14
14
|
when 'DTSTAMP', 'DTSTART', 'DTEND'
|
15
|
-
|
15
|
+
line.value_with_params
|
16
16
|
when 'GEO'
|
17
|
-
value.split(';')
|
17
|
+
line.value.split(';')
|
18
18
|
else
|
19
|
-
value
|
19
|
+
line.value
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
data/lib/selene/line.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
module Selene
|
2
|
+
class Line < Struct.new(:name, :params, :value)
|
3
|
+
|
4
|
+
# Match everything until we hit ';' (parameter separator) or ':' (value separator)
|
5
|
+
NAME = /(?<name>[^:\;]+)/
|
6
|
+
|
7
|
+
# Match everything until we hit ':' (value separator)
|
8
|
+
PARAMS = /(?:\;(?<params>[^:]*))/
|
9
|
+
|
10
|
+
# Match everything that's left
|
11
|
+
VALUE = /(?<value>.*)/
|
12
|
+
|
13
|
+
# Match everything that is not ';' (parameter separator)
|
14
|
+
PARAM = /[^\;]+/
|
15
|
+
|
16
|
+
# Match everything before '='
|
17
|
+
PARAM_KEY_VALUE = /(?<key>[^=]+)=(?<value>.*)/
|
18
|
+
|
19
|
+
# Split a string into content lines
|
20
|
+
def self.split(string)
|
21
|
+
delimiter = string.scan(/\r\n?|\n/).first || "\r\n"
|
22
|
+
string.gsub(/#{delimiter}\s/, '').split(delimiter).map { |line| parse(line) }
|
23
|
+
end
|
24
|
+
|
25
|
+
# Parse a content line into a line object
|
26
|
+
def self.parse(content_line)
|
27
|
+
content_line.match(/#{NAME}#{PARAMS}?:#{VALUE}/) do |match|
|
28
|
+
return new(match[:name], parse_params(match[:params]), match[:value])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def self.parse_params(params_string)
|
34
|
+
{}.tap do |params|
|
35
|
+
return params unless params_string
|
36
|
+
params_string.scan(PARAM).map do |param|
|
37
|
+
param.match(PARAM_KEY_VALUE) do |match|
|
38
|
+
params[match[:key].downcase] = match[:value]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize(name, params, value)
|
45
|
+
self.name = name.upcase
|
46
|
+
self.params = params || {}
|
47
|
+
self.value = value
|
48
|
+
end
|
49
|
+
|
50
|
+
def begin_component?
|
51
|
+
name == 'BEGIN'
|
52
|
+
end
|
53
|
+
|
54
|
+
def end_component?
|
55
|
+
name == 'END'
|
56
|
+
end
|
57
|
+
|
58
|
+
def params?
|
59
|
+
params && !params.empty?
|
60
|
+
end
|
61
|
+
|
62
|
+
def value_with_params
|
63
|
+
params? ? [value, params] : value
|
64
|
+
end
|
65
|
+
|
66
|
+
def ==(line)
|
67
|
+
super(line) unless line.is_a?(Hash)
|
68
|
+
members.all? { |key| line[key] == self.send(key) }
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
data/lib/selene/parser.rb
CHANGED
@@ -2,6 +2,7 @@ require 'selene/alarm_builder'
|
|
2
2
|
require 'selene/calendar_builder'
|
3
3
|
require 'selene/daylight_savings_time_builder'
|
4
4
|
require 'selene/event_builder'
|
5
|
+
require 'selene/line'
|
5
6
|
require 'selene/standard_time_builder'
|
6
7
|
require 'selene/time_zone_builder'
|
7
8
|
|
@@ -23,11 +24,10 @@ module Selene
|
|
23
24
|
def self.parse(string)
|
24
25
|
{ 'calendars' => [] }.tap do |feed|
|
25
26
|
stack = []
|
26
|
-
|
27
|
-
line
|
28
|
-
|
29
|
-
|
30
|
-
elsif line[:name].upcase == 'END'
|
27
|
+
Line.split(string).each do |line|
|
28
|
+
if line.begin_component?
|
29
|
+
stack << builder(line.value).new
|
30
|
+
elsif line.end_component?
|
31
31
|
builder = stack.pop
|
32
32
|
if !stack.empty?
|
33
33
|
stack[-1].append(builder)
|
@@ -35,35 +35,11 @@ module Selene
|
|
35
35
|
feed['calendars'] << builder.component
|
36
36
|
end
|
37
37
|
else
|
38
|
-
stack[-1].parse(line
|
38
|
+
stack[-1].parse(line)
|
39
39
|
end
|
40
40
|
end
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
-
def self.parse_content_line(string)
|
45
|
-
string =~ /([^:\;]+)(?:\;([^:]*))?:(.*)/i
|
46
|
-
{ :name => $1, :params => parse_params($2), :value => $3 }
|
47
|
-
end
|
48
|
-
|
49
|
-
def self.parse_date_and_time(name, params, value)
|
50
|
-
params && params != {} ? [value, params] : value
|
51
|
-
end
|
52
|
-
|
53
|
-
def self.parse_params(raw_params)
|
54
|
-
return unless raw_params
|
55
|
-
{}.tap do |params|
|
56
|
-
raw_params.scan(/[^\;]+/i).map do |param|
|
57
|
-
param =~ /([^=]+)=(.*)/
|
58
|
-
params[$1.downcase] = $2
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def self.split_content_lines(string)
|
64
|
-
line_break = string.scan(/\r\n?|\n/).first || "\r\n"
|
65
|
-
string.gsub(/#{line_break}\s/, '').split(line_break)
|
66
|
-
end
|
67
|
-
|
68
44
|
end
|
69
45
|
end
|
data/lib/selene/version.rb
CHANGED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Selene
|
4
|
+
class AlarmBuilderTest < TestCase
|
5
|
+
|
6
|
+
def builder
|
7
|
+
@builder ||= AlarmBuilder.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_parses_action
|
11
|
+
parse_line 'ACTION', {}, 'AUDIO'
|
12
|
+
assert_equal builder.component['action'], 'AUDIO'
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -16,27 +16,27 @@ module Selene
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def test_parse_prodid
|
19
|
-
|
19
|
+
parse_line('PRODID', '', '-//Meetup//RemoteApi//EN')
|
20
20
|
assert_equal builder.component['prodid'], '-//Meetup//RemoteApi//EN'
|
21
21
|
end
|
22
22
|
|
23
23
|
def test_parse_version
|
24
|
-
|
24
|
+
parse_line('VERSION', '', '2.0')
|
25
25
|
assert_equal @builder.component['version'], '2.0'
|
26
26
|
end
|
27
27
|
|
28
28
|
def test_parse_calscale
|
29
|
-
|
29
|
+
parse_line('CALSCALE', '', 'Gregorian')
|
30
30
|
assert_equal builder.component['calscale'], 'Gregorian'
|
31
31
|
end
|
32
32
|
|
33
33
|
def test_parse_method
|
34
|
-
|
34
|
+
parse_line('METHOD', '', 'Publish')
|
35
35
|
assert_equal builder.component['method'], 'Publish'
|
36
36
|
end
|
37
37
|
|
38
38
|
def test_parse_x_prop
|
39
|
-
|
39
|
+
parse_line('X-ORIGINAL-URL', '', 'http://www.google.com')
|
40
40
|
assert_equal builder.component['x-original-url'], 'http://www.google.com'
|
41
41
|
end
|
42
42
|
|
@@ -8,7 +8,7 @@ module Selene
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def test_parses_rrule
|
11
|
-
|
11
|
+
parse_line('RRULE', '', 'FREQ=YEARLY;BYMONTH=3;BYDAY=2SU')
|
12
12
|
assert_equal builder.component['rrule'], { 'freq' => 'YEARLY', 'bymonth' => '3', 'byday' => '2SU' }
|
13
13
|
end
|
14
14
|
|
@@ -9,78 +9,78 @@ module Selene
|
|
9
9
|
|
10
10
|
def test_parses_dtstart
|
11
11
|
expected = ['20130110T183000', { 'tzid' => 'America/New_York' }]
|
12
|
-
|
12
|
+
parse_line('DTSTART', { 'tzid' => 'America/New_York' }, '20130110T183000')
|
13
13
|
assert_equal builder.component['dtstart'], expected
|
14
14
|
end
|
15
15
|
|
16
16
|
def test_parses_dtstamp
|
17
|
-
|
17
|
+
parse_line('DTSTAMP', nil, '20121231T093631Z')
|
18
18
|
assert_equal builder.component['dtstamp'], '20121231T093631Z'
|
19
19
|
end
|
20
20
|
|
21
21
|
def test_parses_dtstart_without_tzid
|
22
|
-
|
22
|
+
parse_line('DTSTART', {}, '20130110T183000')
|
23
23
|
assert_equal builder.component['dtstart'], '20130110T183000'
|
24
24
|
end
|
25
25
|
|
26
26
|
def test_parses_dtend
|
27
27
|
expected = ['20130110T183000', { 'tzid' => 'America/New_York' }]
|
28
|
-
|
28
|
+
parse_line('DTEND', { 'tzid' => 'America/New_York' }, '20130110T183000')
|
29
29
|
assert_equal builder.component['dtend'], expected
|
30
30
|
end
|
31
31
|
|
32
32
|
def test_parses_dtstart_without_tzid
|
33
|
-
|
33
|
+
parse_line('DTEND', nil, '20130110T183000')
|
34
34
|
assert_equal builder.component['dtend'], '20130110T183000'
|
35
35
|
end
|
36
36
|
|
37
37
|
def test_parses_status
|
38
|
-
|
38
|
+
parse_line('STATUS', nil, 'CONFIRMED')
|
39
39
|
assert_equal builder.component['status'], 'CONFIRMED'
|
40
40
|
end
|
41
41
|
|
42
42
|
def test_parses_summary
|
43
|
-
|
43
|
+
parse_line('SUMMARY', nil, 'DetroitRuby: 2012 Plan + Lightning talks')
|
44
44
|
assert_equal builder.component['summary'], 'DetroitRuby: 2012 Plan + Lightning talks'
|
45
45
|
end
|
46
46
|
|
47
47
|
def test_parses_description
|
48
|
-
|
48
|
+
parse_line('DESCRIPTION', nil, 'DetroitRuby\nThursday\, January 10 at 6:30 PM\n\n')
|
49
49
|
assert_equal builder.component['description'], 'DetroitRuby\nThursday\, January 10 at 6:30 PM\n\n'
|
50
50
|
end
|
51
51
|
|
52
52
|
def test_parses_class
|
53
|
-
|
53
|
+
parse_line('CLASS', nil, 'PUBLIC')
|
54
54
|
assert_equal builder.component['class'], 'PUBLIC'
|
55
55
|
end
|
56
56
|
|
57
57
|
def test_parses_created
|
58
|
-
|
58
|
+
parse_line('CREATED', nil, '20120106T161509Z')
|
59
59
|
assert_equal builder.component['created'], '20120106T161509Z'
|
60
60
|
end
|
61
61
|
|
62
62
|
def test_parses_geo
|
63
|
-
|
63
|
+
parse_line('GEO', nil, '42.33;-83.05')
|
64
64
|
assert_equal builder.component['geo'], ['42.33', '-83.05']
|
65
65
|
end
|
66
66
|
|
67
67
|
def test_parses_location
|
68
|
-
|
68
|
+
parse_line('LOCATION', nil, 'Compuware Building (One Campus Martius\, Detroit\, MI 48226)')
|
69
69
|
assert_equal builder.component['location'], 'Compuware Building (One Campus Martius\, Detroit\, MI 48226)'
|
70
70
|
end
|
71
71
|
|
72
72
|
def test_parses_url
|
73
|
-
|
73
|
+
parse_line('URL', nil, 'http://www.meetup.com/DetroitRuby/events/93346412/')
|
74
74
|
assert_equal builder.component['url'], 'http://www.meetup.com/DetroitRuby/events/93346412/'
|
75
75
|
end
|
76
76
|
|
77
77
|
def test_parses_last_modified
|
78
|
-
|
78
|
+
parse_line('LAST-MODIFIED', nil, '20120106T161509Z')
|
79
79
|
assert_equal builder.component['last-modified'], '20120106T161509Z'
|
80
80
|
end
|
81
81
|
|
82
82
|
def test_parses_uid
|
83
|
-
|
83
|
+
parse_line('UID', nil, 'event_qgkxkcyrcbnb@meetup.com')
|
84
84
|
assert_equal builder.component['uid'], 'event_qgkxkcyrcbnb@meetup.com'
|
85
85
|
end
|
86
86
|
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Selene
|
4
|
+
class LineTest < TestCase
|
5
|
+
|
6
|
+
def test_lines_are_unfolded_before_splitting
|
7
|
+
assert_equal Line.split("TEST:This is a\r\n test").first.value, "This is a test"
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_parses_content_line
|
11
|
+
assert_equal Line.parse('VERSION:2.0'), { :name => 'VERSION', :params => {}, :value => '2.0' }
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_parses_url
|
15
|
+
expected = { :name => 'TZURL', :params => {}, :value => 'http://www.meetup.com/DetroitRuby/events/ical/DetroitRuby/' }
|
16
|
+
assert_equal Line.parse('TZURL:http://www.meetup.com/DetroitRuby/events/ical/DetroitRuby/'), expected
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_parses_params
|
20
|
+
expected = { :name => 'DTSTART', :params => { 'tzid' => 'America/New_York' }, :value => '20130110T183000' }
|
21
|
+
assert_equal Line.parse('DTSTART;TZID=America/New_York:20130110T183000'), expected
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/test/selene/parser_test.rb
CHANGED
@@ -4,30 +4,10 @@ require 'json'
|
|
4
4
|
module Selene
|
5
5
|
class ParserTest < TestCase
|
6
6
|
|
7
|
-
def test_lines_are_unfolded_before_splitting
|
8
|
-
assert_equal Selene::Parser.split_content_lines("This is a\r\n test").first, "This is a test"
|
9
|
-
end
|
10
|
-
|
11
7
|
def test_parses_blank_string
|
12
8
|
assert_equal Selene::Parser.parse(""), { 'calendars' => [] }
|
13
9
|
end
|
14
10
|
|
15
|
-
def test_parses_content_line
|
16
|
-
assert_equal Selene::Parser.parse_content_line('VERSION:2.0'), { :name => 'VERSION', :params => nil, :value => '2.0' }
|
17
|
-
end
|
18
|
-
|
19
|
-
def test_parses_content_line_with_url
|
20
|
-
expected = { :name => 'TZURL', :params => nil, :value => 'http://www.meetup.com/DetroitRuby/events/ical/DetroitRuby/' }
|
21
|
-
assert_equal Selene::Parser.parse_content_line('TZURL:http://www.meetup.com/DetroitRuby/events/ical/DetroitRuby/'), expected
|
22
|
-
end
|
23
|
-
|
24
|
-
def test_parses_content_line_with_param
|
25
|
-
expected = { :name => 'DTSTART', :params => { 'tzid' => 'America/New_York' }, :value => '20130110T183000' }
|
26
|
-
assert_equal Selene::Parser.parse_content_line('DTSTART;TZID=America/New_York:20130110T183000'), expected
|
27
|
-
end
|
28
|
-
|
29
|
-
# Sanity tests just to make sure the thing works
|
30
|
-
|
31
11
|
def test_parses_simple_calendar
|
32
12
|
assert_equal Selene::Parser.parse("BEGIN:VCALENDAR\r\nSUMMARY:Meetups\r\nEND:VCALENDAR"), { 'calendars' => [{ 'summary' => 'Meetups' }] }
|
33
13
|
end
|
data/test/test_helper.rb
CHANGED
@@ -12,6 +12,13 @@ module Selene
|
|
12
12
|
File.read(File.join(File.dirname(__FILE__), 'fixtures', filename))
|
13
13
|
end
|
14
14
|
|
15
|
+
# This is a helper method that takes a name, params and a value, turns them into a proper line hash,
|
16
|
+
# and passes them to the builder to be parsed.
|
17
|
+
def parse_line(name, params, value)
|
18
|
+
raise "Must define builder method in test case" unless builder
|
19
|
+
builder.parse(Line.new(name, params, value))
|
20
|
+
end
|
21
|
+
|
15
22
|
end
|
16
23
|
end
|
17
24
|
|
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.
|
4
|
+
version: 0.3.0
|
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-08 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: debugger
|
@@ -79,6 +79,7 @@ files:
|
|
79
79
|
- lib/selene/calendar_builder.rb
|
80
80
|
- lib/selene/daylight_savings_time_builder.rb
|
81
81
|
- lib/selene/event_builder.rb
|
82
|
+
- lib/selene/line.rb
|
82
83
|
- lib/selene/parser.rb
|
83
84
|
- lib/selene/standard_time_builder.rb
|
84
85
|
- lib/selene/time_zone_builder.rb
|
@@ -86,9 +87,11 @@ files:
|
|
86
87
|
- selene.gemspec
|
87
88
|
- test/fixtures/meetup.ics
|
88
89
|
- test/fixtures/meetup.json
|
90
|
+
- test/selene/alarm_builder_test.rb
|
89
91
|
- test/selene/calendar_builder_test.rb
|
90
92
|
- test/selene/daylight_savings_time_builder_test.rb
|
91
93
|
- test/selene/event_builder_test.rb
|
94
|
+
- test/selene/line_test.rb
|
92
95
|
- test/selene/parser_test.rb
|
93
96
|
- test/selene/time_zone_builder_test.rb
|
94
97
|
- test/test_helper.rb
|
@@ -106,7 +109,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
106
109
|
version: '0'
|
107
110
|
segments:
|
108
111
|
- 0
|
109
|
-
hash:
|
112
|
+
hash: -4237878709699740480
|
110
113
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
111
114
|
none: false
|
112
115
|
requirements:
|
@@ -115,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
118
|
version: '0'
|
116
119
|
segments:
|
117
120
|
- 0
|
118
|
-
hash:
|
121
|
+
hash: -4237878709699740480
|
119
122
|
requirements: []
|
120
123
|
rubyforge_project:
|
121
124
|
rubygems_version: 1.8.24
|
@@ -125,9 +128,11 @@ summary: Selene is an iCalendar parser for Ruby
|
|
125
128
|
test_files:
|
126
129
|
- test/fixtures/meetup.ics
|
127
130
|
- test/fixtures/meetup.json
|
131
|
+
- test/selene/alarm_builder_test.rb
|
128
132
|
- test/selene/calendar_builder_test.rb
|
129
133
|
- test/selene/daylight_savings_time_builder_test.rb
|
130
134
|
- test/selene/event_builder_test.rb
|
135
|
+
- test/selene/line_test.rb
|
131
136
|
- test/selene/parser_test.rb
|
132
137
|
- test/selene/time_zone_builder_test.rb
|
133
138
|
- test/test_helper.rb
|