selene 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|