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 CHANGED
@@ -1,18 +1,6 @@
1
1
  *.gem
2
- *.rbc
3
2
  .bundle
4
- .config
5
- .yardoc
3
+ .rvmrc
6
4
  Gemfile.lock
7
- InstalledFiles
8
- _yardoc
9
- coverage
10
5
  doc/
11
- lib/bundler/man
12
- notes.md
13
6
  pkg
14
- rdoc
15
- spec/reports
16
- test/tmp
17
- test/version_tmp
18
- tmp
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
@@ -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
- def initialize
5
- @errors = []
6
- @component = Hash.new { |component, property| component[property] = [] }
7
- end
4
+ REQUIRED_PROPERTIES = %w(prodid version)
8
5
 
9
- def parse(line)
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 initialize
5
- @component = {}
6
- end
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
- line.value
9
+ super
18
10
  end
19
11
  end
12
+
20
13
  end
21
14
  end
@@ -1,29 +1,45 @@
1
1
  module Selene
2
- class EventBuilder
2
+ class EventBuilder < ComponentBuilder
3
3
 
4
- def initialize
5
- @component = Hash.new { |component, property| component[property] = [] }
6
- end
4
+ # These properties are required
5
+ REQUIRED_PROPERTIES = %w(dtstamp uid)
7
6
 
8
- def component
9
- @component
10
- end
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
- def parse(line)
13
- component[line.name.downcase] = case line.name
14
- when 'DTSTAMP', 'DTSTART', 'DTEND'
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 'GEO'
17
- line.value.split(';')
25
+ when 'geo'
26
+ line.values
18
27
  else
19
- line.value
28
+ super
20
29
  end
21
30
  end
22
31
 
23
- def append(builder)
24
- case builder
25
- when AlarmBuilder
26
- @component['alarms'] << builder.component
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
 
@@ -0,0 +1,13 @@
1
+ module Selene
2
+ class FeedBuilder < Builder
3
+
4
+ def initialize
5
+ @component = { 'vcalendar' => [] }
6
+ end
7
+
8
+ def component
9
+ @component
10
+ end
11
+
12
+ end
13
+ end
@@ -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.upcase
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 == 'BEGIN'
51
+ name == 'begin'
52
52
  end
53
53
 
54
54
  def end_component?
55
- name == 'END'
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 ==(line)
67
- super(line) unless line.is_a?(Hash)
68
- members.all? { |key| line[key] == self.send(key) }
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
@@ -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 'VCALENDAR' then CalendarBuilder
15
- when 'VTIMEZONE' then TimeZoneBuilder
16
- when 'DAYLIGHT' then DaylightSavingsTimeBuilder
17
- when 'STANDARD' then StandardTimeBuilder
18
- when 'VEVENT' then EventBuilder
19
- when 'VALARM' then AlarmBuilder
20
- else raise "Unknown component #{component}"
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
- { 'calendars' => [] }.tap do |feed|
26
- stack = []
27
- Line.split(string).each do |line|
28
- if line.begin_component?
29
- stack << builder(line.value).new
30
- elsif line.end_component?
31
- builder = stack.pop
32
- if !stack.empty?
33
- stack[-1].append(builder)
34
- else
35
- feed['calendars'] << builder.component
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
@@ -1,3 +1,3 @@
1
1
  module Selene
2
- VERSION = "0.3.0"
2
+ VERSION = "0.3.1"
3
3
  end
@@ -1,5 +1,5 @@
1
1
  {
2
- "calendars": [
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
- "time_zones": [
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
- "events": [
39
+ "vevent": [
31
40
  {
32
41
  "dtstamp": "20121231T093631Z",
33
42
  "dtstart": [
@@ -1,7 +1,11 @@
1
1
  require 'test_helper'
2
2
 
3
3
  module Selene
4
- class AlarmBuilderTest < TestCase
4
+ class AlarmBuilderTest < MiniTest::Unit::TestCase
5
+ include BuilderTestHelper
6
+
7
+ def setup
8
+ end
5
9
 
6
10
  def builder
7
11
  @builder ||= AlarmBuilder.new
@@ -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', '', '-//Meetup//RemoteApi//EN')
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', '', '2.0')
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', '', 'Gregorian')
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', '', 'Publish')
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', '', 'http://www.google.com')
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.append(event_builder)
46
- assert_equal builder.component['events'].first['summary'], "Bluth's Best Party"
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.append(time_zone_builder)
52
- assert_equal builder.component['time_zones'].first, { 'tzid' => 'America/Detroit' }
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'], expected
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', nil, '20121231T093631Z')
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'], expected
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', nil, '20130110T183000')
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', nil, 'CONFIRMED')
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', nil, 'DetroitRuby: 2012 Plan + Lightning talks')
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', nil, 'DetroitRuby\nThursday\, January 10 at 6:30 PM\n\n')
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', nil, 'PUBLIC')
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', nil, '20120106T161509Z')
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', nil, '42.33;-83.05')
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', nil, 'Compuware Building (One Campus Martius\, Detroit\, MI 48226)')
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', nil, 'http://www.meetup.com/DetroitRuby/events/93346412/')
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', nil, '20120106T161509Z')
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', nil, 'event_qgkxkcyrcbnb@meetup.com')
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
@@ -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'), { :name => 'VERSION', :params => {}, :value => '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
- 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
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
- expected = { :name => 'DTSTART', :params => { 'tzid' => 'America/New_York' }, :value => '20130110T183000' }
21
- assert_equal Line.parse('DTSTART;TZID=America/New_York:20130110T183000'), expected
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
@@ -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(""), { 'calendars' => [] }
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"), { 'calendars' => [{ 'summary' => 'Meetups' }] }
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
- expected = JSON.parse(fixture('meetup.json'))
17
- assert_equal Selene::Parser.parse(fixture('meetup.ics')), expected
18
+ assert_equal Selene::Parser.parse(fixture('meetup.ics')),
19
+ JSON.parse(fixture('meetup.json'))
18
20
  end
19
21
  end
20
22
  end
@@ -1,7 +1,7 @@
1
1
  require 'test_helper'
2
2
 
3
3
  module Selene
4
- class TimeZoneBuilderTest < TestCase
4
+ class TimeZoneBuilderTest < MiniTest::Unit::TestCase
5
5
 
6
6
  def builder
7
7
  @builder ||= TimeZoneBuilder.new
@@ -6,36 +6,60 @@ require 'minitest/mock'
6
6
  require 'selene'
7
7
 
8
8
  module Selene
9
- class TestCase < MiniTest::Unit::TestCase
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
- end
23
- end
24
-
25
- module Stubbable
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
- # stub :method => 'value'
28
- # stub :method, 'value'
29
- # stub :method { 'value' }
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
- def stub(hash_or_method, value_or_block = nil)
32
- if hash_or_method.is_a?(Hash)
33
- hash_or_method.each { |method, value| stub method, Proc.new { value } }
34
- elsif !value_or_block.is_a?(Proc)
35
- stub hash_or_method, Proc.new { value_or_block }
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.0
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-08 00:00:00.000000000 Z
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: -4237878709699740480
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: -4237878709699740480
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