fare 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,57 @@
1
+ module Fare
2
+ class Configuration
3
+
4
+ attr_reader :publish_topics, :environments, :subscribers
5
+ attr_accessor :app_name, :app_version, :default_version
6
+
7
+ def initialize
8
+ @publish_topics = []
9
+ @environments = {}
10
+ @subscribers = {}
11
+ @default_version = "0.0"
12
+ @backup = false
13
+ end
14
+
15
+ def load_environment(environment)
16
+ environments.fetch(environment) { Proc.new {} }.call
17
+ end
18
+
19
+ def subscribe_topics
20
+ subscribers.map { |name, config| config.topics }.flatten
21
+ end
22
+
23
+ def subscriber(name)
24
+ subscribers[name.to_s] ||= SubscriberConfiguration.new(name.to_s)
25
+ end
26
+
27
+ def backup?
28
+ @backup
29
+ end
30
+
31
+ def backup!
32
+ @backup = true
33
+ end
34
+
35
+ class SubscriberConfiguration
36
+
37
+ attr_accessor :setup, :always_run, :stacks
38
+
39
+ def initialize(name)
40
+ @name = name
41
+ @stacks = []
42
+ @setup = Proc.new {}
43
+ @always_run = Proc.new {}
44
+ end
45
+
46
+ def topics
47
+ stacks.map(&:topics).flatten
48
+ end
49
+
50
+ def load_setup
51
+ @setup.call
52
+ end
53
+
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,134 @@
1
+ module Fare
2
+
3
+ # This class is the context in which the fare config file is loaded.
4
+ # It provides a DSL that fills a configuration object.
5
+ class ConfigurationDSL
6
+
7
+ InvalidOption = Class.new(ArgumentError)
8
+ VALID_PART_OF_TOPIC_NAME = /\A\w+\z/
9
+
10
+ attr_reader :configuration
11
+
12
+ def initialize
13
+ @configuration = Configuration.new
14
+ end
15
+
16
+ def environment(name, &block)
17
+ validate! "Environment", name.to_s
18
+ configuration.environments[name.to_s] = block
19
+ end
20
+
21
+ def app_name(name)
22
+ validate! "App name", name
23
+ configuration.app_name = name
24
+ end
25
+
26
+ def default_version(version)
27
+ configuration.default_version = version
28
+ end
29
+
30
+ def app_version(version)
31
+ configuration.app_version = version
32
+ end
33
+
34
+ def backup!
35
+ configuration.backup!
36
+ end
37
+
38
+ def publishes(topic)
39
+ topic.each do |key, value|
40
+ case key
41
+ when :subject then validate! "Subject", value
42
+ when :action then validate! "Action", value
43
+ when :version then true
44
+ else
45
+ raise InvalidOption, "Unknown option: #{key}"
46
+ end
47
+ end
48
+ configuration.publish_topics << topic
49
+ end
50
+
51
+ def subscriber(name = configuration.app_name, &block)
52
+ config = configuration.subscriber(name)
53
+ SubscriberDSL.new(config).parse(&block)
54
+ end
55
+
56
+ private
57
+
58
+ def validate!(name, value)
59
+ if value !~ VALID_PART_OF_TOPIC_NAME
60
+ raise InvalidOption, "#{name} can only contain word characters: #{value.inspect}"
61
+ end
62
+ end
63
+
64
+ class SubscriberDSL
65
+
66
+ attr_reader :configuration
67
+
68
+ def initialize(configuration)
69
+ @configuration = configuration
70
+ end
71
+
72
+ def parse(&block)
73
+ instance_eval(&block)
74
+ end
75
+
76
+ def stack(&block)
77
+ stack_dsl = StackDSL.new(configuration)
78
+ stack_dsl.parse(&block)
79
+ stack_dsl.verify!
80
+ stack = stack_dsl.to_stack
81
+ configuration.stacks << stack
82
+ end
83
+
84
+ def setup(&block)
85
+ configuration.setup = block
86
+ end
87
+
88
+ def always_run(&block)
89
+ configuration.always_run = block
90
+ end
91
+
92
+ end
93
+
94
+ class StackDSL
95
+
96
+ attr_reader :configuration
97
+
98
+ def initialize(configuration)
99
+ @configuration = configuration
100
+ @topics = []
101
+ @run = :not_specified
102
+ end
103
+
104
+ def verify!
105
+ if @topics.empty?
106
+ raise "Stack without topics for stack on line #{@source_location.last} of #{@source_location.first}"
107
+ end
108
+ if @run == :not_specified
109
+ raise "No run list specified for stack on line #{@source_location.last} of #{@source_location.first}"
110
+ end
111
+ end
112
+
113
+ def to_stack
114
+ SubscriberStack.new(configuration, @topics, @run)
115
+ end
116
+
117
+ def parse(&block)
118
+ @source_location = block.source_location
119
+ instance_eval(&block)
120
+ end
121
+
122
+ def listen_to(topic)
123
+ @topics << Topic.new(topic)
124
+ end
125
+
126
+ def run(&block)
127
+ @run = block
128
+ end
129
+
130
+ end
131
+
132
+ end
133
+
134
+ end
@@ -0,0 +1,82 @@
1
+ module Fare
2
+ class ConfigurationWhenLocked
3
+
4
+ attr_reader :options
5
+
6
+ def initialize(options)
7
+ @options = options
8
+ validate_lockfile!
9
+ @configuration = loaded_configuration.load_configuration
10
+ @configuration.load_environment(environment)
11
+ end
12
+
13
+ def validate_subscriber!
14
+ configuration.setup.call
15
+ configuration.stacks.each(&:to_app)
16
+ end
17
+
18
+ def find_publishable_topic(options)
19
+ lock_file_contents.fetch("publishes").find { |t|
20
+ t.fetch("subject") == options.fetch(:subject) && t.fetch("action") == options.fetch(:action)
21
+ }
22
+ end
23
+
24
+ def app_name
25
+ configuration.app_name
26
+ end
27
+
28
+ def app_version
29
+ configuration.app_version || "unknown"
30
+ end
31
+
32
+ def fetch_subscriber(name)
33
+ configuration.subscribers.fetch(name)
34
+ end
35
+
36
+ def fetch_subscriber_queue(name)
37
+ Fare.queue_adapter.fetch(environment, name)
38
+ end
39
+
40
+ private
41
+
42
+ def subscribers
43
+ configuration.subscribers
44
+ end
45
+
46
+ attr_reader :configuration
47
+
48
+ def validate_lockfile!
49
+ unless File.exist?(lock_filename)
50
+ raise LockFileNotFoundError, "Lockfile doesn't exist at #{lock_filename}"
51
+ end
52
+ if loaded_configuration.checksum != lock_file_contents.fetch("checksum")
53
+ raise ChecksumNotMatchingError, "Checksum of lockfile doesn't match. Run `fare update` maybe?"
54
+ end
55
+ end
56
+
57
+ def lock_filename
58
+ loaded_configuration.lock_filename
59
+ end
60
+
61
+ def lock_file_contents
62
+ @lock_file_contents ||= YAML.load(File.open(loaded_configuration.lock_filename, "r:utf-8").read)
63
+ end
64
+
65
+ def loaded_configuration
66
+ @loaded_configuration ||= LoadConfigurationFile.new(filename: filename, environment: environment)
67
+ end
68
+
69
+ def environment
70
+ options.fetch(:environment) { find_environment }
71
+ end
72
+
73
+ def filename
74
+ options.fetch(:filename) { Fare.default_config_file }
75
+ end
76
+
77
+ def find_environment
78
+ Fare.default_environment || raise(NoEnvironmentFoundError, "No environment specified!")
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,26 @@
1
+ module Fare
2
+ class Event
3
+ include Virtus.model
4
+
5
+ attribute :id
6
+ attribute :subject
7
+ attribute :action
8
+ attribute :payload
9
+ attribute :source
10
+ attribute :version
11
+ attribute :sent_at, DateTime
12
+
13
+ def serialize
14
+ Base64.urlsafe_encode64(attributes.to_json)
15
+ end
16
+
17
+ def self.deserialize(message)
18
+ new JSON.parse(Base64.urlsafe_decode64(message))
19
+ end
20
+
21
+ def to_json(*)
22
+ attributes.to_json
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,131 @@
1
+ module Fare
2
+ class GenerateLockFile
3
+ extend Forwardable
4
+
5
+ def_delegators :@configuration, :environment, :lock_filename, :checksum
6
+
7
+ attr_reader :topics, :force, :queues
8
+
9
+ def initialize(options = {})
10
+ @force = options.delete(:force) { false }
11
+ @configuration = LoadConfigurationFile.new(options)
12
+ @topics = {}
13
+ @queues = {}
14
+ end
15
+
16
+ def configuration
17
+ @configuration.configuration
18
+ end
19
+
20
+ def call
21
+ return if skip_generating?
22
+
23
+ configuration.load_environment(environment)
24
+
25
+ find_publishing_topics
26
+ find_subscriber_queues
27
+
28
+ File.open(lock_filename, "w:utf-8") do |f|
29
+ f.write(data.to_yaml)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def backup?
36
+ configuration.backup?
37
+ end
38
+
39
+ def skip_generating?
40
+ !force && checksum_matches?
41
+ end
42
+
43
+ def checksum_matches?
44
+ File.exist?(lock_filename) && YAML.load(File.open(lock_filename, "r:utf-8").read).fetch("checksum") == checksum
45
+ end
46
+
47
+ def find_publishing_topics
48
+ configuration.publish_topics.each do |topic_info|
49
+ topic = topic_for(topic_info)
50
+ subscribe topic, backup_queue if backup?
51
+ end
52
+ end
53
+
54
+ def find_subscriber_queues
55
+ configuration.subscribers.each do |subscriber_name, subscriber|
56
+ subscriber.topics.each do |topic_info|
57
+ topic = topic_for(topic_info)
58
+ subscribe topic, backup_queue if backup?
59
+ subscribe topic, queue_for(subscriber_name)
60
+ end
61
+ end
62
+ end
63
+
64
+ def data
65
+ data = {
66
+ "publishes" => configuration.publish_topics.map { |topic_info| serialize_topic(topic_info) },
67
+ "subscribes" => configuration.subscribe_topics.map { |topic_info| serialize_topic(topic_info) },
68
+ "queues" => Hash[ queues.map { |name, queue| [ name, queue.url ] }],
69
+ "checksum" => checksum,
70
+ }
71
+ data["backup_queue"] = backup_queue.url if backup?
72
+ data
73
+ end
74
+
75
+ def subscribe(topic, queue)
76
+ subscription = topic.subscribe(queue.arn)
77
+ subscription.raw_message_delivery = true
78
+ account_id = topic.owner
79
+ policy = {
80
+ "Version" => "2012-10-17",
81
+ "Id" => "Fare_Policy",
82
+ "Statement" => [
83
+ {
84
+ "Sid" => "1",
85
+ "Effect" => "Allow",
86
+ "Principal" => { "AWS" => "*" },
87
+ "Action" => "SQS:SendMessage",
88
+ "Resource" => queue.arn,
89
+ "Condition" => {
90
+ "ArnLike" => {
91
+ "aws:SourceArn" => "arn:aws:sns:*:#{account_id}:*"
92
+ }
93
+ }
94
+ },
95
+ ]
96
+ }
97
+ queue.policy = policy
98
+ end
99
+
100
+ def serialize_topic(topic_info)
101
+ {
102
+ "subject" => topic_info.fetch(:subject),
103
+ "action" => topic_info.fetch(:action),
104
+ "version" => topic_info.fetch(:version) { configuration.default_version },
105
+ "arn" => topic_for(topic_info).arn,
106
+ }
107
+ end
108
+
109
+ def backup_queue
110
+ if backup?
111
+ @backup_queue ||= queue_for("backup")
112
+ else
113
+ fail "NO BACKUP FOR YOU!"
114
+ end
115
+ end
116
+
117
+ def app_name
118
+ configuration.app_name
119
+ end
120
+
121
+ def topic_for(options)
122
+ topic_key = [options[:subject], options[:action] ]
123
+ topics[topic_key] ||= Fare.topic_adapter.create("#{environment}-#{options[:subject]}-#{options[:action]}").tap { |topic| puts "Created topic #{topic.arn}" }
124
+ end
125
+
126
+ def queue_for(name)
127
+ queues[name] ||= Fare.queue_adapter.fetch(environment, name)
128
+ end
129
+
130
+ end
131
+ end
@@ -0,0 +1,45 @@
1
+ module Fare
2
+ class LoadConfigurationFile
3
+
4
+ attr_reader :filename, :environment
5
+
6
+ def initialize(options)
7
+ @filename = options.fetch(:filename) { Fare.default_config_file }
8
+ @environment = options.fetch(:environment) { Fare.default_environment }
9
+ verify_file!
10
+ end
11
+
12
+ def verify_file!
13
+ unless exists?
14
+ raise ConfigurationNotFoundError, "Configuration file doesn't exist at #{filename}"
15
+ end
16
+ end
17
+
18
+ def configuration
19
+ @configuration ||= load_configuration
20
+ end
21
+
22
+ def load_configuration
23
+ dsl = ConfigurationDSL.new
24
+ dsl.instance_eval(code, filename)
25
+ dsl.configuration
26
+ end
27
+
28
+ def code
29
+ @code ||= File.open(filename.to_s, "r:utf-8").read
30
+ end
31
+
32
+ def lock_filename
33
+ File.expand_path("../fare.#{environment}.lock", filename)
34
+ end
35
+
36
+ def checksum
37
+ Digest::MD5.hexdigest(code)
38
+ end
39
+
40
+ def exists?
41
+ File.exist?(filename)
42
+ end
43
+
44
+ end
45
+ end