diff --git a/.gitignore b/.gitignore index 3a62e1a..5204516 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ coverage/ doc/api/ pkg/ tmp/**/* +start_mongod.sh +spec/fixtures/db/ +doc diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9b0e70b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: ruby +rvm: + - 1.9.2 + - 1.9.3 +#script: bundle exec rake test +script: bundle exec rake spec +#before_install: + #- gem update --system + #- gem --version +env: + - USE_PLYMOUTH="no" +branches: + only: + - master +notifications: + email: + recipients: + - dsiw@dsiw-it.de + on_succes: change +# [always|never|change] + on_failure: always diff --git a/Gemfile.lock b/Gemfile.lock index df0ef2d..79f2d8f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - app_config (1.0.1) + app_config (2.0.2) GEM remote: http://rubygems.org/ @@ -39,6 +39,6 @@ DEPENDENCIES maruku mongo rake - rspec (~> 2.10.0) + rspec simplecov yard diff --git a/README.md b/README.md index 1872fc7..ceebff5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# AppConfig +# AppConfig [![Build Status](https://secure.travis-ci.org/DSIW/app_config.png)](http://travis-ci.org/DSIW/app_config) [![Dependency Status](https://gemnasium.com/DSIW/app_config.png)](https://gemnasium.com/DSIW/app_config) An easy to use, customizable library to easily store and retrieve application (or library) configuration, API keys or basically anything in 'key/value' pairs. @@ -10,21 +10,40 @@ Usage is simple. Just pass either a hash of options, or a block, to {AppConfig. In it's simplest form, you can use it like so: - AppConfig.setup(:admin_email => 'admin@example.com') - # ..or.. - AppConfig.setup do |config| + config = AppConfig.setup do |config| config[:admin_email] = 'admin@example.com' + # Other setter: + # config["admin_email"] = 'admin@example.com' + # config.admin_email = 'admin@example.com' end - # Strings or symbols as keys. - AppConfig[:admin_email] # => 'admin@example.com' + # Strings or symbols or dynamic methods as keys. + config[:admin_email] #=> 'admin@example.com' + config["admin_email"] #=> 'admin@example.com' + config.admin_email #=> 'admin@example.com' You may also specify the storage method along with options specific to that storage method. Check the [wiki](https://github.com/Oshuma/app_config/wiki) for more usage examples. +If you like to use the {AppConfig::Configurable} module, see how easy it is: + + module Client + include Configurable + + def set_config + # Use it everywhere you want + config.api_url = "http://example.com/api/" + config.api_key = "2H28dsaa4" + config.api_version = "2" + end + end + +Please scroll down to the last chapter for more informtation and details. + + ## Deprecation Note -Version `1.0` is \***not**\* backwards compatible with `0.7.1`! See the [wiki](https://github.com/Oshuma/app_config/wiki) +Version `2.0` is ***not*** backwards compatible with `1.0` and `0.7.1`! See the [wiki](https://github.com/Oshuma/app_config/wiki) for upgrade instructions. @@ -39,14 +58,34 @@ Given this YAML file: Use it like so: - AppConfig.setup(:yaml => '/path/to/app_config.yml') + config = AppConfig.setup(:yaml => '/path/to/app_config.yml') # Later on... - # Strings or symbols as keys. - AppConfig['admin_email'] # => 'admin@example.com' - AppConfig[:api_name] # => 'Supr Webz 2.0' - AppConfig[:api_key] # => 'SUPERAWESOMESERVICE' + # Strings or symbols or dynamic methods as keys. + config['admin_email'] #=> 'admin@example.com' + config.api_name #=> 'Supr Webz 2.0' + config[:api_key] #=> 'SUPERAWESOMESERVICE' + +### Create the YAML file, if it doesn't exist +I like to create the YAML file, if it doesn't exist + + config = AppConfig.setup(:yaml => '/path/to/file_doesnt_exist.yml', :create => true) + # File will be created with all non-existing directories. + + # Later on... + config['admin_email'] = 'admin@example.com' + config['admin_email'] #=> 'admin@example.com' + +### Save all changes automatically + +When you use your config sometimes, it would be nice, if your config will be saved automatically to your YAML file. + + config = AppConfig.setup(:yaml => '/path/to/app_config.yml', :save_changes => true) + + # Later on... + config['admin_email'] #=> 'admin@example.com' + # YAML file will be written and includes 'admin_email'. ## AppConfig::Storage::Mongo @@ -60,13 +99,12 @@ constant for the default Mongo connection options. :collection => 'app_config' # default } - AppConfig.setup(:mongo => mongo_opts) + config = AppConfig.setup(:mongo => mongo_opts) - AppConfig[:admin_email] - # => 'admin@example.com' + config[:admin_email] #=> 'admin@example.com' # Override an existing value (saves to the database): - AppConfig[:admin_email] = 'other_admin@example.com' + config[:admin_email] = 'other_admin@example.com' The values are read/saved (by default) to the `app_config` database and `app_config` collection. These defaults can be overridden, however, which @@ -75,7 +113,6 @@ might lend well to versioned configurations; collection names such as AppConfig.setup(:mongo => { :collection => 'app_config_v2' }) - ## Environment Mode There's also an 'environment mode' where you can organize the config @@ -91,12 +128,44 @@ sort of like the Rails database config. Given this YAML file: Set the `:env` option to your desired environment. # Rails.root/config/initializers/app_config.rb - AppConfig.setup({ + config = config.setup({ :yaml => "#{Rails.root}/config/app_config.yml", :env => Rails.env # or any string }) # Uses the given environment section of the config. - AppConfig[:title] + config[:title] # => 'Production Mode' +## Module Configurable + +You can include the module `Configurable` to have access to your configuration in all your code. +This module is created to include it in all classes where you want to have access to your configuration. + + +### Forcing or reloading + +Your access by _config_ is cached. So if you'd like to have another object, you could do this by passing :force or :reload. + + config.object_id #=> 123456 + config(:force => true).object_id #=> 654321 + # ..or.. + config(:reload => true) + +#### New object, because :force is set + config(:force => true) + +#### New object, because :force is set and block is given + config(:force => true) { |config| config[:key] = "value" } + +#### New object, because :force isn't set and block is given + config { |config| config[:key] = "value" } + +#### No new object, because :force is false and block is given + config(:force => false) { |config| config[:key] = "value" } + +#### Creates a new object and YAML file will be empty + config(:yaml => "path/to/file.yml", :force => true, :save_changes => true) + File.read("path/to/file.yml") #=> "--- {}" + +See {AppConfig::Configurable#config} for datails. diff --git a/app_config.gemspec b/app_config.gemspec index feae4f1..f1a1aad 100644 --- a/app_config.gemspec +++ b/app_config.gemspec @@ -16,7 +16,7 @@ Gem::Specification.new do |s| s.add_development_dependency 'maruku' s.add_development_dependency 'mongo' s.add_development_dependency 'rake' - s.add_development_dependency 'rspec', '~> 2.10.0' + s.add_development_dependency 'rspec' s.add_development_dependency 'simplecov' s.add_development_dependency 'yard' diff --git a/lib/app_config.rb b/lib/app_config.rb index fcc4f36..90a34e6 100644 --- a/lib/app_config.rb +++ b/lib/app_config.rb @@ -1,81 +1,93 @@ require 'core_ext/hashish' +# @example Usage +# config = AppConfig.setup do |config| +# config[:key] = "value" +# config[:newkey] = "newvalue" +# end +# config[:tmp] = "/tmp" +# config[:newkey] #=> "newvalue" +# config.tmp #=> "/tmp" +# +# config.key # raises ArgumentError, because Hash#key needs an argument. +# # But you can use: +# config._key #=> "value" module AppConfig - VERSION = '1.0.2' + # Version of this Gem. + VERSION = '2.0.3' - autoload :Error, 'app_config/error' - autoload :Storage, 'app_config/storage' + autoload :Error, 'app_config/error' + autoload :Storage, 'app_config/storage' + autoload :Configurable, 'app_config/configurable' + autoload :Force, 'util/force' class << self - # Accepts an `options` hash or a block. + # Accepts an _options_ hash or a block. # See each storage method's documentation for their specific options. # # Valid storage methods: - # * `:memory` - {AppConfig::Storage::Memory AppConfig::Storage::Memory} - # * `:mongo` - {AppConfig::Storage::Mongo AppConfig::Storage::Mongo} - # * `:yaml` - {AppConfig::Storage::YAML AppConfig::Storage::YAML} - def setup(options = {}, &block) - @@options = options - - if @@options[:yaml] - @@storage = AppConfig::Storage::YAML.new(@@options.delete(:yaml)) - elsif @@options[:mongo] - @@storage = AppConfig::Storage::Mongo.new(@@options.delete(:mongo)) + # * *:memory* - {AppConfig::Storage::Memory AppConfig::Storage::Memory} + # * *:mongo* - {AppConfig::Storage::Mongo AppConfig::Storage::Mongo} + # * *:yaml* - {AppConfig::Storage::YAML AppConfig::Storage::YAML} + # + # @param options [Hash] + # + # It will be configured by the _options_ hash. _options_ can have the following + # keys: + # * *:yaml*: Path to YAML file. + # * *:mongo*: Options for MongoDB database. + # * *:create*: This will create the specified YAML file. (:yaml has to be included.) + # * *:save_changes*: Every change in your storage object will be saved in your YAML file. (:yaml has to be included.) + # + # @yieldparam [Storage] config + # + # @note Your yielded block will be returned. + # + # @example Initialization without + # config = AppConfig.setup + # config.class #=> AppConfig::Storage::Memory + # + # @example Initialization with :yaml + # config = AppConfig.setup(:yaml => "/path/to/file.yml") + # config.class #=> AppConfig::Storage::YAML + # + # @example Initialization with :mongo + # config = AppConfig.setup(:mongo => {:username => "usename", :password => "password"}) + # config.class #=> AppConfig::Storage::Mongo + # + # @return [Storage] config + def setup(options = {}) + @options = options + + if @options[:yaml] + @storage = AppConfig::Storage::YAML.new(@options.delete(:yaml), @options) + elsif @options[:mongo] + @storage = AppConfig::Storage::Mongo.new(@options.delete(:mongo)) else - @@storage = AppConfig::Storage::Memory.new(@@options) + @storage = AppConfig::Storage::Memory.new({}, @options) end - yield @@storage if block_given? + yield storage if block_given? - to_hash + storage end - # Returns `true` if {AppConfig.setup AppConfig.setup} has been called. + # @return true if {AppConfig.setup AppConfig.setup} has been called. def setup? - !!(defined?(@@storage) && !@@storage.empty?) - end - - # Clears the `@@storage`. - def reset! - if defined?(@@storage) - remove_class_variable(:@@storage) - true - else - false - end - end - - # Access the configured `key`'s value. - def [](key) - setup unless setup? - storage[key] - end - - # Set a new `value` for `key` (persistence depends on the type of Storage). - def []=(key, value) - setup unless setup? - storage[key] = value - end - - def empty? - storage.empty? - end - - def to_hash - setup? ? storage.to_hash : Hashish.new + !!(defined?(@storage) && !@storage.empty?) end private def environment - (@@options[:environment] || @@options[:env]) || nil + (@options[:environment] || @options[:env]) || nil end alias_method :env, :environment - # Returns the `@@storage` contents, which is what is exposed as the configuration. + # @return the `@storage` contents, which is what is exposed as the configuration. def storage - environment ? @@storage[environment] : @@storage + environment ? @storage[environment] : @storage end end # self diff --git a/lib/app_config/configurable.rb b/lib/app_config/configurable.rb new file mode 100644 index 0000000..b9d8939 --- /dev/null +++ b/lib/app_config/configurable.rb @@ -0,0 +1,118 @@ +module AppConfig + # This module is created to include it in all classes where you would like to have access to your configuration. + # + # You access by _config_ is cached. So if you'd like to have another object, you could do this by passing :force => + # true like: + # @example Creates a new object with :force + # config.object_id #=> 123456 + # config(:force => true).object_id #=> 654321 + module Configurable + # + # + # @param options [Hash] + # + # It will be configured by the _options_ hash. _options_ can have the following + # keys: + # * *:yaml*: Path to YAML file. + # * *:mongo*: Options for MongoDB. + # * *:create*: This will create the specified YAML file. (:yaml has to be included.) + # * *:save_changes*: Every change in your storage object will be saved in your YAML file. (:yaml has to be included.) + # * *:force*: Creates a new object. + # * *:reload*: Creates a new object. + # + # @example Creates a new object + # config.object_id #=> 123456 + # config(:force => true).object_id #=> 654321 + # config(:reload => true).object_id #=> 101010 + # + # @example Creates a new object and YAML file will be empty + # config(:yaml => "path/to/file.yml", :force => true, :save_changes => true) + # File.read("path/to/file.yml") #=> "--- {}" + # + # @example Usage with block + # config(:yaml => "path/to/file.yml") do |config| + # config[:key] = "value" + # end + # config[:key] #=> "value" + # + # @example New object with :force is set + # config.object_id #=> 123456 + # config(:force => true) + # # New object + # config.object_id #=> 654321 + # + # @example New object with :force is set and block is given + # config.object_id #=> 123456 + # config(:force => true) { |config| config[:key] = "value" } + # # New object + # config.object_id #=> 654321 + # + # @example New object with :force isn't set and block is given + # config.object_id #=> 123456 + # config { |config| config[:key] = "value" } + # # New object + # config.object_id #=> 654321 + # + # @example Old object with :force is false and block is given + # config.object_id #=> 123456 + # config(:force => false) { |config| config[:key] = "value" } + # # No new object + # config.object_id #=> 123456 + # + # @yield configuration + # @yieldparam config [AppConfig::Storage] + # @return [AppConfig::Storage] config + def config(options={}) + @options = options + @block_given = block_given? + @config = nil if !@config.nil? && refresh? + @config ||= AppConfig.setup(@options) do |config| + config.clear! + yield config if @block_given + end + end + alias _config config + + private + + # Checks if object has to be a new one. + # + # @example :force is set + # config(:force => true) + # refresh? #=> true + # + # @example :force is set and block is given + # config(:force => true) { |config| config[:key] = "value" } + # refresh? #=> true + # + # @example :force isn't set and block is given + # config { |config| config[:key] = "value" } + # refresh? #=> true + # + # @example :force is false and block is given + # config(:force => false) { |config| config[:key] = "value" } + # refresh? #=> false + def refresh? + force = Force.new(@options) + @options.merge!(:force => true) if @block_given && !force.set? + return false if force.false? + force.true? + end + + # Check if old config object options aren't equals with actual options + # + # @note useless + def options_changed? + old_options = @config.options + new_options = @options + # removes all Force::KEYS + Force::KEYS.each do |key| + old_options.delete(key) + new_options.delete(key) + end + old_options != new_options + rescue NoMethodError + true + end + end +end diff --git a/lib/app_config/storage/base.rb b/lib/app_config/storage/base.rb index e27c348..cd69cd4 100644 --- a/lib/app_config/storage/base.rb +++ b/lib/app_config/storage/base.rb @@ -1,15 +1,36 @@ module AppConfig module Storage + # This class is the base class for all other storages. class Base + # @attr Hash options hash + attr_reader :options - def initialize(options) + def initialize(data, options={}) + @data = Hashish.new(data) @options = options end + # Converts data to hash. + # + # @return [Hashish] hash def to_hash Hashish.new(@data.to_hash) end + # Converts data to YAML. + # + # @return [::YAML] yaml + def to_yaml + to_hash.to_yaml + end + + private + + # Each method will be sent to {Hashish} object. + def method_missing(method, *args, &blk) + @data.send(method, *args, &blk) + end + end # Base end # Storage end # AppConfig diff --git a/lib/app_config/storage/memory.rb b/lib/app_config/storage/memory.rb index 93261e5..75d0da1 100644 --- a/lib/app_config/storage/memory.rb +++ b/lib/app_config/storage/memory.rb @@ -1,21 +1,16 @@ module AppConfig module Storage + # This storage saves it data in memory (RAM). There is no save possibility. class Memory < Storage::Base - def initialize(options) - @data = Hashish.new(options) - end - - def [](key) - @data[key] - end - - def []=(key, value) - @data[key] = value - end - - def empty? - @data.empty? + # Instantiates a new Storage::Memory. + # + # @param data will be saved in an {Hashish} + # @param options options hash + # + # @return [Memory] + def initialize(data, options) + super(data, options) end end # Memory diff --git a/lib/app_config/storage/mongo.rb b/lib/app_config/storage/mongo.rb index 06f578d..dfda657 100644 --- a/lib/app_config/storage/mongo.rb +++ b/lib/app_config/storage/mongo.rb @@ -1,4 +1,5 @@ module AppConfig + # This storage saves it data in a MongoDB database. You can write and read it. module Storage require 'mongo' @@ -6,6 +7,7 @@ module Storage # Mongo storage method. class Mongo < Storage::Base + # All default information for access to your database. DEFAULTS = { :host => 'localhost', :port => '27017', @@ -15,26 +17,45 @@ class Mongo < Storage::Base :password => nil } + # Connects and loads data of your database. + # + # + # @param options options hash + # Your can use it to set your username and password and override all default options in _DEFAULTS_. + # + # It will be configured by the _options_ hash. _options_ can have the following + # keys: + # * *:host*: Host of your running MongoDB instance. + # * *:port*: Port number of your running MongoDB instance. + # * *:database*: Name of your database. + # * *:collection*: Name of your collection. + # * *:user*: Your username of your running MongoDB instance. + # * *:password*: Your password of your running MongoDB instance. + # See {Mongo::DEFAULTS default options}. def initialize(options) - @connected = false - @options = DEFAULTS.merge(options) + super({}, DEFAULTS.merge(options)) setup_connection fetch_data! end - def [](key) - @data[key] - end - + # {include:Storage::Base#[]=} + # Writes changes to your database. + # + # @param key [String, Symbol] + # @param value [String, Symbol] + # + # @example + # mem = Storage::Mongo.new + # mem[:newkey] = "newvalue" + # mem[:newkey] #=> "newvalue + # + # @note It could overwrite your collection in your database. def []=(key, value) - @data[key] = value + # @override + super(key, value) save! end - def empty? - @data.empty? - end - private def save! diff --git a/lib/app_config/storage/yaml.rb b/lib/app_config/storage/yaml.rb index d609320..7313a4b 100644 --- a/lib/app_config/storage/yaml.rb +++ b/lib/app_config/storage/yaml.rb @@ -1,32 +1,125 @@ module AppConfig + # This storage saves it data in a YAML file. You can write and read it. module Storage - require 'yaml' - # YAML storage method. + # YAML storage. It can create your file + # + # @example Usage with :create and :save_changes + # File.exist?("path/to/file.yml") #=> false + # config = Storage::YAML.new("path/to/file.yml", :create => true, :save_changes => true) + # File.exist?("path/to/file.yml") #=> true + # File.read("path/to/file.yml").empty? #=> true + # config[:newkey] = "newvalue" + # config[:newkey] #=> "newvalue" + # File.read("path/to/file.yml").empty? #=> false + # config.save! + # + # @example Usage without :create and :save_changes + # File.exist?("path/to/file.yml") #=> false + # # Create directories _path_ and _to_... + # # Touch file.yml + # config = Storage::YAML.new("path/to/file.yml") + # File.read("path/to/file.yml").empty? #=> true + # config[:newkey] = "newvalue" + # config[:newkey] #=> "newvalue" + # File.read("path/to/file.yml").empty? #=> true + # config.save! + # File.read("path/to/file.yml").empty? #=> false class YAML < Storage::Base + # @example + # yaml = Storage::YAML.new("path/to/file.yml", :create => true, :save_changes => true) + # yaml.path #=> "path/to/file.yml" + attr_reader :path + # Default path to your YAML file. DEFAULT_PATH = File.expand_path(File.join(ENV['HOME'], '.app_config.yml')) - # Loads `@data` with the YAML file located at `path`. + # Loads @data with the YAML file located at `path`. # `@data` will be the Hashish that is accessed with `AppConfig[:key]`. # # Defaults to `$HOME/.app_config.yml` - def initialize(path = DEFAULT_PATH) - # Make sure to use the top-level YAML module here. - @data = Hashish.new(::YAML.load_file(path)) - end - def [](key) - @data[key] + # @param path Path to YAML file + # @param options options hash + # + # It will be configured by the _options_ hash. _options_ can have the following + # keys: + # * *:create*: This will create the specified YAML file even it doesn't exist. + # Each directory and the file will be created. + # * *:save_changes*: Every change in your storage object will be saved in your YAML file. + # + def initialize(path = DEFAULT_PATH, options={}) + super({}, options) + @path = path + create_file if @options[:create] + @data = Hashish.new(load_yaml) end + # {include:Storage::Base#[]=} + # + # @param key [String, Symbol] + # @param value [String, Symbol] + # + # @example + # mem = Storage::YAML.new({}) + # mem[:newkey] = "newvalue" + # mem[:newkey] #=> "newvalue + # + # @note It could overwrite your YAML file if option :save_changes is set. def []=(key, value) - @data[key] = value + # @override + super(key, value) + save! if save_changes? + end + + # Clears data and saves it if option :save_changes is set. This means that your YAML file is empty. + def clear! + @data.clear + save! if save_changes? end + alias :reset! :clear! - def empty? - @data.empty? + # Checks if option :save_changes is set. + # + # @example + # yaml = Storage::YAML.new("path/to/file.yml", :save_changes => true) + # yaml.save? #=> true + # + # @example + # yaml = Storage::YAML.new("path/to/file.yml") + # yaml.save_changes? #=> false + # + # @return true if option :save_changes is set. + def save_changes? + !!@options[:save_changes] + end + + # Writes your configuration in file which is located in your specified path. + # + # @param file [String] Path of file, which will be written. + # + # @note It could overwrite your YAML file. + def save!(file=@path) + # @override + to_hash.save!(file, :format => :yaml) + end + + private + + # Creates non-existing directories and the file is empty. + def create_file + require 'fileutils' + dirname = File.dirname(@path) + FileUtils.mkdir_p dirname + FileUtils.touch @path + end + + # Loads content of YAML file. + # @return [Hash] hash + def load_yaml + # Make sure to use the top-level YAML module here. + ::YAML.load(File.read(@path)) || {} end end # YAML diff --git a/lib/core_ext/hashish.rb b/lib/core_ext/hashish.rb index dcc0ab2..4d5ae92 100644 --- a/lib/core_ext/hashish.rb +++ b/lib/core_ext/hashish.rb @@ -1,8 +1,13 @@ # Stolen from Rails Active Support and renamed to Hashish. # # This class has dubious semantics and we only have it so that -# people can write `params[:key]` instead of `params['key']` -# and they get the same value for both keys. +# people can get the same value for key as String and Symbol. See example: +# +# @example +# hashish = Hashish.new({:key => "value"}) +# hashish[:key] #=> "value" +# hashish["key"] #=> "value" + class Hashish < Hash def initialize(constructor = {}) if constructor.is_a?(Hash) @@ -26,18 +31,19 @@ def default(key = nil) # Assigns a new value to the hash: # - # hash = HashWithIndifferentAccess.new + # hash = Hashish.new # hash[:key] = "value" - def []=(key, value) + def store(key, value) regular_writer(convert_key(key), convert_value(value)) end + alias :[]= :store # Updates the instantized hash with values from the second: # - # hash_1 = HashWithIndifferentAccess.new + # hash_1 = Hashish.new # hash_1[:key] = "value" # - # hash_2 = HashWithIndifferentAccess.new + # hash_2 = Hashish.new # hash_2[:key] = "New Value!" # # hash_1.update(hash_2) # => {"key"=>"New Value!"} @@ -50,7 +56,7 @@ def update(other_hash) # Checks the hash for a key matching the argument passed in: # - # hash = HashWithIndifferentAccess.new + # hash = Hashish.new # hash["key"] = "value" # hash.key? :key # => true # hash.key? "key" # => true @@ -62,14 +68,21 @@ def key?(key) alias_method :has_key?, :key? alias_method :member?, :key? - # Fetches the value for the specified key, same as doing `hash[key]`. + # Fetches the value for the specified key. + # + # @example + # hashish = Hashish.new({:key => "value"}) + # hashish.fetch(:key) #=> "value" + # hashish.fetch("key") #=> "value" + # hashish[:key] #=> "value" + # hashish["key"] #=> "value" def fetch(key, *extras) super(convert_key(key), *extras) end # Returns an array of the values at the specified indices: # - # hash = HashWithIndifferentAccess.new + # hash = Hashish.new # hash[:a] = "x" # hash[:b] = "y" # hash.values_at("a", "b") # => ["x", "y"] @@ -79,7 +92,7 @@ def values_at(*indices) # Returns an exact copy of the hash. def dup - HashWithIndifferentAccess.new(self) + Hashish.new(self) end # Merges the instantized and the specified hashes together, giving precedence to the values from the second hash @@ -94,6 +107,10 @@ def reverse_merge(other_hash) super other_hash.with_indifferent_access end + # {include:#reverse_merge} It replaces the actual object. + # + # @see #reverse_merge + # def reverse_merge!(other_hash) replace(reverse_merge( other_hash )) end @@ -108,10 +125,76 @@ def symbolize_keys!; self end def to_options!; self end # Convert to a Hash with String keys. + # + # @example + # hashish = Hashish.new({:key => 'value', :four => 20}) + # hashish.to_hash #=> {"key" => "value", "four" => 20} + # + # @return [Hash] hash def to_hash Hash.new(default).merge!(self) end + # Converts hash to YAML. + # + # @example + # hashish = Hashish.new({:key => 'value', :four => 20}) + # hashish.to_yaml #=> "---\nkey: value\nfour: 20\n" + # + # @return [::YAML] yaml + def to_yaml + require 'yaml' + to_hash.to_yaml + end + + # Converts hash to JSON. + # + # @example + # hashish = Hashish.new({:key => 'value', :four => 20}) + # hashish.to_json #=> "{\"key\":\"value\",\"four\":20}" + # + # @return [JSON] json + def to_json + require 'json' + to_hash.to_json + end + + alias_method :reset, :clear + + # Writes your configuration in file which is located in your specified path. + # + # @param file [String] Path of file, which will be written. + # @param options [Hash] + # + # It will be configured by the _options_ hash. _options_ can have the following + # keys: + # * *:format*: Format (YAML, JSON, Hash) of converted data. + # + # @raise [ArgumentError] if no format is set. + # + # @example Save as YAML + # hashish.save!("path/to/file.yml", :format => :yaml) + # @example Save as JSON + # hashish.save!("path/to/file.json", :format => :json) + # @example Save as Hash + # hashish.save!("path/to/file.txt", :format => :hash) + # + # @note It could overwrite your file. + # + # @see #to_yaml + # @see #to_json + # @see #to_hash + # + # @return true if file could be written. + def save!(file, options={}) + raise ArgumentError, "You have to specify :format" unless options[:format] + # :format => :to_format + format = "to_#{options[:format].downcase}".to_sym + content = send(format) + File.open(file, 'w') { |f| f.puts content } + true + end + protected def convert_key(key) @@ -129,9 +212,48 @@ def convert_value(value) end end + private + + # You have access by method. + # + # @note You use Hash. Use methods with prefix "_" to have definitely access to values. + # + # @example Normal usage + # hashish = Hashish.new({:newkey => "value"}) + # hashish[:newkey] #=> "value" + # hashish.newkey #=> "value" + # hashish._newkey #=> "value" + # # with dynamic setter: + # hashish.anotherkey = "anothervalue" + # hashish.anotherkey #=> "anothervalue" + # hashish._somekey = "somevalues" + # hashish.somekey #=> "somevalues" + # + # @example Problem with Hash methods like Hash#key + # hashish = Hashish.new({:key => "value"}) + # hashish[:key] #=> "value" + # config.key # raises ArgumentError, because Hash#key needs an argument. + # # But you can use: + # config._key #=> "value" + def method_missing(method, *args, &blk) + prefix = '_' + method = method[prefix.length..-1] if method.to_s.start_with? prefix + + if args.empty? + key = method + fetch(method, *args) if has_key? key + elsif method.to_s.end_with?("=") + # :newkey= => :newkey + key = method[0..-2].to_sym + value = args.first + store(key, value) + end + end + end # Hashish class Hash + # Converts Hash to {Hashish}. def with_indifferent_access hash = Hashish.new(self) hash.default = self.default diff --git a/lib/util/force.rb b/lib/util/force.rb new file mode 100644 index 0000000..5958e9d --- /dev/null +++ b/lib/util/force.rb @@ -0,0 +1,87 @@ +# You get information about your force items in your options hash. +class Force + + # This array includes symbols which will be used for checking. + KEYS = [:force, :reload] + + # @example + # Force.new({:a => b, :force => true, :c => d}) + def initialize(options) + @options = options + end + + # Checks if options hash includes any force key + # + # @example + # Force.new({:force => true}).set? #=> true + # Force.new({:force => false}).set? #=> true + # Force.new({}).set? #=> false + # + # @return true if any key is set + def set? + values.any? { |o| !o.nil? } + end + + # Checks if options hash doesn't include any force key + # + # @see #set? + # @example + # Force.new({:force => true}).nil? #=> false + # Force.new({:force => false}).nil? #=> false + # Force.new({}).nil? #=> true + # + # @return true if no key is set + def nil? + !set? + end + + # Checks if options hash includes one of the force keys and if its value is true + # + # @example + # Force.new({:force => true}).true? #=> true + # Force.new({:force => false}).true? #=> false + # Force.new({}).true? #=> false + # + # @return true if any force key is set and its value is true + def true? + set? && values.any? + end + + # Checks if options hash includes one of the force keys and if its value is false + # + # @example + # Force.new({:force => true}).false? #=> false + # Force.new({:force => false}).false? #=> true + # Force.new({}).false? #=> false + # + # @return true if any force key is set and its value is false + def false? + set? && !true? + end + + # String representation + # + # @example Stringify with :force + # Force.new({:force => true}).to_s #=> "true" + # Force.new({:force => false}).to_s #=> "false" + # Force.new({}).to_s #=> "unset" + # + # @example Stringify with :reload + # Force.new({:reload => true}).to_s #=> "true" + # Force.new({:reload => false}).to_s #=> "false" + # Force.new({}).to_s #=> "unset" + # + # @return [String] value + def to_s + return "true" if true? + return "false" if false? + return "unset" if nil? + end + + private + + # Select only the force keys + def values + KEYS.map { |o| @options[o] } + end +end diff --git a/spec/app_config/configurable_spec.rb b/spec/app_config/configurable_spec.rb new file mode 100644 index 0000000..45aa560 --- /dev/null +++ b/spec/app_config/configurable_spec.rb @@ -0,0 +1,112 @@ +require 'spec_helper' + +describe Configurable do + include Configurable + + before :each do + config(:force => true) + end + + it "should be kind of Storage::Base" do + config.should be_kind_of AppConfig::Storage::Base + end + + it "should be another object if force" do + obj_id = config.object_id + config(:force => true) + config.object_id.should_not eq obj_id + end + + it "should be another object if reload" do + obj_id = config.object_id + config(:reload => true) + config.object_id.should_not eq obj_id + end + + it "should eval block if force or reload is set" do + config.clear + config(:force => true) { |config| config[:newkey] = :newvalue } + config.should include(:newkey) + end + + it "should eval block if force or reload isn't set" do + config { |config| config[:newkey] = :newvalue } + config.should include(:newkey) + end + + it "shouldn't eval block if force or reload is false" do + config(:force => false) { |config| config[:newkey] = :newvalue } + config.should_not include(:newkey) + end + + it "should set values from block" do + # Preparing: file isn't empty + config(:yaml => temp_config_file) { |config| config[:newkey] = :newvalue } + config.should include(:newkey) + end + + it "should clear data and save it if force or reload is set" do + # Preparing: file isn't empty + config_file = temp_config_file + config({ + :yaml => config_file, + :save_changes => true, + }) { |config| config[:newkey] = :newvalue } + config.should be_kind_of Storage::YAML + config.should include(:newkey) + load_yaml_of_config(config).should include("newkey") + + # clears file, because :force is set + config({ + :yaml => config_file, + :force => true, + :save_changes => true + }) + config.should be_kind_of Storage::YAML + + load_yaml_of_config(config).should be_empty + + # save + config({ + :yaml => config_file, + :force => true, + :save_changes => true + }) { |config| config[:newkey] = :newvalue } + config.should be_kind_of Storage::YAML + config.should include(:newkey) + load_yaml_of_config(config).should include("newkey") + end + + it "should eval block if block is given" do + config { |config| config[:newkey] = :newvalue } + config.should include(:newkey) + end + + it "should not eval block if option 'force' is false" do + config(:force => false) { |config| config[:newkey] = :newvalue } + config.should_not include(:newkey) + end + + it "should set a new key value pair []=" do + config.should_not include(:newkey) + config[:newkey] = :newvalue + config.should include(:newkey) + end + + it "should set a new key value pair by store()" do + config.should_not include(:newkey_store) + config.store(:newkey_store, :newvalue_store) + config.should include(:newkey_store) + end + + it "should set a new key value pair by dynamic method" do + config.should_not include(:newkey_dyn) + config.newkey_dyn = :newvalue_dyn + config.should include(:newkey_dyn) + end + + it "should be the same object in each access" do + @obj_id = config.object_id + config.object_id.should eq @obj_id + end +end diff --git a/spec/app_config/storage/mongo_spec.rb b/spec/app_config/storage/mongo_spec.rb index 73ee096..1db9243 100644 --- a/spec/app_config/storage/mongo_spec.rb +++ b/spec/app_config/storage/mongo_spec.rb @@ -1,29 +1,23 @@ require 'spec_helper' -# TODO: Drop the Mongo test db before running specs. describe AppConfig::Storage::Mongo do - - before(:all) do - AppConfig.reset! - config_for_mongo - end + let(:config) { config_for_mongo } it 'should have some values' do - AppConfig[:api_key].should_not be_nil + config[:api_key].should_not be_nil end it 'should update the values' do - AppConfig.class_variable_get(:@@storage).should_receive(:save!) - AppConfig[:api_key] = 'SOME_NEW_API_KEY' - AppConfig[:api_key].should == 'SOME_NEW_API_KEY' + config.should_receive(:save!) + config[:api_key] = 'SOME_NEW_API_KEY' + config[:api_key].should == 'SOME_NEW_API_KEY' end it 'should not have the Mongo _id in storage' do - AppConfig['_id'].should be_nil + config['_id'].should be_nil end it 'should have a @_id variable for the Mongo ID' do - AppConfig.class_variable_get(:@@storage). - instance_variable_get(:@_id).should_not be_nil + config.instance_variable_get(:@_id).should_not be_nil end end diff --git a/spec/app_config/storage/yaml_spec.rb b/spec/app_config/storage/yaml_spec.rb index 4b0f868..1069b5c 100644 --- a/spec/app_config/storage/yaml_spec.rb +++ b/spec/app_config/storage/yaml_spec.rb @@ -3,8 +3,8 @@ describe AppConfig::Storage::YAML do it 'should have some values' do - config_for_yaml - AppConfig[:api_key].should_not be_nil + config = config_for_yaml + config[:api_key].should_not be_nil end it 'should raise file not found' do @@ -13,10 +13,31 @@ end.should raise_error(Errno::ENOENT) end + it 'should create file if its option is set' do + file = fixture('does_not_exist.yml') + config = config_for_yaml(:yaml => file, :create => true) + File.exist?(file).should be_true + config[:api_key] = 'api_key' + AppConfig.should be_setup + File.delete file + end + + it "should reset config" do + config = config_for_yaml + config.reset! + config.should be_empty + end + it 'saves the new value in memory' do - config_for_yaml - AppConfig[:new_key] = 'new value' - AppConfig[:new_key].should == 'new value' + config = AppConfig.setup + config[:new_key] = 'new value' + config[:new_key].should == 'new value' end + it 'saves the new value in file' do + config = example_yaml_config(:save_changes => true) + config[:new_key] = 'new value' + config2 = config_for_yaml(:yaml => config.path) + config2[:new_key].should == 'new value' + end end diff --git a/spec/app_config/storage_spec.rb b/spec/app_config/storage_spec.rb index 27330f6..b09e3c5 100644 --- a/spec/app_config/storage_spec.rb +++ b/spec/app_config/storage_spec.rb @@ -1,4 +1,20 @@ require 'spec_helper' -describe AppConfig::Storage do +describe AppConfig::Storage::Base do + subject { AppConfig::Storage::Base.new({}) } + + it 'should set new pairs by store()' do + subject.store(:key, 'value') + subject[:key].should == 'value' + end + + it 'should set new pairs by []=' do + subject[:key] = 'value' + subject[:key].should == 'value' + end + + it 'should set new pairs by dynamic method' do + subject.key = 'value' + subject[:key].should == 'value' + end end diff --git a/spec/app_config_spec.rb b/spec/app_config_spec.rb index 7fe5840..f3f01b6 100644 --- a/spec/app_config_spec.rb +++ b/spec/app_config_spec.rb @@ -14,38 +14,36 @@ AppConfig.should respond_to(:setup?) end - it 'responds to .reset!()' do - AppConfig.should respond_to(:reset!) - end - it 'should have to_hash' do - config_for_yaml - AppConfig.to_hash.class.should == Hashish + config = config_for_yaml + config.to_hash.class.should == Hashish end it 'should reset @@storage' do # configure first - config_for_yaml(:api_key => 'API_KEY') + config = config_for_yaml(:api_key => 'API_KEY') # then reset - AppConfig.reset! - AppConfig[:api_key].should be_nil + config.reset! + config[:api_key].should be_nil end it 'to_hash() returns an empty hash if storage not set' do - AppConfig.reset! - AppConfig.to_hash.should == {} + config = config_for_yaml + config.reset! + config.to_hash.should == {} end describe 'environment mode' do it 'should load the proper environment' do - config_for_yaml(:yaml => fixture('env_app_config.yml'), + config = config_for_yaml(:yaml => fixture('env_app_config.yml'), :env => 'development') - AppConfig[:api_key].should_not be_nil + config[:api_key].should_not be_nil end end it 'should not be setup' do - AppConfig.reset! + config = config_for_yaml + config.reset! AppConfig.should_not be_setup end @@ -56,20 +54,41 @@ it 'should create nested keys' do pending 'Implement nested keys' - AppConfig.reset! - AppConfig.setup + config = AppConfig.setup - AppConfig[:name][:first] = 'Dale' - AppConfig[:name][:first].should == 'Dale' + config[:name][:first] = 'Dale' + config[:name][:first].should == 'Dale' end - it 'returns a Hashish on setup' do - AppConfig.reset! + it 'returns a Storage::Memory on setup' do config = AppConfig.setup do |c| c[:name] = 'Dale' c[:nick] = 'Oshuma' end - config.should be_instance_of(Hashish) + config.should be_instance_of(Storage::Memory) end + it 'saves data as Storage::YAML by Hashish' do + config = example_yaml_config + config.save! + check_save_config(config.to_yaml, config.path) + end + + it 'should be able to access values by methods' do + config = AppConfig.setup do |c| + c.store(:key, 'value') + c.four = 20 + c[:name] = 'Dale' + end + + config.four.should == 20 + config._four.should == 20 + + config.name.should == 'Dale' + config._name.should == 'Dale' + + # Hash#key(value) is already defined (see Hashish#method_missing) + expect { config.key }.to raise_error(ArgumentError) + config._key.should == 'value' + end end diff --git a/spec/core_ext/hashish_spec.rb b/spec/core_ext/hashish_spec.rb index 6214710..2892d71 100644 --- a/spec/core_ext/hashish_spec.rb +++ b/spec/core_ext/hashish_spec.rb @@ -6,6 +6,47 @@ @symbols = { :key => 'value', :four => 20 } end + describe "Setter" do + subject { Hashish.new } + + it 'should set new pairs by store()' do + subject.store(:key, 'value') + subject[:key].should == 'value' + end + + it 'should set new pairs by []=' do + subject[:key] = 'value' + subject[:key].should == 'value' + end + + it 'should set new pairs by dynamic method' do + subject.key = 'value' + subject[:key].should == 'value' + end + + it 'should set new pairs by dynamic method with prefix' do + subject._key = 'value' + subject[:key].should == 'value' + end + end + + describe "Convert to other formats" do + subject { Hashish.new(@symbols) } + + it "should convert to JSON" do + subject.to_json.should eq "{\"key\":\"value\",\"four\":20}" + end + + it "should convert to YAML" do + subject.to_yaml.should eq "---\nkey: value\nfour: 20\n" + end + + it "should convert to Hash" do + hash = {"key" => "value", "four" => 20} + subject.to_hash.should eq hash + end + end + it 'should not give a fuck about symbols' do hashish = Hashish.new(@strings) hashish[:key].should == 'value' @@ -15,4 +56,33 @@ hashish = Hashish.new(@symbols) hashish['key'].should == 'value' end + + it 'should be convertable to YAML with strings' do + hashish = Hashish.new(@strings) + hashish.to_yaml.should eq "---\nkey: value\nfour: 20\n" + end + + it 'should be convertable to YAML with symbols' do + hashish = Hashish.new(@symbols) + hashish.to_yaml.should eq "---\nkey: value\nfour: 20\n" + end + + it 'should be saveable' do + config_file = temp_config_file + + hashish = Hashish.new(@symbols) + hashish.save!(config_file, :format => :yaml) + hashish.to_yaml.should eq File.read(config_file) + end + + it 'should be able to access values by methods' do + hashish = Hashish.new(@strings) + + hashish.four.should == 20 + hashish._four.should == 20 + + # Hash#key(value) is already defined + expect { hashish.key }.to raise_error(ArgumentError) + hashish._key.should == 'value' + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5fe0480..fc70316 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -15,15 +15,39 @@ def fixture(name) end # AppConfig.setup wrapper. Accepts a hash of +options+. - def config_for(options) - AppConfig.reset! - AppConfig.setup(options) + def config_for(options, &blk) + AppConfig.setup(options, &blk) end # Setup YAML options and pass to config_for(). - def config_for_yaml(opts = {}) + def config_for_yaml(opts = {}, &blk) path = opts[:yaml] || fixture('app_config.yml') - config_for({ :yaml => path }.merge(opts)) + config_for({ :yaml => path }.merge(opts), &blk) + end + + def temp_config_file + require 'tempfile' + Tempfile.new('config') + end + + def example_yaml_config(options={}) + options = {:yaml => temp_config_file}.update(options) + config = AppConfig.setup(options) do |c| + c[:name] = 'Dale' + c[:nick] = 'Oshuma' + yield c if block_given? + end + config.should be_instance_of(Storage::YAML) + config[:date] = Date.today + config + end + + def check_save_config(yaml, file) + yaml.should eq File.read(file) + end + + def load_yaml_of_config(config) + YAML.load(File.read(config.path.path)) end def config_for_mongo(opts = {}, load_test_data = true) @@ -32,6 +56,7 @@ def config_for_mongo(opts = {}, load_test_data = true) :database => 'app_config_test', }) begin + remove_mongo_test_collection(mongo) load_mongo_test_config(mongo) if load_test_data config_for({:mongo => mongo}.merge(opts)) rescue Mongo::ConnectionFailure @@ -54,4 +79,11 @@ def load_mongo_test_config(options) collection.save(test_data) end end + + def remove_mongo_test_collection(options) + connection = ::Mongo::Connection.new(options[:host], options[:port].to_i) + database = connection.db(options[:database]) + collection = database.collection(options[:collection]) + collection.remove + end end diff --git a/spec/util/force_spec.rb b/spec/util/force_spec.rb new file mode 100644 index 0000000..542c744 --- /dev/null +++ b/spec/util/force_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' +require './lib/util/force' + +describe Force do + context "when hash includes :force => true" do + options_hash = {:force => true} + subject { Force.new(options_hash) } + + it "should be set" do + subject.should be_set + end + + it "should not be nil" do + subject.should_not be_nil + end + + it "should be true" do + subject.true?.should be_true + end + + it "should not be false" do + subject.false?.should be_false + end + + it "#to_s" do + subject.to_s.should eq "true" + end + end + + context "when hash includes :force => false" do + options_hash = {:force => false} + subject { Force.new(options_hash) } + + it "should be set" do + subject.should be_set + end + + it "should not be nil" do + subject.should_not be_nil + end + + it "should not be true" do + subject.true?.should be_false + end + + it "should be false" do + subject.false?.should be_true + end + + it "#to_s" do + subject.to_s.should eq "false" + end + end + + context "when hash doesn't include :force" do + options_hash = {:key => :value} + subject { Force.new(options_hash) } + + it "should not be set" do + subject.should_not be_set + end + + it "should be nil" do + subject.should be_nil + end + + it "should not be true" do + subject.true?.should be_false + end + + it "should not be false" do + subject.false?.should be_false + end + + it "#to_s" do + subject.to_s.should eq "unset" + end + end +end