selene 0.3.1 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a94d8b8e9a90bf2658c322521edc6e6a89ccab65
4
+ data.tar.gz: d54aa750eab708bd7ea60ac21be9201073be09d0
5
+ SHA512:
6
+ metadata.gz: 525d85405335dcf90a579d1bde6d0a7c5472b96d91aa127606c0b81a19aabbc2cb2e33ab707201e884cb0d7a15c107616713f6c756a09d91763d7b2b1349529c
7
+ data.tar.gz: 70e1b7e2ce0a7651cb2c3526217107a53b092cc39dd9855d87d0358553615e56a038fe00c2ea40734aac2e3143e7a30ea9ad2c3b424b7bac261a462227a557e2
data/Gemfile CHANGED
@@ -1,2 +1,2 @@
1
- source :rubygems
1
+ source 'https://rubygems.org'
2
2
  gemspec
@@ -0,0 +1,4 @@
1
+ guard :minitest, all_on_start: false do
2
+ watch(%r|^test/(.*)\/?(.*)_test\.rb|)
3
+ watch(%r|^lib/(.*)\.rb|) { |m| "test/#{m[1]}_test.rb" }
4
+ end
data/Rakefile CHANGED
@@ -1,5 +1,4 @@
1
1
  require 'bundler/gem_tasks'
2
- require 'debugger'
3
2
  require 'json'
4
3
  require 'pp'
5
4
  require 'rake/testtask'
@@ -2,9 +2,7 @@ require 'selene/parser'
2
2
  require 'selene/version'
3
3
 
4
4
  module Selene
5
-
6
5
  def self.parse(string)
7
6
  Parser.parse(string)
8
7
  end
9
-
10
8
  end
@@ -1,4 +1,7 @@
1
1
  module Selene
2
2
  class AlarmBuilder < ComponentBuilder
3
+ def initialize
4
+ super('valarm')
5
+ end
3
6
  end
4
7
  end
@@ -1,9 +1,15 @@
1
1
  module Selene
2
2
  class CalendarBuilder < ComponentBuilder
3
+ property 'prodid', required: true, :multiple => false
4
+ property 'version', :required => true, :multiple => false
3
5
 
4
- REQUIRED_PROPERTIES = %w(prodid version)
6
+ property 'calscale', :multiple => false
7
+ property 'method', :multiple => false
5
8
 
6
- DISTINCT_PROPERTIES = %w(prodid version calscale method)
9
+ # Custom properties: x-prop, iana-prop
7
10
 
11
+ def initialize
12
+ super('vcalendar')
13
+ end
8
14
  end
9
15
  end
@@ -1,46 +1,130 @@
1
1
  module Selene
2
+ # This is the base class for all component builders.
3
+ #
4
+ # Properties are specified one per property with optional rules, e.g.:
5
+ #
6
+ # property 'version', :required => true, :multiple => false
7
+ #
8
+ # If :required is truthy, a component is not valid without that property.
9
+ # If :multiple is falsy, a component can only have one of that property
10
+ #
11
+ # Custom rules can be implemented by overriding can_add?(property) or valid?
2
12
  class ComponentBuilder
3
- include ComponentRules
13
+ class ParseError < StandardError; end
4
14
 
5
- attr_accessor :component, :parent, :errors
15
+ @property_rules = {}
6
16
 
7
- REQUIRED_PROPERTIES = []
8
- DISTINCT_PROPERTIES = []
9
- EXCLUSIVE_PROPERTIES = []
17
+ class << self
18
+ attr_accessor :property_rules
19
+ end
10
20
 
11
- def add(name, builder)
12
- @component[name.downcase] << builder.component
13
- builder.parent = self
21
+ attr_accessor :component, :errors, :name, :parent
22
+
23
+ def self.property(name, rules = {})
24
+ property_rules[name] = rules
14
25
  end
15
26
 
16
- def initialize
27
+ def self.inherited(subclass)
28
+ subclass.instance_variable_set('@property_rules', @property_rules)
29
+ end
30
+
31
+ def initialize(name)
32
+ @name = name
17
33
  @component = Hash.new { |component, property| component[property] = [] }
18
- @errors = []
19
- @property_rules = property_rules(self)
20
- @component_rules = component_rules(self)
34
+ @errors = Hash.new { |errors, property| errors[property] = [] }
21
35
  end
22
36
 
23
- def parse(line)
24
- return unless @property_rules.all? { |message, rule| rule.call(name(line), line, message) }
25
- @component[name(line)] = value(line)
37
+ def add(name, builder)
38
+ @component[name] << builder.component
26
39
  end
27
40
 
28
- def name(line)
29
- line.name
41
+ def parse(*properties)
42
+ properties.each do |property|
43
+ case property
44
+ when Line
45
+ @component[name(property)] = value(property) if can_add?(property)
46
+ when String
47
+ Line.split(property).each { |line| parse(line) }
48
+ else
49
+ raise ParseError, "Cannot parse argument of type #{property.class}"
50
+ end
51
+ end
30
52
  end
31
53
 
