selene 0.3.4 → 0.3.5
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.
- checksums.yaml +5 -5
- data/LICENSE.txt +2 -2
- data/lib/selene/component_builder.rb +9 -55
- data/lib/selene/component_validator.rb +64 -0
- data/lib/selene/event.rb +28 -0
- data/lib/selene/event_builder.rb +3 -3
- data/lib/selene/line.rb +11 -7
- data/lib/selene/parser.rb +9 -5
- data/lib/selene/version.rb +1 -1
- data/test/selene/calendar_builder_test.rb +1 -2
- data/test/selene/component_builder_test.rb +1 -1
- data/test/selene/line_test.rb +2 -2
- data/test/selene/parser_test.rb +3 -5
- data/test/test_helper.rb +3 -3
- metadata +21 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a7327895d75318126155ef90f53ed6947798227f92066d61d82b38e9a7189d20
|
4
|
+
data.tar.gz: abd28599b4fdd3fd5d53f5b1dfe7797a6062421503ac9a1ef3939db97c7e355e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 80738a49e57cb5346990d8c48598aea5234b46129839273d2ec10045c8fab4b246367b4ddff15ff98727f3548bee00957d66863d47ec76ce24235e24d1f432eb
|
7
|
+
data.tar.gz: efda1236b74295332b7d01e3f27b1c44cb5ee93ca5b6ce94783d1145bad094537fc4474a9c5941211ef0937e8126870e6a3658ebec2e3e2d07dc1500f6ee70cf
|
data/LICENSE.txt
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
Copyright (c) 2012 Cory Kaufman-Schofield
|
1
|
+
Copyright (c) 2012-2019 Cory Kaufman-Schofield
|
2
2
|
|
3
3
|
MIT License
|
4
4
|
|
@@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
19
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
20
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
21
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -1,37 +1,27 @@
|
|
1
|
+
require 'selene/component_validator'
|
2
|
+
|
1
3
|
module Selene
|
2
4
|
# This is the base class for all component builders.
|
3
5
|
#
|
4
6
|
# Properties are specified one per property with optional rules, e.g.:
|
5
7
|
#
|
6
|
-
# property 'version', :
|
8
|
+
# property 'version', required: true, multiple: false
|
7
9
|
#
|
8
10
|
# If :required is truthy, a component is not valid without that property.
|
9
11
|
# If :multiple is falsy, a component can only have one of that property
|
10
12
|
#
|
11
13
|
# Custom rules can be implemented by overriding can_add?(property) or valid?
|
12
14
|
class ComponentBuilder
|
13
|
-
|
15
|
+
include ComponentValidator
|
14
16
|
|
15
|
-
|
16
|
-
|
17
|
-
class << self
|
18
|
-
attr_accessor :property_rules
|
19
|
-
end
|
17
|
+
class ParseError < StandardError; end
|
20
18
|
|
21
19
|
attr_accessor :component, :errors, :name, :parent
|
22
20
|
|
23
|
-
def self.property(name, rules = {})
|
24
|
-
property_rules[name] = rules
|
25
|
-
end
|
26
|
-
|
27
|
-
def self.inherited(subclass)
|
28
|
-
subclass.instance_variable_set('@property_rules', @property_rules)
|
29
|
-
end
|
30
|
-
|
31
21
|
def initialize(name)
|
32
22
|
@name = name
|
33
|
-
@component = Hash.new { |
|
34
|
-
@errors = Hash.new { |
|
23
|
+
@component = Hash.new { |h, k| h[k] = [] }
|
24
|
+
@errors = Hash.new { |h, k| h[k] = [] }
|
35
25
|
end
|
36
26
|
|
37
27
|
def add(name, builder)
|
@@ -87,44 +77,8 @@ module Selene
|
|
87
77
|
property.value
|
88
78
|
end
|
89
79
|
|
90
|
-
def
|
91
|
-
@component.key?(property
|
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
|
118
|
-
@errors.empty?
|
119
|
-
end
|
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?
|
80
|
+
def contains?(property)
|
81
|
+
@component.key?(property)
|
128
82
|
end
|
129
83
|
end
|
130
84
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Selene
|
2
|
+
module ComponentValidator
|
3
|
+
module ClassMethods
|
4
|
+
def properties
|
5
|
+
@properties ||= Hash.new({})
|
6
|
+
end
|
7
|
+
|
8
|
+
def properties=(properties)
|
9
|
+
@properties = properties
|
10
|
+
end
|
11
|
+
|
12
|
+
def property(name, rules = {})
|
13
|
+
properties[name] = rules
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.included(base)
|
18
|
+
base.extend(ClassMethods)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.inherited(subclass)
|
22
|
+
subclass.properties = properties
|
23
|
+
end
|
24
|
+
|
25
|
+
def properties
|
26
|
+
self.class.properties.keys
|
27
|
+
end
|
28
|
+
|
29
|
+
# Determine whether this property is required. Defauls to false.
|
30
|
+
def required?(property_name)
|
31
|
+
self.class.properties[property_name].fetch(:required, false)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Determine whether this property can be set more than once. Defaults
|
35
|
+
# to true.
|
36
|
+
def multiple?(property_name)
|
37
|
+
self.class.properties[property_name].fetch(:multiple, true)
|
38
|
+
end
|
39
|
+
|
40
|
+
def can_contain?(property)
|
41
|
+
true
|
42
|
+
end
|
43
|
+
|
44
|
+
def can_add?(property)
|
45
|
+
if contains?(property.name) && !multiple?(property.name)
|
46
|
+
error(property.name, "property '%s' must not occur more than once" % property.name)
|
47
|
+
end
|
48
|
+
@errors.empty?
|
49
|
+
end
|
50
|
+
|
51
|
+
def error(property_name, message)
|
52
|
+
@errors[property_name] << message
|
53
|
+
end
|
54
|
+
|
55
|
+
def valid?
|
56
|
+
properties.select { |property_name| required?(property_name) }.each do |property_name|
|
57
|
+
if !contains?(property_name)
|
58
|
+
error(property_name, "missing required property '%s'" % property_name)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
@errors.empty?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/selene/event.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module Selene
|
2
|
+
class Component
|
3
|
+
def initialize(properties = {})
|
4
|
+
@properties = properties
|
5
|
+
end
|
6
|
+
|
7
|
+
def to_h
|
8
|
+
@properties
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_ical
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Event < Component
|
16
|
+
def initialize(properties = {})
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_ical
|
20
|
+
lines = []
|
21
|
+
if @properties['exdate'] && @properties['exdate'].count > 0
|
22
|
+
exdate_values = @exdate.map { |time| time.strftime('%Y%m%dT%H%M%S') }
|
23
|
+
lines << "EXDATE;TZID=America/Detroit:#{exdate_values.join(',')}"
|
24
|
+
end
|
25
|
+
lines.join("\n")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/selene/event_builder.rb
CHANGED
@@ -86,16 +86,16 @@ module Selene
|
|
86
86
|
end
|
87
87
|
|
88
88
|
def can_add?(property)
|
89
|
-
if property.name == 'dtend' &&
|
89
|
+
if property.name == 'dtend' && contains?('duration')
|
90
90
|
error('dtend', "The 'dtend' property cannot be set if the 'duration' property already exists")
|
91
|
-
elsif property.name == 'duration' &&
|
91
|
+
elsif property.name == 'duration' && contains?('dtend')
|
92
92
|
error('duration', "The 'duration' property cannot be set if the 'dtend' property already exists")
|
93
93
|
end
|
94
94
|
super(property)
|
95
95
|
end
|
96
96
|
|
97
97
|
def valid?
|
98
|
-
if !
|
98
|
+
if !contains?('dtstart') && parent && !parent.contains?('method')
|
99
99
|
error('dtstart', "The 'dtstart' property is required if the calendar does not have a 'method' property")
|
100
100
|
end
|
101
101
|
super
|
data/lib/selene/line.rb
CHANGED
@@ -17,15 +17,19 @@ module Selene
|
|
17
17
|
PARAM_KEY_VALUE = /(?<key>[^=]+)=(?<value>.*)/
|
18
18
|
|
19
19
|
# Split a string into content lines
|
20
|
-
def self.split(string)
|
21
|
-
|
22
|
-
string.gsub(
|
20
|
+
def self.split(string, &block)
|
21
|
+
separator = string.match(/\r\n|\r|\n/, &:to_s) || "\r\n"
|
22
|
+
string.gsub("#{separator}\s", '').split(separator).map do |line_string|
|
23
|
+
parse(line_string).tap do |line|
|
24
|
+
block.call(line) if line && block
|
25
|
+
end
|
26
|
+
end
|
23
27
|
end
|
24
28
|
|
25
|
-
#
|
26
|
-
def self.parse(
|
27
|
-
|
28
|
-
|
29
|
+
# convert line string into line object
|
30
|
+
def self.parse(line_string)
|
31
|
+
line_string.match(/#{NAME}#{PARAMS}?:#{VALUE}/) do |match|
|
32
|
+
new(match[:name], match[:value], parse_params(match[:params]))
|
29
33
|
end
|
30
34
|
end
|
31
35
|
|
data/lib/selene/parser.rb
CHANGED
@@ -11,18 +11,22 @@ require 'selene/time_zone_builder'
|
|
11
11
|
|
12
12
|
module Selene
|
13
13
|
class Parser
|
14
|
-
def self.parse(
|
15
|
-
new(
|
14
|
+
def self.parse(data)
|
15
|
+
new(data).parse
|
16
16
|
end
|
17
17
|
|
18
|
-
def initialize(
|
19
|
-
@
|
18
|
+
def initialize(data)
|
19
|
+
@data = encode(data)
|
20
|
+
end
|
21
|
+
|
22
|
+
def encode(data)
|
23
|
+
data.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
|
20
24
|
end
|
21
25
|
|
22
26
|
def parse
|
23
27
|
feed = FeedBuilder.new
|
24
28
|
stack = [feed]
|
25
|
-
Line.split(@
|
29
|
+
Line.split(@data) do |line|
|
26
30
|
if line.begin_component?
|
27
31
|
builder = create_builder(line.component_name)
|
28
32
|
stack[-1].add(line.component_name, builder)
|
data/lib/selene/version.rb
CHANGED
@@ -20,7 +20,7 @@ module Selene
|
|
20
20
|
assert_equal builder.component['vtimezone'].first, { 'tzid' => 'America/Detroit' }
|
21
21
|
end
|
22
22
|
|
23
|
-
# Test properties with :
|
23
|
+
# Test properties with required: true
|
24
24
|
%w(prodid version).each do |property|
|
25
25
|
define_method "test_#{property}_required" do
|
26
26
|
assert_required(CalendarBuilder.new, property)
|
@@ -33,6 +33,5 @@ module Selene
|
|
33
33
|
assert_single(CalendarBuilder.new, property)
|
34
34
|
end
|
35
35
|
end
|
36
|
-
|
37
36
|
end
|
38
37
|
end
|
@@ -4,7 +4,7 @@ module Selene
|
|
4
4
|
class ComponentBuilderTest < MiniTest::Test
|
5
5
|
def test_sets_property_rules
|
6
6
|
ComponentBuilder.property :version, :required => true
|
7
|
-
assert_equal({ :required => true }, ComponentBuilder.
|
7
|
+
assert_equal({ :required => true }, ComponentBuilder.properties[:version])
|
8
8
|
end
|
9
9
|
end
|
10
10
|
end
|
data/test/selene/line_test.rb
CHANGED
@@ -17,8 +17,8 @@ module Selene
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def test_parses_params
|
20
|
-
assert_equal Line.
|
21
|
-
Line.
|
20
|
+
assert_equal Line.new('DTSTART', '20130110T183000', 'tzid' => 'America/New_York'),
|
21
|
+
Line.parse('DTSTART;TZID=America/New_York:20130110T183000')
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
data/test/selene/parser_test.rb
CHANGED
@@ -7,17 +7,15 @@ module Selene
|
|
7
7
|
include FixtureHelper
|
8
8
|
|
9
9
|
def test_parses_blank_string
|
10
|
-
assert_equal Parser.parse("")
|
10
|
+
assert_equal({}, Parser.parse(""))
|
11
11
|
end
|
12
12
|
|
13
13
|
def test_parses_simple_calendar
|
14
|
-
assert_equal Parser.parse("BEGIN:VCALENDAR\r\nSUMMARY:Meetups\r\nEND:VCALENDAR")
|
15
|
-
{ 'vcalendar' => [{ 'summary' => 'Meetups' }] }
|
14
|
+
assert_equal({ 'vcalendar' => [{ 'summary' => 'Meetups' }] }, Parser.parse("BEGIN:VCALENDAR\r\nSUMMARY:Meetups\r\nEND:VCALENDAR"))
|
16
15
|
end
|
17
16
|
|
18
17
|
def test_parses_meetup_calendar
|
19
|
-
assert_equal Parser.parse(fixture('meetup.ics'))
|
20
|
-
JSON.parse(fixture('meetup.json'))
|
18
|
+
assert_equal JSON.parse(fixture('meetup.json')), Parser.parse(fixture('meetup.ics'))
|
21
19
|
end
|
22
20
|
|
23
21
|
def test_contain_error
|
data/test/test_helper.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
require 'bundler/setup'
|
2
|
+
require 'byebug'
|
2
3
|
require 'minitest/autorun'
|
3
|
-
require 'minitest/reporters'
|
4
4
|
require 'minitest/mock'
|
5
|
+
require 'minitest/reporters'
|
5
6
|
require 'selene'
|
6
7
|
|
7
8
|
Minitest::Reporters.use! Minitest::Reporters::DefaultReporter.new
|
@@ -34,9 +35,7 @@ module Selene
|
|
34
35
|
end
|
35
36
|
|
36
37
|
module BuilderTestHelper
|
37
|
-
|
38
38
|
def assert_required builder, property
|
39
|
-
builder.valid?
|
40
39
|
assert_error builder, property, "missing required property '#{property}'"
|
41
40
|
end
|
42
41
|
|
@@ -49,6 +48,7 @@ module Selene
|
|
49
48
|
end
|
50
49
|
|
51
50
|
def assert_error builder, property, message
|
51
|
+
builder.valid?
|
52
52
|
assert builder.errors[property].any? { |e| e =~ /#{message}/ }, "#{builder.class.name}: #{message}"
|
53
53
|
end
|
54
54
|
end
|
metadata
CHANGED
@@ -1,97 +1,97 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: selene
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cory Kaufman-Schofield
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-02-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ice_cube
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: byebug
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: minitest-reporters
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: rake
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- -
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- -
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: guard
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- -
|
73
|
+
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
75
|
version: '0'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- -
|
80
|
+
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: guard-minitest
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- -
|
87
|
+
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
89
|
version: '0'
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- -
|
94
|
+
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
97
|
description: Selene is an iCalendar parser for Ruby
|
@@ -101,8 +101,8 @@ executables: []
|
|
101
101
|
extensions: []
|
102
102
|
extra_rdoc_files: []
|
103
103
|
files:
|
104
|
-
- .gitattributes
|
105
|
-
- .gitignore
|
104
|
+
- ".gitattributes"
|
105
|
+
- ".gitignore"
|
106
106
|
- Gemfile
|
107
107
|
- Guardfile
|
108
108
|
- LICENSE.txt
|
@@ -113,7 +113,9 @@ files:
|
|
113
113
|
- lib/selene/calendar_builder.rb
|
114
114
|
- lib/selene/component_builder.rb
|
115
115
|
- lib/selene/component_errors.rb
|
116
|
+
- lib/selene/component_validator.rb
|
116
117
|
- lib/selene/daylight_savings_time_builder.rb
|
118
|
+
- lib/selene/event.rb
|
117
119
|
- lib/selene/event_builder.rb
|
118
120
|
- lib/selene/feed_builder.rb
|
119
121
|
- lib/selene/line.rb
|
@@ -143,17 +145,17 @@ require_paths:
|
|
143
145
|
- lib
|
144
146
|
required_ruby_version: !ruby/object:Gem::Requirement
|
145
147
|
requirements:
|
146
|
-
- -
|
148
|
+
- - ">="
|
147
149
|
- !ruby/object:Gem::Version
|
148
150
|
version: '0'
|
149
151
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
150
152
|
requirements:
|
151
|
-
- -
|
153
|
+
- - ">="
|
152
154
|
- !ruby/object:Gem::Version
|
153
155
|
version: '0'
|
154
156
|
requirements: []
|
155
157
|
rubyforge_project:
|
156
|
-
rubygems_version: 2.
|
158
|
+
rubygems_version: 2.7.3
|
157
159
|
signing_key:
|
158
160
|
specification_version: 4
|
159
161
|
summary: Selene is an iCalendar parser for Ruby
|