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