32
- def value(line)
33
- line.value
54
+ def to_ical(component)
55
+ lines = []
56
+ component.each_pair do |key, value|
57
+ keys = []
58
+ values = []
59
+ keys << key.upcase
60
+ case value
61
+ when Array
62
+ if value[1].is_a?(Hash)
63
+ value[1].each_pair do |pkey, pvalue|
64
+ keys << [pkey.upcase, pvalue].join('=')
65
+ end
66
+ values << value[0]
67
+ else
68
+ values += value
69
+ end
70
+ when Hash
71
+ value.each_pair do |vkey, vvalue|
72
+ values << [vkey.upcase, vvalue].join('=')
73
+ end
74
+ end
75
+
76
+ lines << [keys.join(';'), values.join(';')].join(':')
77
+ end
78
+
79
+ lines.join("\n")
34
80
  end
35
81
 
36
- def a_component_rules
37
- @component_rules
82
+ def name(property)
83
+ property.name
38
84
  end
39
85
 
40
- def valid?
41
- @component_rules.each { |message, rule| rule.call(message) }
86
+ def value(property)
87
+ property.value
88
+ end
89
+
90
+ def contains_property?(property)
91
+ @component.key?(property.to_s)
92
+ end
93
+
94
+ def can_contain?(component_name)
95
+ true
96
+ end
97
+
98
+ def error(property, message)
99
+ @errors[property] << message
100
+ end
101
+
102
+ def properties
103
+ self.class.property_rules.keys
104
+ end
105
+
106
+ def required?(property)
107
+ self.class.property_rules[property][:required]
108
+ end
109
+
110
+ def multiple?(property)
111
+ self.class.property_rules[property][:multiple]
112
+ end
113
+
114
+ def can_add?(property)
115
+ if contains_property?(property.name) && !multiple?(property.name)
116
+ error(property.name, "property '%s' must not occur more than once" % property.name)
117
+ end
42
118
  @errors.empty?
43
119
  end
44
120
 
121
+ def valid?
122
+ properties.select { |property| required?(property) }.each do |property|
123
+ if !contains_property?(property)
124
+ error(property, "missing required property '%s'" % property)
125
+ end
126
+ end
127
+ @errors.empty?
128
+ end
45
129
  end
46
130
  end
@@ -0,0 +1,7 @@
1
+ module ComponentErrors
2
+
3
+ def containment_error(line)
4
+
5
+ end
6
+
7
+ end
@@ -1,5 +1,8 @@
1
1
  module Selene
2
2
  class DaylightSavingsTimeBuilder < ComponentBuilder
3
+ def initialize
4
+ super('daylight')
5
+ end
3
6
 
4
7
  def value(line)
5
8
  case line.name
@@ -9,6 +12,5 @@ module Selene
9
12
  super
10
13
  end
11
14
  end
12
-
13
15
  end
14
16
  end
@@ -1,29 +1,80 @@
1
1
  module Selene
2
2
  class EventBuilder < ComponentBuilder
3
3
 
4
- # These properties are required
5
- REQUIRED_PROPERTIES = %w(dtstamp uid)
4
+ # Required properties
5
+ property 'dtstamp', required: true, multiple: false
6
+ property 'uid', required: true, multiple: false
6
7
 
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)
8
+ # Optional properties
9
+ property 'class', multiple: false
10
+ property 'created', multiple: false
11
+ property 'description', multiple: false
12
+ property 'dtend', multiple: false
13
+ property 'dtstart', multiple: false
14
+ property 'duration', multiple: false
15
+ property 'geo', multiple: false
16
+ property 'last-mod', multiple: false
17
+ property 'location', multiple: false
18
+ property 'organizer', multiple: false
19
+ property 'priority', multiple: false
20
+ property 'recurid', multiple: false
21
+ property 'rrule' # The rrule property should not occur more than once (but can if necessary)
22
+ property 'seq', multiple: false
23
+ property 'status', multiple: false
24
+ property 'summary', multiple: false
25
+ property 'transp', multiple: false
26
+ property 'url', multiple: false
27
+ property 'attach'
28
+ property 'attendee'
29
+ property 'categories'
30
+ property 'comment'
31
+ property 'contact'
32
+ property 'exdate'
33
+ property 'rdate'
34
+ property 'related'
35
+ property 'related-to'
36
+ property 'resources'
37
+ property 'rstatus'
11
38
 
12
- # These properties must not occur together in the same component
13
- EXCLUSIVE_PROPERTIES = [
14
- %w(dtend duration)
15
- ]
39
+ # Custom properties: x-prop, iana-prop
16
40
 
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'
41
+ # TODO: if dtstart is a date, dtend must be as well
42
+ # TODO: multi-day durations must be 'dur-day' or 'dur-week'
20
43
 
21
- def value(line)
22
- case line.name
44
+ def self.update(component, properties)
45
+ new('vevent', component).update_properties(properties)
46
+ end
47
+
48
+ def initialize
49
+ super('vevent')
50
+ end
51
+
52
+ def update_properties(properties)
53
+ properties.inject(self.component) do |component, (name, value)|
54
+ component = update_property(component, name, value)
55
+ component
56
+ end
57
+ end
58
+
59
+ def update_property(component, name, value)
60
+ case name
61
+ when 'dtstart'
62
+ component.merge('dtstart' => [value.strftime('%Y%m%dT%H%M%S'), component['dtstart'][1]])
63
+ else
64
+ component
65
+ end
66
+ end
67
+
68
+ def value(property)
69
+ case property.name
23
70
  when 'dtstamp', 'dtstart', 'dtend'
