diff --git a/.gitignore b/.gitignore index d87d4be..a7ad821 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,7 @@ spec/reports test/tmp test/version_tmp tmp + +# vim artifacts +*.swp +*.swo diff --git a/README.md b/README.md index 05f6c9b..bd09303 100644 --- a/README.md +++ b/README.md @@ -629,6 +629,25 @@ purposes; the exception is generated if `:retired_at` is set to _anything_.) You can retire events, categories, and entire versions; this system ensures the DSL continues to be a historical record of what things were in the past, as well as what they are today. +### Requiring Properties + +Sometimes it is helpful to enforce the presence of certain properties. For example, imagine you are tracking views of +a set of banners that advertise a set of new features. In order to determine which banners (and therefore features) are +driving conversions, you need to be sure to include `:banner_id` and `:banner_size`. If there are many places in the +code that can call this event, you may forget to pass along these properties. With `:required_properties` an exception +is generated if the keys are not present _or if their values are blank_. + +```ruby +global_events_prefix :ab + +version 1, "2014-02-04" do + category :user do + event :viewed_banner, "2014-02-04", "user creates a brand-new account", :required_properties => [ :banner_id, :banner_size ] + end +end +``` + + ### Adding Notes to Events You can also add notes to events. They must be tagged with the author and the time, and they can be very useful for diff --git a/lib/meta_events/definition/definition_set.rb b/lib/meta_events/definition/definition_set.rb index b6c5583..b81a308 100644 --- a/lib/meta_events/definition/definition_set.rb +++ b/lib/meta_events/definition/definition_set.rb @@ -15,6 +15,7 @@ module Definition class DefinitionSet class BaseError < StandardError; end class RetiredEventError < BaseError; end + class RequiredPropertyMissingError < BaseError; end class << self # Creates an MetaEvents::Definition::DefinitionSet. +source+ can be one of: diff --git a/lib/meta_events/definition/event.rb b/lib/meta_events/definition/event.rb index 5eb72dc..10466b5 100644 --- a/lib/meta_events/definition/event.rb +++ b/lib/meta_events/definition/event.rb @@ -73,8 +73,10 @@ def initialize(category, name, *args, &block) # provide for required properties, property validation, or anything else. def validate!(properties) if retired_at - raise ::MetaEvents::Definition::DefinitionSet::RetiredEventError, "Event #{full_name} was retired at #{retired_at.inspect} (or its category or version was); you can't use it any longer." + raise ::MetaEvents::Definition::DefinitionSet::RetiredEventError, + "Event #{full_name} was retired at #{retired_at.inspect} (or its category or version was); you can't use it any longer." end + validate_properties!(properties) end # Returns, or sets, the description for an event. @@ -142,10 +144,26 @@ def ensure_complete! raise ArgumentError, "You must record when you introduced event #{full_name}, either as an argument, in the options, or using 'introduced'" if (! @introduced) end + def validate_properties!(properties) + properties_with_indifferent_access = properties.with_indifferent_access + if @required_properties + missing_properties = [ ] + @required_properties.each do |required_property| + if properties_with_indifferent_access[required_property].blank? + missing_properties << required_property + end + end + unless missing_properties.empty? + raise ::MetaEvents::Definition::DefinitionSet::RequiredPropertyMissingError, + "Event #{full_name} requires the properties #{@required_properties.join(', ')}. #{missing_properties.join(', ')} were missing or had blank values." + end + end + end + # Called with the set of options (which can be empty) supplied in the constructor; responsible for applying those # to the object properly. def apply_options!(options) - options.assert_valid_keys(:introduced, :desc, :description, :retired_at, :external_name) + options.assert_valid_keys(:introduced, :desc, :description, :retired_at, :external_name, :required_properties) introduced options[:introduced] if options[:introduced] desc options[:desc] if options[:desc] @@ -153,6 +171,7 @@ def apply_options!(options) external_name options[:external_name] if options[:external_name] @retired_at = Time.parse(options[:retired_at]) if options[:retired_at] + @required_properties = Array(options[:required_properties]) if options[:required_properties] end # Called with the arguments (past the category and event name) supplied to the constructor; responsible for diff --git a/meta_events.gemspec b/meta_events.gemspec index 51508cc..2ec5e7f 100644 --- a/meta_events.gemspec +++ b/meta_events.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |spec| spec.add_dependency "json", "~> 1.0" spec.add_dependency "activesupport", ">= 3.0" - spec.add_development_dependency "bundler", "~> 1.5" + spec.add_development_dependency "bundler", "~> 1.11.2" spec.add_development_dependency "rake" spec.add_development_dependency "rspec", "~> 2.14" end diff --git a/spec/meta_events/definition/event_spec.rb b/spec/meta_events/definition/event_spec.rb index 4336968..953d297 100644 --- a/spec/meta_events/definition/event_spec.rb +++ b/spec/meta_events/definition/event_spec.rb @@ -66,6 +66,14 @@ allow(category).to receive(:retired_at).and_return(Time.parse("2013-02-01")) expect { instance.validate!(:foo => :bar) }.to raise_error(::MetaEvents::Definition::DefinitionSet::RetiredEventError, /2013/) end + + it "should fail if required properties are missing" do + expect { klass.new(category, :foo, "2016-1-1", "foobar", :required_properties => [ :foo ]).validate!(:baz => :bar) }.to raise_error(::MetaEvents::Definition::DefinitionSet::RequiredPropertyMissingError, /foo/) + end + + it "should fail if required properties have blank values" do + expect { klass.new(category, :foo, "2016-1-1", "foobar", :required_properties => [ :foo ]).validate!(:foo => '') }.to raise_error(::MetaEvents::Definition::DefinitionSet::RequiredPropertyMissingError, /foo/) + end end it "should return and allow setting its description via #desc" do @@ -143,5 +151,15 @@ expect(instance.external_name).to eq("my name") end end + + context "with required properties" do + it "should work with strings and symbols" do + expect do + event = klass.new(category, :foo, "2016-1-1", "foobar", :required_properties => [ "string", :symbol ]) + event.validate!('string' => "foo", :symbol => "foo") + event.validate!(:string => "foo", "symbol" => "foo") + end.to_not raise_error(::MetaEvents::Definition::DefinitionSet::RequiredPropertyMissingError) + end + end end end