hootenanny 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +5 -7
- data/Gemfile.lock +46 -28
- data/app/controllers/hootenanny/notifications_controller.rb +31 -0
- data/app/controllers/hootenanny/parameters.rb +16 -0
- data/app/controllers/hootenanny/subscriptions_controller.rb +24 -3
- data/app/models/hootenanny/publish_notification.rb +90 -0
- data/app/models/hootenanny/subscription.rb +72 -23
- data/config/routes.rb +2 -1
- data/db/migrate/20130607182642_add_started_at_and_lease_duration_to_subscriptions.rb +15 -0
- data/db/migrate/20130608225621_add_hmac_secret_to_subscription.rb +5 -0
- data/db/migrate/20130611235218_add_publish_notifications.rb +9 -0
- data/db/migrate/20130612153138_add_timestamps_to_publish_notification.rb +5 -0
- data/db/migrate/20130705200729_add_processed_flag_to_publish_notifications.rb +6 -0
- data/db/migrate/20130711061329_switch_publish_notification_process_state_from_boolean_to_string.rb +11 -0
- data/db/migrate/20130711061558_add_index_to_notifications_state.rb +5 -0
- data/hootenanny.gemspec +6 -2
- data/lib/hootenanny/configuration.rb +43 -0
- data/lib/hootenanny/correspondent.rb +44 -0
- data/lib/hootenanny/errors.rb +42 -1
- data/lib/hootenanny/feed.rb +75 -0
- data/lib/hootenanny/feed/atom_feed.rb +18 -0
- data/lib/hootenanny/feed/atom_feed_item.rb +8 -0
- data/lib/hootenanny/feed/digest_feed.rb +14 -0
- data/lib/hootenanny/feed/digest_feed_item.rb +11 -0
- data/lib/hootenanny/feed/feed_item.rb +30 -0
- data/lib/hootenanny/feed/file.rb +66 -0
- data/lib/hootenanny/feed/json_feed.rb +48 -0
- data/lib/hootenanny/feed/json_feed_item.rb +8 -0
- data/lib/hootenanny/feed/null_feed.rb +27 -0
- data/lib/hootenanny/feed/rss_feed.rb +52 -0
- data/lib/hootenanny/feed/rss_feed_item.rb +8 -0
- data/lib/hootenanny/feed_store.rb +30 -0
- data/lib/hootenanny/feed_store/file_feed_store.rb +55 -0
- data/lib/hootenanny/feed_store/web_feed_store.rb +42 -0
- data/lib/hootenanny/hub.rb +116 -22
- data/lib/hootenanny/publish_notification_expiration_policy.rb +17 -0
- data/lib/hootenanny/request.rb +40 -0
- data/lib/hootenanny/request/publish_notification.rb +94 -0
- data/lib/hootenanny/request/subscription.rb +153 -0
- data/lib/hootenanny/subscription_delivery.rb +47 -0
- data/lib/hootenanny/topic.rb +38 -0
- data/lib/hootenanny/topic_synchronizer.rb +71 -0
- data/lib/hootenanny/uri.rb +53 -0
- data/lib/hootenanny/verification.rb +108 -0
- data/lib/hootenanny/version.rb +1 -1
- data/spec/dummy/config/database.yml +12 -6
- data/spec/dummy/db/migrate/20130607183149_add_started_at_and_lease_duration_to_subscriptions.hootenanny.rb +16 -0
- data/spec/dummy/db/migrate/20130608231253_add_hmac_secret_to_subscription.hootenanny.rb +6 -0
- data/spec/dummy/db/migrate/20130611235546_add_publish_notifications.hootenanny.rb +10 -0
- data/spec/dummy/db/migrate/20130612153353_add_timestamps_to_publish_notification.hootenanny.rb +6 -0
- data/spec/dummy/db/migrate/20130705200832_add_processed_flag_to_publish_notifications.hootenanny.rb +7 -0
- data/spec/dummy/db/migrate/20130711061518_switch_publish_notification_process_state_from_boolean_to_string.hootenanny.rb +12 -0
- data/spec/dummy/db/migrate/20130711061629_add_index_to_notifications_state.hootenanny.rb +6 -0
- data/spec/factories/publish_notification.rb +13 -0
- data/spec/factories/requests/publish_notification.rb +11 -0
- data/spec/factories/requests/subscription.rb +46 -0
- data/spec/factories/subscription.rb +14 -2
- data/spec/factories/verification.rb +15 -0
- data/spec/features/publishers/can_notify_the_hub_of_content_updates_spec.rb +58 -0
- data/spec/features/subscribers/are_protected_from_unwarranted_subscriptions_spec.rb +59 -0
- data/spec/features/subscribers/can_receive_distributions_of_topic_content_spec.rb +175 -0
- data/spec/features/subscribers/can_subscribe_to_a_topic_spec.rb +76 -7
- data/spec/fixtures/feeds/atom/97d79220f68b4bf27.atom +0 -0
- data/spec/fixtures/feeds/atom/sample_feed.atom +51 -0
- data/spec/fixtures/feeds/digest/97d79220f68b4bf27.digest +1 -0
- data/spec/fixtures/feeds/digest/complete_broadcasted_items/5b187098da59f077f/97d79220f68b4bf27.digest +6 -0
- data/spec/fixtures/feeds/digest/incomplete_broadcasted_items/5b187098da59f077f/97d79220f68b4bf27.digest +5 -0
- data/spec/fixtures/feeds/digest/sample_feed.digest +6 -0
- data/spec/fixtures/feeds/json/97d79220f68b4bf27.json +1 -0
- data/spec/fixtures/feeds/json/feed_with_one_item.json +21 -0
- data/spec/fixtures/feeds/json/sample_feed.json +30 -0
- data/spec/fixtures/feeds/rss/5b187098da59f077f/97d79220f68b4bf27.rss +21 -0
- data/spec/fixtures/feeds/rss/97d79220f68b4bf27.rss +0 -0
- data/spec/fixtures/feeds/rss/feed_with_one_item.rss +15 -0
- data/spec/fixtures/feeds/rss/minimal_feed.rss +12 -0
- data/spec/fixtures/feeds/rss/sample_feed.rss +22 -0
- data/spec/fixtures/feeds/rss/sample_feed_2.rss +22 -0
- data/spec/lib/hootenanny/configuration_spec.rb +7 -0
- data/spec/lib/hootenanny/correspondent_spec.rb +94 -0
- data/spec/lib/hootenanny/errors_spec.rb +21 -0
- data/spec/lib/hootenanny/feed/atom_feed_item_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/atom_feed_spec.rb +40 -0
- data/spec/lib/hootenanny/feed/digest_feed_item_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/digest_feed_spec.rb +40 -0
- data/spec/lib/hootenanny/feed/feed_item_spec.rb +49 -0
- data/spec/lib/hootenanny/feed/file_spec.rb +66 -0
- data/spec/lib/hootenanny/feed/json_feed_item_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/json_feed_spec.rb +128 -0
- data/spec/lib/hootenanny/feed/null_feed_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/rss_feed_item_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/rss_feed_spec.rb +143 -0
- data/spec/lib/hootenanny/feed_spec.rb +159 -0
- data/spec/lib/hootenanny/feed_store/file_feed_store_spec.rb +58 -0
- data/spec/lib/hootenanny/feed_store/web_feed_store_spec.rb +47 -0
- data/spec/lib/hootenanny/feed_store_spec.rb +27 -0
- data/spec/lib/hootenanny/hub_spec.rb +73 -0
- data/spec/lib/hootenanny/publish_notification_expiration_policy_spec.rb +35 -0
- data/spec/lib/hootenanny/request/publish_notification_spec.rb +43 -0
- data/spec/lib/hootenanny/request/subscription_spec.rb +89 -0
- data/spec/lib/hootenanny/request_spec.rb +21 -0
- data/spec/lib/hootenanny/subscription_delivery_spec.rb +54 -0
- data/spec/lib/hootenanny/topic_spec.rb +15 -0
- data/spec/lib/hootenanny/topic_synchronizer_spec.rb +98 -0
- data/spec/lib/hootenanny/uri_spec.rb +32 -0
- data/spec/lib/hootenanny/verification_spec.rb +92 -0
- data/spec/models/hootenanny/publish_notification_spec.rb +55 -0
- data/spec/models/hootenanny/subscription_spec.rb +58 -19
- data/spec/support/verification.rb +63 -0
- metadata +231 -14
- data/spec/controllers/hootenanny/hub_spec.rb +0 -15
data/config/routes.rb
CHANGED
@@ -0,0 +1,15 @@
|
|
1
|
+
class AddStartedAtAndLeaseDurationToSubscriptions < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
remove_index :hootenanny_subscriptions, :subscriber
|
4
|
+
remove_index :hootenanny_subscriptions, [:subscriber, :topic]
|
5
|
+
|
6
|
+
add_column :hootenanny_subscriptions, :started_at, :datetime
|
7
|
+
add_column :hootenanny_subscriptions, :lease_duration, :integer
|
8
|
+
|
9
|
+
change_column :hootenanny_subscriptions, :started_at, :datetime, :null => false
|
10
|
+
change_column :hootenanny_subscriptions, :lease_duration, :integer, :null => false
|
11
|
+
|
12
|
+
add_index :hootenanny_subscriptions, :subscriber, :unique => false
|
13
|
+
add_index :hootenanny_subscriptions, [:subscriber, :topic], :unique => true
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
class AddPublishNotifications < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :hootenanny_publish_notifications do |t|
|
4
|
+
t.string :topic, :limit => 250, :null => false
|
5
|
+
end
|
6
|
+
|
7
|
+
add_index 'hootenanny_publish_notifications', 'topic', :name => 'index_hootenanny_publish_notifications_on_topic', :unique => true
|
8
|
+
end
|
9
|
+
end
|
data/db/migrate/20130711061329_switch_publish_notification_process_state_from_boolean_to_string.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
class SwitchPublishNotificationProcessStateFromBooleanToString < ActiveRecord::Migration
|
2
|
+
def up
|
3
|
+
add_column :hootenanny_publish_notifications, :state, :string, :limit => 15, :null => false, :default => 'unprocessed'
|
4
|
+
remove_column :hootenanny_publish_notifications, :processed
|
5
|
+
end
|
6
|
+
|
7
|
+
def down
|
8
|
+
add_column :hootenanny_publish_notifications, :processed, :boolean, :default => false, :null => false
|
9
|
+
remove_column :hootenanny_publish_notifications, :state
|
10
|
+
end
|
11
|
+
end
|
data/hootenanny.gemspec
CHANGED
@@ -31,11 +31,15 @@ Gem::Specification.new do |s|
|
|
31
31
|
|
32
32
|
s.add_dependency 'rails', '~> 3.2'
|
33
33
|
s.add_dependency 'strong_parameters', '~> 0.2.1'
|
34
|
+
s.add_dependency 'faraday', '~> 0.8.7'
|
35
|
+
s.add_dependency 'chronological', '~> 1.0.0beta10'
|
34
36
|
|
35
37
|
s.add_development_dependency 'pg'
|
36
|
-
s.add_development_dependency '
|
37
|
-
s.add_development_dependency 'rspec
|
38
|
+
s.add_development_dependency 'rspec-rails', '~> 2.14.0.rc1'
|
39
|
+
s.add_development_dependency 'rspec', '~> 2.14.0.rc1'
|
38
40
|
s.add_development_dependency 'database_cleaner', '~> 0.9.1'
|
39
41
|
s.add_development_dependency 'rspectacular', '~> 0.13'
|
40
42
|
s.add_development_dependency 'factory_girl_rails', '~> 4.2'
|
43
|
+
s.add_development_dependency 'timecop', '~> 0.6.1'
|
44
|
+
s.add_development_dependency 'webmock', '~> 1.11'
|
41
45
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'hootenanny/topic_synchronizer'
|
3
|
+
require 'hootenanny/subscription_delivery'
|
4
|
+
require 'hootenanny/feed_store/web_feed_store'
|
5
|
+
require 'hootenanny/feed_store/file_feed_store'
|
6
|
+
|
7
|
+
module Hootenanny
|
8
|
+
def self.config
|
9
|
+
Hootenanny::Configuration.instance
|
10
|
+
end
|
11
|
+
|
12
|
+
class Configuration
|
13
|
+
include Singleton
|
14
|
+
|
15
|
+
attr_accessor :default_local_feed_store,
|
16
|
+
:default_remote_feed_store,
|
17
|
+
:default_synchronizer,
|
18
|
+
:default_subscription_delivery
|
19
|
+
|
20
|
+
def default_local_feed_store
|
21
|
+
FeedStore::FileFeedStore.new(
|
22
|
+
:location => File.join('.',
|
23
|
+
'tmp',
|
24
|
+
'broadcasted_topics'))
|
25
|
+
end
|
26
|
+
|
27
|
+
def default_remote_feed_store
|
28
|
+
FeedStore::WebFeedStore.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def default_synchronizer
|
32
|
+
TopicSynchronizer
|
33
|
+
end
|
34
|
+
|
35
|
+
def default_subscription_delivery
|
36
|
+
SubscriptionDelivery
|
37
|
+
end
|
38
|
+
|
39
|
+
def default_publish_notification_expiration_threshold
|
40
|
+
Time.now - (24 * 60 * 60)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'hootenanny/configuration'
|
2
|
+
require 'hootenanny/subscription'
|
3
|
+
require 'hootenanny/topic'
|
4
|
+
|
5
|
+
module Hootenanny
|
6
|
+
class Correspondent
|
7
|
+
def initialize(topic, options = {})
|
8
|
+
self.topic = topic
|
9
|
+
self.synchronizer_class = options[:synchronizer] ||
|
10
|
+
Hootenanny.config.default_synchronizer
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.broadcast(topic, options = {})
|
14
|
+
correspondent = new(topic, options = {})
|
15
|
+
|
16
|
+
correspondent.broadcast
|
17
|
+
end
|
18
|
+
|
19
|
+
def broadcast
|
20
|
+
subscriptions.all? do |subscription|
|
21
|
+
synchronizer_class.sync(subscriber: subscription.subscriber,
|
22
|
+
digest_secret: subscription.digest_secret,
|
23
|
+
topic: topic.url)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def subscriptions
|
30
|
+
Hootenanny::Subscription.to(topic.url)
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_accessor :topic,
|
34
|
+
:synchronizer_class
|
35
|
+
|
36
|
+
def topic=(other)
|
37
|
+
@topic = if other.respond_to?(:url)
|
38
|
+
other
|
39
|
+
else
|
40
|
+
Hootenanny::Topic.from_url(other)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/hootenanny/errors.rb
CHANGED
@@ -1,7 +1,48 @@
|
|
1
|
-
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
class Hootenanny::Error < StandardError
|
2
4
|
def to_h
|
3
5
|
{
|
4
6
|
message: self.message
|
5
7
|
}
|
6
8
|
end
|
9
|
+
|
10
|
+
def as_json(options = {})
|
11
|
+
{
|
12
|
+
error: {
|
13
|
+
message: self.message
|
14
|
+
}
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.wrap(exception)
|
19
|
+
hootenanny_error = self.new(exception.message)
|
20
|
+
|
21
|
+
hootenanny_error.set_backtrace exception.backtrace
|
22
|
+
|
23
|
+
hootenanny_error
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module Hootenanny
|
28
|
+
class Request; end
|
29
|
+
class URI < SimpleDelegator; end
|
30
|
+
class Feed; end
|
31
|
+
class FeedStore; end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Hootenanny::Request::BuildError < Hootenanny::Error; end
|
35
|
+
|
36
|
+
class Hootenanny::URI::InvalidError < Hootenanny::Error; end
|
37
|
+
class Hootenanny::URI::InvalidSchemeError < Hootenanny::Error
|
38
|
+
def initialize(message = 'Only HTTP and HTTPS URIs are allowed.')
|
39
|
+
super
|
40
|
+
end
|
7
41
|
end
|
42
|
+
|
43
|
+
class Hootenanny::SubscriptionError < Hootenanny::Error; end
|
44
|
+
class Hootenanny::Feed::UnknownContentTypeError < Hootenanny::Error; end
|
45
|
+
class Hootenanny::Feed::ParseError < Hootenanny::Error; end
|
46
|
+
class Hootenanny::Feed::CombinationError < Hootenanny::Error; end
|
47
|
+
class Hootenanny::FeedStore::InferenceError < Hootenanny::Error; end
|
48
|
+
class Hootenanny::FeedStore::UnknownStorageTypeError < Hootenanny::Error; end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
require 'hootenanny/errors'
|
3
|
+
require 'hootenanny/feed/rss_feed'
|
4
|
+
require 'hootenanny/feed/json_feed'
|
5
|
+
require 'hootenanny/feed/atom_feed'
|
6
|
+
require 'hootenanny/feed/digest_feed'
|
7
|
+
require 'hootenanny/feed/null_feed'
|
8
|
+
|
9
|
+
module Hootenanny
|
10
|
+
class Feed
|
11
|
+
FEED_TYPE_MAPPINGS = {
|
12
|
+
:rss => 'RSS',
|
13
|
+
:json => 'JSON',
|
14
|
+
:atom => 'Atom',
|
15
|
+
:digest => 'Digest',
|
16
|
+
:'' => 'Null',
|
17
|
+
}
|
18
|
+
|
19
|
+
def self.infer(content, options = {})
|
20
|
+
type = options.fetch(:type).to_s.downcase.to_sym
|
21
|
+
|
22
|
+
feed_class(type).from_content(content)
|
23
|
+
rescue KeyError => e
|
24
|
+
raise ::Hootenanny::Feed::UnknownContentTypeError.wrap(e)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.from_content(content)
|
28
|
+
adapter = allocate
|
29
|
+
|
30
|
+
adapter.send :content=, content
|
31
|
+
|
32
|
+
adapter
|
33
|
+
end
|
34
|
+
|
35
|
+
def -(other)
|
36
|
+
feed = self.class.from_content(self.content)
|
37
|
+
feed.items = self.items - other.items
|
38
|
+
feed
|
39
|
+
end
|
40
|
+
|
41
|
+
def +(other)
|
42
|
+
raise Hootenanny::Feed::CombinationError unless self.class == other.class
|
43
|
+
|
44
|
+
feed = self.class.from_content(self.content)
|
45
|
+
feed.items = (self.items + other.items).uniq
|
46
|
+
feed
|
47
|
+
end
|
48
|
+
|
49
|
+
def empty?
|
50
|
+
items.empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_digest_feed
|
54
|
+
feed = DigestFeed.from_content('{}')
|
55
|
+
feed.items = self.items.map { |item| DigestFeedItem.new(item.to_digest) }
|
56
|
+
feed
|
57
|
+
end
|
58
|
+
|
59
|
+
def type
|
60
|
+
self.class.name.match(/Hootenanny::Feed::(\w+)Feed/)[1].downcase.to_sym
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
|
65
|
+
attr_writer :items
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def self.feed_class(type)
|
70
|
+
type_class_partial = FEED_TYPE_MAPPINGS.fetch(type)
|
71
|
+
|
72
|
+
"Hootenanny::Feed::#{type_class_partial}Feed".constantize
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rss'
|
2
|
+
require 'hootenanny/feed'
|
3
|
+
require 'hootenanny/feed/atom_feed_item'
|
4
|
+
|
5
|
+
module Hootenanny
|
6
|
+
class Feed
|
7
|
+
class AtomFeed < ::Hootenanny::Feed::RSSFeed
|
8
|
+
|
9
|
+
def items
|
10
|
+
@items ||= content.entries.map { |i| Hootenanny::Feed::AtomFeedItem.new i }
|
11
|
+
end
|
12
|
+
|
13
|
+
def content_type
|
14
|
+
'application/atom+xml'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'hootenanny/feed'
|
3
|
+
require 'hootenanny/feed/digest_feed_item'
|
4
|
+
|
5
|
+
module Hootenanny
|
6
|
+
class Feed
|
7
|
+
class DigestFeed < ::Hootenanny::Feed::JSONFeed
|
8
|
+
|
9
|
+
def items
|
10
|
+
@items ||= content.fetch('items', []).map { |i| Hootenanny::Feed::DigestFeedItem.new i }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
module Hootenanny
|
4
|
+
class Feed
|
5
|
+
class FeedItem
|
6
|
+
|
7
|
+
attr_accessor :content
|
8
|
+
|
9
|
+
def initialize(content)
|
10
|
+
self.content = content
|
11
|
+
end
|
12
|
+
|
13
|
+
def hash
|
14
|
+
self.to_digest.hash
|
15
|
+
end
|
16
|
+
|
17
|
+
def ==(comparee)
|
18
|
+
self.to_digest == comparee.to_digest
|
19
|
+
end
|
20
|
+
|
21
|
+
def eql?(comparee)
|
22
|
+
self == comparee
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_digest
|
26
|
+
Digest::SHA256.hexdigest content.to_s
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Hootenanny
|
4
|
+
class Feed
|
5
|
+
class File
|
6
|
+
FILE_EXTENSION_MAPPINGS = {
|
7
|
+
'.rss' => 'RSS',
|
8
|
+
'.atom' => 'Atom',
|
9
|
+
'.json' => 'JSON',
|
10
|
+
'.digest' => 'Digest',
|
11
|
+
}
|
12
|
+
|
13
|
+
def initialize(base_path)
|
14
|
+
self.path = base_path
|
15
|
+
end
|
16
|
+
|
17
|
+
def <<(other_path)
|
18
|
+
return self if other_path.nil? || other_path == ''
|
19
|
+
|
20
|
+
self.class.new(path + other_path)
|
21
|
+
end
|
22
|
+
|
23
|
+
def read
|
24
|
+
file.exist? ? file.read : ''
|
25
|
+
end
|
26
|
+
|
27
|
+
def write(content)
|
28
|
+
path.dirname.mkpath
|
29
|
+
|
30
|
+
path.open('w+') do |f|
|
31
|
+
f.write(content)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def type
|
36
|
+
FILE_EXTENSION_MAPPINGS[extension]
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
file.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
attr_accessor :path
|
46
|
+
|
47
|
+
def path=(other)
|
48
|
+
@path = Pathname.new(other)
|
49
|
+
end
|
50
|
+
|
51
|
+
def file
|
52
|
+
@file ||= if path.file?
|
53
|
+
path
|
54
|
+
else
|
55
|
+
Pathname.glob(path).fetch(0, Pathname.new(''))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def extension
|
62
|
+
file.extname
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|