24
- line.value_with_params
71
+ property.value_with_params
72
+ when 'exdate'
73
+ property.values_with_params
25
74
  when 'geo'
26
- line.values
75
+ property.values
76
+ when 'rrule'
77
+ property.rrule
27
78
  else
28
79
  super
29
80
  end
@@ -34,14 +85,20 @@ module Selene
34
85
  super(builder)
35
86
  end
36
87
 
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
88
+ def can_add?(property)
89
+ if property.name == 'dtend' && contains_property?('duration')
90
+ error('dtend', "The 'dtend' property cannot be set if the 'duration' property already exists")
91
+ elsif property.name == 'duration' && contains_property?('dtend')
92
+ error('duration', "The 'duration' property cannot be set if the 'dtend' property already exists")
43
93
  end
94
+ super(property)
44
95
  end
45
96
 
97
+ def valid?
98
+ if !contains_property?('dtstart') && !parent.contains_property?('method')
99
+ error('dtstart', "The 'dtstart' property is required if the calendar does not have a 'method' property")
100
+ end
101
+ super
102
+ end
46
103
  end
47
104
  end
@@ -1,13 +1,15 @@
1
1
  module Selene
2
- class FeedBuilder < Builder
3
-
2
+ class FeedBuilder < ComponentBuilder
4
3
  def initialize
5
- @component = { 'vcalendar' => [] }
4
+ super('feed')
6
5
  end
7
6
 
8
- def component
9
- @component
7
+ def can_contain?(builder)
8
+ !%w(vevent vtimezone valarm standard daylight).include?(builder.name)
10
9
  end
11
10
 
11
+ def error(message)
12
+ @component['errors'] << message
13
+ end
12
14
  end
13
15
  end
@@ -1,5 +1,5 @@
1
1
  module Selene
2
- class Line < Struct.new(:name, :params, :value)
2
+ class Line < Struct.new(:name, :value, :params)
3
3
 
4
4
  # Match everything until we hit ';' (parameter separator) or ':' (value separator)
5
5
  NAME = /(?<name>[^:\;]+)/
@@ -13,7 +13,7 @@ module Selene
13
13
  # Match everything that is not ';' (parameter separator)
14
14
  PARAM = /[^\;]+/
15
15
 
16
- # Match everything before '='
16
+ # Match parameter key and value
17
17
  PARAM_KEY_VALUE = /(?<key>[^=]+)=(?<value>.*)/
18
18
 
19
19
  # Split a string into content lines
@@ -25,7 +25,7 @@ module Selene
25
25
  # Parse a content line into a line object
26
26
  def self.parse(content_line)
27
27
  content_line.match(/#{NAME}#{PARAMS}?:#{VALUE}/) do |match|
28
- return new(match[:name], parse_params(match[:params]), match[:value])
28
+ return new(match[:name], match[:value], parse_params(match[:params]))
29
29
  end
30
30
  end
31
31
 
@@ -41,35 +41,40 @@ module Selene
41
41
  end
42
42
  end
43
43
 
44
- def initialize(name, params, value)
45
- self.name = name.downcase
46
- self.params = params || {}
47
- self.value = value
44
+ def initialize(name, value, params = {})
45
+ super(name.downcase, value, params)
48
46
  end
49
47
 
50
48
  def begin_component?
51
49
  name == 'begin'
52
50
  end
53
51
 
52
+ def component_name
53
+ value.downcase
54
+ end
55
+
54
56
  def end_component?
55
57
  name == 'end'
56
58
  end
57
59
 
58
60
  def params?
59
- params && !params.empty?
61
+ !params.empty?
60
62
  end
61
63
 
62
64
  def value_with_params
63
65
  params? ? [value, params] : value
64
66
  end
65
67
 
68
+ def values_with_params
69
+ params? ? [values, params] : values
70
+ end
71
+
66
72
  def rrule
67
73
  Hash[values.map { |values| k, v = values.split('=', 2); [k.downcase, v] }]
68
74
  end
69
75
 
70
76
  def values
71
- value.split(';')
77
+ value.split(/[;,]/)
72
78
  end
73
-
74
79
  end
75
80
  end
@@ -1,36 +1,31 @@
1
1
  require 'selene/line'
2
- require 'selene/component_rules'
3
- require 'selene/component_builder'
4
2
 
3
+ require 'selene/component_builder'
5
4
  require 'selene/alarm_builder'
6
5
  require 'selene/calendar_builder'
7
6
  require 'selene/daylight_savings_time_builder'
8
7
  require 'selene/event_builder'
8
+ require 'selene/feed_builder'
9
9
  require 'selene/standard_time_builder'
10
10
  require 'selene/time_zone_builder'
11
11
 
12
12
  module Selene
13
- module Parser
13
+ class Parser
14
+ def self.parse(string)
15
+ new(string).parse
16
+ end
14
17
 
15
- def self.builder(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
24
- end
18
+ def initialize(string)
19
+ @string = string
25
20
  end
26
21
 
27
- def self.parse(string)
28
- stack = []
29
- stack << builder('feed').new
30
- Line.split(string).each do |line|
22
+ def parse
23
+ feed = FeedBuilder.new
24
+ stack = [feed]
25
+ Line.split(@string).each do |line|
31
26
  if line.begin_component?
32
- builder = builder(line.value).new
33
- stack[-1].add(line.value, builder) unless stack.empty?
27
+ builder = create_builder(line.component_name)
28
+ stack[-1].add(line.component_name, builder)
34
29
  stack << builder
35
30
  elsif line.end_component?
36
31
  stack.pop
@@ -38,8 +33,19 @@ module Selene
38
33
  stack[-1].parse(line)
39
34
  end
40
35
  end
41
- stack[-1].component
36
+ feed.component
42
37
  end
43
38
 
39
+ def create_builder(name)
40
+ case name
41
+ when 'daylight' then DaylightSavingsTimeBuilder.new
42
+ when 'standard' then StandardTimeBuilder.new
43
+ when 'valarm' then AlarmBuilder.new
44
+ when 'vcalendar' then CalendarBuilder.new
45
+ when 'vevent' then EventBuilder.new
46
+ when 'vtimezone' then TimeZoneBuilder.new
47
+ else ComponentBuilder.new(name)
48
+ end
49
+ end
44
50
  end
45
51
  end
@@ -1,4 +1,7 @@
1
1
  module Selene
2
2
  class StandardTimeBuilder < ComponentBuilder
3
+ def initialize
4
+ super('standard')
5
+ end
3
6
  end
4
7
  end
@@ -1,4 +1,7 @@
1
1
  module Selene
2
2
  class TimeZoneBuilder < ComponentBuilder
3
+ def initialize
4
+ super('vtimezone')
5
+ end
3
6
  end
4
7
  end
@@ -1,3 +1,3 @@
1
1
  module Selene
2
- VERSION = "0.3.1"
2
+ VERSION = "0.3.4"
3
3
  end
@@ -17,7 +17,11 @@ Gem::Specification.new do |gem|
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
  gem.require_paths = ["lib"]
19
19
 
20
- gem.add_development_dependency 'debugger'
21
- gem.add_development_dependency 'minitest-colorize'
20
+ gem.add_dependency 'ice_cube'
21
+
22
+ gem.add_development_dependency 'byebug'
23
+ gem.add_development_dependency 'minitest-reporters'
22
24
  gem.add_development_dependency 'rake'
25
+ gem.add_development_dependency 'guard'
26
+ gem.add_development_dependency 'guard-minitest'
23
27
  end
@@ -0,0 +1,6 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/reporters'
3
+
4
+ Minitest::Reporters.use!
5
+
6
+ require_relative 'support/fixtures_helper'
@@ -1,20 +1,17 @@
1
1
  require 'test_helper'
2
2
 
3
3
  module Selene
4
- class AlarmBuilderTest < MiniTest::Unit::TestCase
4
+ class AlarmBuilderTest < MiniTest::Test
5
5
  include BuilderTestHelper
6
6
 
7
- def setup
7
+ def test_parses_action
8
+ builder.parse(Line.new('ACTION', 'AUDIO'))
9
+ assert_equal builder.component['action'], 'AUDIO'
8
10
  end
9
11
 
12
+ private
10
13
  def builder
11
14
  @builder ||= AlarmBuilder.new
12
15
  end
13
-
14
- def test_parses_action
15
- parse_line 'ACTION', {}, 'AUDIO'
16
- assert_equal builder.component['action'], 'AUDIO'
17
- end
18
-
19
16
  end
20
17
  end
@@ -1,70 +1,36 @@
1
1
  require 'test_helper'
2
2
 
3
3
  module Selene
4
- class CalendarBuilderTest < MiniTest::Unit::TestCase
4
+ class CalendarBuilderTest < MiniTest::Test
5
5
  include BuilderTestHelper
6
6
 
7
- def builder
8
- @builder ||= CalendarBuilder.new
9
- end
10
-
11
- def event_builder
12
- @event_builder ||= EventBuilder.new.extend(Stubbable)
13
- end
14
-
15
- def time_zone_builder
16
- @time_zone_builder ||= TimeZoneBuilder.new.extend(Stubbable)
17
- end
18
-
19
- def test_parse_prodid
20
- parse_line('PRODID', {}, '-//Meetup//RemoteApi//EN')
21
- assert_equal builder.component['prodid'], '-//Meetup//RemoteApi//EN'
22
- end
23
-
24
- def test_parse_version
25
- parse_line('VERSION', {}, '2.0')
26
- assert_equal @builder.component['version'], '2.0'
27
- end
28
-
29
- def test_parse_calscale
30
- parse_line('CALSCALE', {}, 'Gregorian')
31
- assert_equal builder.component['calscale'], 'Gregorian'
32
- end
33
-
34
- def test_parse_method
35
- parse_line('METHOD', {}, 'Publish')
36
- assert_equal builder.component['method'], 'Publish'
37
- end
38
-
39
- def test_parse_x_prop
40
- parse_line('X-ORIGINAL-URL', {}, 'http://www.google.com')
41
- assert_equal builder.component['x-original-url'], 'http://www.google.com'
42
- end
43
-
44
7
  def test_append_event_builder
8
+ builder = CalendarBuilder.new
9
+ event_builder = EventBuilder.new.extend(Stubbable)
45
10
  event_builder.stub :component, { 'summary' => "Bluth's Best Party" }
46
11
  builder.add('vevent', event_builder)
47
12
  assert_equal builder.component['vevent'].first['summary'], "Bluth's Best Party"
48
13
  end
49
14
 
50
15
  def test_append_time_zone_builder
16
+ builder = CalendarBuilder.new
17
+ time_zone_builder = TimeZoneBuilder.new.extend(Stubbable)
51
18
  time_zone_builder.stub :component, { 'tzid' => 'America/Detroit' }
52
19
  builder.add('vtimezone', time_zone_builder)
53
20
  assert_equal builder.component['vtimezone'].first, { 'tzid' => 'America/Detroit' }
54
21
  end
55
22
 
56
- # Validation
57
-
23
+ # Test properties with :required => true
58
24
  %w(prodid version).each do |property|
59
25
  define_method "test_#{property}_required" do
60
- assert_required property
26
+ assert_required(CalendarBuilder.new, property)
61
27
  end
62
28
  end
63
29
 
30
+ # Test properties with :multiple => false
64
31
  %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
32
+ define_method "test_#{property}_cannot_be_defined_more_than_once" do
33
+ assert_single(CalendarBuilder.new, property)
68
34
  end
69
35
  end
70
36
 
@@ -0,0 +1,10 @@
1
+ require 'test_helper'
2
+
3
+ module Selene
4
+ class ComponentBuilderTest < MiniTest::Test
5
+ def test_sets_property_rules
6
+ ComponentBuilder.property :version, :required => true
7
+ assert_equal({ :required => true }, ComponentBuilder.property_rules[:version])
8
+ end
9
+ end
10
+ end
@@ -1,7 +1,7 @@
1
1
  require 'test_helper'
2
2
 
3
3
  module Selene
4
- class DaylightSavingsTimeBuilderTest < MiniTest::Unit::TestCase
4
+ class DaylightSavingsTimeBuilderTest < MiniTest::Test
5
5
  include BuilderTestHelper
6
6
 
7
7
  def builder
@@ -9,9 +9,8 @@ module Selene
9
9
  end
10
10
 
11
11
  def test_parses_rrule
12
- parse_line('RRULE', '', 'FREQ=YEARLY;BYMONTH=3;BYDAY=2SU')
13
- assert_equal builder.component['rrule'], { 'freq' => 'YEARLY', 'bymonth' => '3', 'byday' => '2SU' }
12
+ builder.parse(Line.new('RRULE', 'FREQ=YEARLY;BYMONTH=3;BYDAY=2SU'))
13
+ assert_equal({ 'freq' => 'YEARLY', 'bymonth' => '3', 'byday' => '2SU' }, builder.component['rrule'])
14
14
  end
15
-
16
15
  end
17
16
  end
@@ -1,121 +1,73 @@
1
1
  require 'test_helper'
2
2
 
3
3
  module Selene
4
- class EventBuilderTest < MiniTest::Unit::TestCase
4
+ class EventBuilderTest < MiniTest::Test
5
5
  include BuilderTestHelper
6
6
 
7
- def builder
8
- @builder ||= EventBuilder.new
9
- end
10
-
11
- def builder_with_parent
12
- builder.tap { |b| b.parent = CalendarBuilder.new }
13
- end
14
-
15
- def test_parses_dtstart
16
- parse_line('DTSTART', { 'tzid' => 'America/New_York' }, '20130110T183000')
17
- assert_equal builder.component['dtstart'],
18
- ['20130110T183000', { 'tzid' => 'America/New_York' }]
19
- end
20
-
21
- def test_parses_dtstamp
22
- parse_line('DTSTAMP', {}, '20121231T093631Z')
23
- assert_equal builder.component['dtstamp'], '20121231T093631Z'
24
- end
25
-
26
- def test_parses_dtstart_without_tzid
27
- parse_line('DTSTART', {}, '20130110T183000')
28
- assert_equal builder.component['dtstart'], '20130110T183000'
29
- end
30
-
31
- def test_parses_dtend
32
- parse_line('DTEND', { 'tzid' => 'America/New_York' }, '20130110T183000')
33
- assert_equal builder.component['dtend'], ['20130110T183000', { 'tzid' => 'America/New_York' }]
34
- end
35
-
36
- def test_parses_dtstart_without_tzid
37
- parse_line('DTEND', {}, '20130110T183000')
38
- assert_equal builder.component['dtend'], '20130110T183000'
39
- end
40
-
41
- def test_parses_status
42
- parse_line('STATUS', {}, 'CONFIRMED')
43
- assert_equal builder.component['status'], 'CONFIRMED'
44
- end
45
-
46
- def test_parses_summary
47
- parse_line('SUMMARY', {}, 'DetroitRuby: 2012 Plan + Lightning talks')
48
- assert_equal builder.component['summary'], 'DetroitRuby: 2012 Plan + Lightning talks'
49
- end
50
-
51
- def test_parses_description
52
- parse_line('DESCRIPTION', {}, 'DetroitRuby\nThursday\, January 10 at 6:30 PM\n\n')
53
- assert_equal builder.component['description'], 'DetroitRuby\nThursday\, January 10 at 6:30 PM\n\n'
54
- end
55
-
56
- def test_parses_class
57
- parse_line('CLASS', {}, 'PUBLIC')
58
- assert_equal builder.component['class'], 'PUBLIC'
59
- end
60
-
61
- def test_parses_created
62
- parse_line('CREATED', {}, '20120106T161509Z')
63
- assert_equal builder.component['created'], '20120106T161509Z'
7
+ # Test properties that can contain property parameters
8
+ %w(dtstamp dtstart dtend).each do |property|
9
+ define_method "test_parses_#{property}_with_property_parameters" do
10
+ builder = EventBuilder.new
11
+ builder.parse(Line.new(property.upcase, '20130110T183000', 'tzid' => 'America/New_York'))
12
+ assert_equal ['20130110T183000', { 'tzid' => 'America/New_York' }],
13
+ builder.component[property]
14
+ end
64
15
  end
65
16
 
66
- def test_parses_geo
67
- parse_line('GEO', {}, '42.33;-83.05')
17
+ def test_parses_geo_with_multiple_values
18
+ builder = EventBuilder.new
19
+ builder.parse(Line.new('GEO', '42.33;-83.05'))
68
20
  assert_equal builder.component['geo'], ['42.33', '-83.05']
69
21
  end
70
22
 
71
- def test_parses_location
72
- parse_line('LOCATION', {}, 'Compuware Building (One Campus Martius\, Detroit\, MI 48226)')
73
- assert_equal builder.component['location'], 'Compuware Building (One Campus Martius\, Detroit\, MI 48226)'
74
- end
75
-
76
- def test_parses_url
77
- parse_line('URL', {}, 'http://www.meetup.com/DetroitRuby/events/93346412/')
78
- assert_equal builder.component['url'], 'http://www.meetup.com/DetroitRuby/events/93346412/'
79
- end
80
-
81
- def test_parses_last_modified
82
- parse_line('LAST-MODIFIED', {}, '20120106T161509Z')
83
- assert_equal builder.component['last-modified'], '20120106T161509Z'
84
- end
85
-
86
- def test_parses_uid
87
- parse_line('UID', {}, 'event_qgkxkcyrcbnb@meetup.com')
88
- assert_equal builder.component['uid'], 'event_qgkxkcyrcbnb@meetup.com'
89
- end
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
-
23
+ # Test properties with :required => true
97
24
  %w(dtstamp uid).each do |property|
98
25
  define_method "test_#{property}_required" do
99
- assert_required property
26
+ builder = EventBuilder.new
27
+ builder.parent = CalendarBuilder.new
28
+ assert_required(builder, property)
100
29
  end
101
30
  end
102
31
 
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
32
+ # Test properties with :multiple => false
33
+ %w(dtstamp uid dtstart class created description geo last-mod location
34
+ organizer priority seq status summary transp url recurid).each do |property|
35
+ define_method "test_#{property}_cannot_be_defined_more_than_once" do
36
+ assert_single(EventBuilder.new, property)
107
37
  end
108
38
  end
109
39
 
110
- def test_adding_to_non_calendar_raises_exception
40
+ def test_invalid_parent
111
41
  assert_raises Exception do
112
- builder.parent = TimeZoneBuilder.new
42
+ EventBuilder.new.parent = TimeZoneBuilder.new
113
43
  end
114
44
  end
115
45
 
116
46
  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" }
47
+ builder = EventBuilder.new
48
+ builder.parent = CalendarBuilder.new
49
+ builder.valid?
50
+ assert_error builder, 'dtstart', "The 'dtstart' property is required if the calendar does not have a 'method' property"
51
+ end
52
+
53
+ def test_dtend_invalid_if_duration
54
+ builder = EventBuilder.new
55
+ builder.parse(Line.new('DURATION', {}, 'PT15M'))
56
+ builder.parse(Line.new('DTEND', {}, '19970903T190000Z'))
57
+ assert_error builder, 'dtend', "The 'dtend' property cannot be set if the 'duration' property already exists"
58
+ end
59
+
60
+ def test_duration_invalid_if_dtend
61
+ builder = EventBuilder.new
62
+ builder.parse(Line.new('DTEND', {}, '19970903T190000Z'))
63
+ builder.parse(Line.new('DURATION', {}, 'PT15M'))
64
+ assert_error builder, 'duration', "The 'duration' property cannot be set if the 'dtend' property already exists"
65
+ end
66
+
67
+ def test_exdate
68
+ builder = EventBuilder.new
69
+ builder.parse("EXDATE;VALUE=DATE-TIME;TZID=America/Detroit:19960402T010000,19960403T010000,19960404T010000")
70
+ assert_equal builder.component['exdate'], [['19960402T010000','19960403T010000','19960404T010000'], { 'value' => 'DATE-TIME', 'tzid' => 'America/Detroit' }]
119
71
  end
120
72
  end
121
73
  end
@@ -1,24 +1,24 @@
1
1
  require 'test_helper'
2
+ require 'selene/line'
2
3
 
3
4
  module Selene
4
- class LineTest < MiniTest::Unit::TestCase
5
-
5
+ class LineTest < MiniTest::Test
6
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"
7
+ assert_equal Line.new('WHAT', "Say it ain't so"), Line.split("WHAT:Say it\r\n ain't so").first
8
8
  end
9
9
 
10
10
  def test_parses_content_line
11
- assert_equal Line.parse('VERSION:2.0'), Line.new('VERSION', {}, '2.0')
11
+ assert_equal Line.new('VERSION', '2.0'), Line.parse('VERSION:2.0')
12
12
  end
13
13
 
14
14
  def test_parses_url
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/')
15
+ assert_equal Line.new('TZURL', 'https://townstage.com'),
16
+ Line.parse('TZURL:https://townstage.com')
17
17
  end
18
18
 
19
19
  def test_parses_params
20
20
  assert_equal Line.parse('DTSTART;TZID=America/New_York:20130110T183000'),
21
- Line.new('DTSTART', { 'tzid' => 'America/New_York' }, '20130110T183000')
21
+ Line.new('DTSTART', '20130110T183000', 'tzid' => 'America/New_York')
22
22
  end
23
23
  end
24
24
  end
@@ -1,22 +1,29 @@
1
- require 'test_helper'
1
+ require_relative '../test_helper'
2
2
  require 'json'
3
+ require 'selene/parser'
3
4
 
4
5
  module Selene
5
- class ParserTest < MiniTest::Unit::TestCase
6
+ class ParserTest < MiniTest::Test
6
7
  include FixtureHelper
7
8
 
8
9
  def test_parses_blank_string
9
- assert_equal Selene::Parser.parse(""), {}
10
+ assert_equal Parser.parse(""), {}
10
11
  end
11
12
 
12
13
  def test_parses_simple_calendar
13
- assert_equal Selene::Parser.parse("BEGIN:VCALENDAR\r\nSUMMARY:Meetups\r\nEND:VCALENDAR"),
14
+ assert_equal Parser.parse("BEGIN:VCALENDAR\r\nSUMMARY:Meetups\r\nEND:VCALENDAR"),
14
15
  { 'vcalendar' => [{ 'summary' => 'Meetups' }] }
15
16
  end
16
17
 
17
18
  def test_parses_meetup_calendar
18
- assert_equal Selene::Parser.parse(fixture('meetup.ics')),
19
+ assert_equal Parser.parse(fixture('meetup.ics')),
19
20
  JSON.parse(fixture('meetup.json'))
20
21
  end
22
+
23
+ def test_contain_error
24
+ skip
25
+ ical = Parser.parse("BEGIN:VEVENT\r\nEND:VEVENT")
26
+ assert ical['errors'].any? { |e| e.include? "can't contain" }, "Feed can't contain a vevent"
27
+ end
21
28
  end
22
29
  end
@@ -1,7 +1,7 @@
1
1
  require 'test_helper'
2
2
 
3
3
  module Selene
4
- class TimeZoneBuilderTest < MiniTest::Unit::TestCase
4
+ class TimeZoneBuilderTest < MiniTest::Test
5
5
 
6
6
  def builder
7
7
  @builder ||= TimeZoneBuilder.new
@@ -1,10 +1,11 @@
1
1
  require 'bundler/setup'
2
- require 'debugger'
3
2
  require 'minitest/autorun'
4
- require 'minitest/colorize'
3
+ require 'minitest/reporters'
5
4
  require 'minitest/mock'
6
5
  require 'selene'
7
6
 
7
+ Minitest::Reporters.use! Minitest::Reporters::DefaultReporter.new
8
+
8
9
  module Selene
9
10
  module Stubbable
10
11
 
@@ -34,32 +35,21 @@ module Selene
34
35
 
35
36
  module BuilderTestHelper
36
37
 
37
- # This is a helper method that takes a name, params and a value, turns them into a proper line hash,
38
- # and passes them to the builder to be parsed.
39
- def parse_line(name, params, value)
40
- builder.parse(Line.new(name, params, value))
41
- end
42
-
43
- def assert_required property
38
+ def assert_required builder, property
44
39
  builder.valid?
45
- message = "missing required property '#{property}'"
46
- assert builder.errors.any? { |e| e[:message] =~ /#{message}/ }, message
40
+ assert_error builder, property, "missing required property '#{property}'"
47
41
  end
48
42
 
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}"
43
+ def assert_single builder, property
44
+ builder.parse(Line.new(property, 'First Value'))
45
+ original_value = builder.component[property]
46
+ builder.parse(Line.new(property, 'Second Value'))
47
+ assert_error builder, property, "property '#{property}' must not occur more than once"
48
+ assert_equal original_value, builder.component[property]
55
49
  end
56
50
 
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
51
+ def assert_error builder, property, message
52
+ assert builder.errors[property].any? { |e| e =~ /#{message}/ }, "#{builder.class.name}: #{message}"
62
53
  end
63
-
64
54
  end
65
55
  end
metadata CHANGED
@@ -1,62 +1,97 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: selene
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
5
- prerelease:
4
+ version: 0.3.4
6
5
  platform: ruby
7
6
  authors:
8
7
  - Cory Kaufman-Schofield
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-01-15 00:00:00.000000000 Z
11
+ date: 2015-03-08 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
- name: debugger
14
+ name: ice_cube
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
- - - ! '>='
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
20
32
  - !ruby/object:Gem::Version
21
33
  version: '0'
22
34
  type: :development
23
35
  prerelease: false
24
36
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
37
  requirements:
27
- - - ! '>='
38
+ - - '>='
28
39
  - !ruby/object:Gem::Version
29
40
  version: '0'
30
41
  - !ruby/object:Gem::Dependency
31
- name: minitest-colorize
42
+ name: minitest-reporters
32
43
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
44
  requirements:
35
- - - ! '>='
45
+ - - '>='
36
46
  - !ruby/object:Gem::Version
37
47
  version: '0'
38
48
  type: :development
39
49
  prerelease: false
40
50
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
51
  requirements:
43
- - - ! '>='
52
+ - - '>='
44
53
  - !ruby/object:Gem::Version
45
54
  version: '0'
46
55
  - !ruby/object:Gem::Dependency
47
56
  name: rake
48
57
  requirement: !ruby/object:Gem::Requirement
49
- none: false
50
58
  requirements:
51
- - - ! '>='
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: guard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: guard-minitest
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
52
88
  - !ruby/object:Gem::Version
53
89
  version: '0'
54
90
  type: :development
55
91
  prerelease: false
56
92
  version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
93
  requirements:
59
- - - ! '>='
94
+ - - '>='
60
95
  - !ruby/object:Gem::Version
61
96
  version: '0'
62
97
  description: Selene is an iCalendar parser for Ruby
@@ -68,8 +103,8 @@ extra_rdoc_files: []
68
103
  files:
69
104
  - .gitattributes
70
105
  - .gitignore
71
- - .rspec
72
106
  - Gemfile
107
+ - Guardfile
73
108
  - LICENSE.txt
74
109
  - README.md
75
110
  - Rakefile
@@ -77,7 +112,7 @@ files:
77
112
  - lib/selene/alarm_builder.rb
78
113
  - lib/selene/calendar_builder.rb
79
114
  - lib/selene/component_builder.rb
80
- - lib/selene/component_rules.rb
115
+ - lib/selene/component_errors.rb
81
116
  - lib/selene/daylight_savings_time_builder.rb
82
117
  - lib/selene/event_builder.rb
83
118
  - lib/selene/feed_builder.rb
@@ -89,8 +124,10 @@ files:
89
124
  - selene.gemspec
90
125
  - test/fixtures/meetup.ics
91
126
  - test/fixtures/meetup.json
127
+ - test/helper.rb
92
128
  - test/selene/alarm_builder_test.rb
93
129
  - test/selene/calendar_builder_test.rb
130
+ - test/selene/component_builder_test.rb
94
131
  - test/selene/daylight_savings_time_builder_test.rb
95
132
  - test/selene/event_builder_test.rb
96
133
  - test/selene/line_test.rb
@@ -99,39 +136,34 @@ files:
99
136
  - test/test_helper.rb
100
137
  homepage: https://github.com/allspiritseve/selene
101
138
  licenses: []
139
+ metadata: {}
102
140
  post_install_message:
103
141
  rdoc_options: []
104
142
  require_paths:
105
143
  - lib
106
144
  required_ruby_version: !ruby/object:Gem::Requirement
107
- none: false
108
145
  requirements:
109
- - - ! '>='
146
+ - - '>='
110
147
  - !ruby/object:Gem::Version
111
148
  version: '0'
112
- segments:
113
- - 0
114
- hash: 500821961400754664
115
149
  required_rubygems_version: !ruby/object:Gem::Requirement
116
- none: false
117
150
  requirements:
118
- - - ! '>='
151
+ - - '>='
119
152
  - !ruby/object:Gem::Version
120
153
  version: '0'
121
- segments:
122
- - 0
123
- hash: 500821961400754664
124
154
  requirements: []
125
155
  rubyforge_project:
126
- rubygems_version: 1.8.24
156
+ rubygems_version: 2.4.2
127
157
  signing_key:
128
- specification_version: 3
158
+ specification_version: 4
129
159
  summary: Selene is an iCalendar parser for Ruby
130
160
  test_files:
131
161
  - test/fixtures/meetup.ics
132
162
  - test/fixtures/meetup.json
163
+ - test/helper.rb
133
164
  - test/selene/alarm_builder_test.rb
134
165
  - test/selene/calendar_builder_test.rb
166
+ - test/selene/component_builder_test.rb
135
167
  - test/selene/daylight_savings_time_builder_test.rb
136
168
  - test/selene/event_builder_test.rb
137
169
  - test/selene/line_test.rb
data/.rspec DELETED
@@ -1,2 +0,0 @@
1
- --color
2
- --format progress
@@ -1,38 +0,0 @@
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