coconut 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -19,16 +19,16 @@ Or install it yourself as:
19
19
  $ gem install coconut
20
20
 
21
21
 
22
- ## What is coconut?
22
+ ## Overview
23
23
  Coconut provides **a simple DSL that allows you to configure your application's
24
- assets on different environments**
24
+ assets on different environments**.
25
25
 
26
26
  ```ruby
27
27
  require 'coconut'
28
28
 
29
29
  Coconut.configure MyApp do
30
30
  twitter do
31
- environment :development do
31
+ environments :development, :test do
32
32
  consumer_key 'development_key'
33
33
  consumer_secret 'development_secret'
34
34
  end
@@ -41,11 +41,8 @@ Coconut.configure MyApp do
41
41
  end
42
42
  ```
43
43
 
44
- You only need to require file from your app. Coconut will define a
44
+ You only need to require the config file from your app. Coconut will define a
45
45
  `config` method on your application's namespace that you can use to query it.
46
- **You don't have to specify the environment when querying for configuration
47
- values**. Coconut will only run the configuration for the environment it's
48
- running on.
49
46
 
50
47
  ```ruby
51
48
  ENV['RACK_ENV'] = :development
@@ -55,28 +52,244 @@ ENV['RACK_ENV'] = :production
55
52
  MyApp.config.twitter.consumer_key # => production_key
56
53
  ```
57
54
 
58
- ## Why coconut?
59
- TODO
55
+ **You don't have to specify the environment when querying for configuration
56
+ values. Coconut will only run the configuration for the environment it's
57
+ running on.**
60
58
 
61
59
 
62
- ## Coconut flavours
63
- TODO
64
- ### Single file
65
- TODO
60
+ ## How is the environment detected?
61
+ Coconut uses the `RACK_ENV` environment variable by default to detect the
62
+ environment the app is running on. If this variable is not set or is empty it
63
+ will default to `:development`.
66
64
 
67
- ### Folder
68
- TODO
65
+ If your ecosystem uses something that is not RACK based you can specify how
66
+ Coconut should find out the environment with the `environment` method on the
67
+ **Application** level
68
+ (See the *Specifying how the environment should be found* section on
69
+ *Application* under *Coconut Anatomy*).
69
70
 
70
- ### List of files
71
- TODO
72
71
 
72
+ ## Coconut Anatomy
73
+ Coconut is composed of **3 different parts**:
73
74
 
74
- ## How is the environment detected?
75
- Right now coconut uses the environment variable `RACK_ENV` to detect the
76
- environment the app is running on. If `RACK_ENV` is not set or is empty it
77
- defaults to `:development`.
75
+ ### 1) Application
76
+ *Application is the top level block* of Coconut. It's composed of the configuration
77
+ of the *Assets* your application has. It may also contain information about
78
+ where Coconut should look for *Asset* configuration files and how it should
79
+ find out which environment the application is running on.
80
+
81
+ You will enter this block when you call the `Coconut.configure` method passing
82
+ in your application's namespace.
83
+
84
+ ```ruby
85
+ require 'coconut'
86
+
87
+ Coconut.configure MyApp do
88
+ # your application configuration
89
+ end
90
+ ```
91
+
92
+ Coconut will then detect the environment,
93
+ run the configuration and create a `Coconut::Config` object with the assets
94
+ and it's properties for the current environment. A `config` method will be
95
+ defined in the namespace passed to `configure`. That method will return the
96
+ `Coconut::Config` object with your application's configuration.
97
+
98
+ That means:
99
+ * You only need to make sure that you require the config file that includes the
100
+ *Application* block for your configuration to be run.
101
+ * Your configuration will be run just once (unless you use `Kernel::load`)
102
+ * If there is a `config` method on your namespace it will be overriden.
103
+ * You will be able to access the configuration from anywhere by calling
104
+ `Namespace::config`.
105
+
106
+ #### Specifying how the environment should be found
107
+ If your application should not use the RACK_ENV environment variable to
108
+ determine which configuration to use you can tell Coconut how to find out:
109
+
110
+ ```ruby
111
+ Coconut.configure MyApp do
112
+ environment { MyApp::find_out_environment }
113
+ # ...
114
+ end
115
+ ```
116
+
117
+ Coconut will call the block passed to the `environment` method to find out
118
+ what environment the application is running on. Therefore **the environment
119
+ will be the return value of that block**.
120
+
121
+
122
+ #### Coconut flavours (a.k.a. how to structure your configuration).
123
+ You can structure the configuration the way that suits you best. Maybe it fits
124
+ in a single file best. Maybe you want to split it and have several configuration
125
+ files. Coconut offers you three different alternatives.
126
+
127
+ #####a) Single file
128
+ If your configuration is small this is probably the best choice. In this case
129
+ your application section will only be composed of *Assets*. You will need
130
+ nothing more. The first example of this README uses this Coconut flavour.
131
+
132
+ #####b) Folder
133
+ If you want to split your configuration in several files and put them in the
134
+ same folder you will be able to tell Coconut how to find them. Coconut will
135
+ load **every file in that folder and run them as if they were Coconut scripts**.
136
+
137
+ If the file that has the *Application* level configuration is in that folder
138
+ too it will be run and will cause an error. You have two options to avoid this:
139
+
140
+ 1. Name that file `config.rb`. By convention Coconut won't run a file in an
141
+ asset folder if it is called like that.
142
+ 2. Put the configuration file that has the *Application* level configuration
143
+ in a different folder. In this case the files in the given folder will only
144
+ have *Asset* configuration.
145
+
146
+ ```ruby
147
+ Coconut.configure MyApp do
148
+ asset_folder 'path/to/folder'
149
+ end
150
+ ```
151
+
152
+ This allows you to write your files focusing only on *Assets*, given that
153
+ when Coconut will run then within the *Application* block. So you could
154
+ have the following project structure:
155
+
156
+ .
157
+ ..
158
+ /config
159
+ config.rb
160
+ database.rb
161
+ oauth.rb
162
+ s3.rb
163
+ /lib
164
+ /spec
165
+
166
+ You could have the *Application* block on the `config.rb` file like this
167
+ (assuming that you run the application from the root of the project):
168
+
169
+ ```ruby
170
+ Coconut.configure MyApp do
171
+ asset_folder 'config'
172
+ end
173
+ ```
174
+
175
+ And each of the asset files would only include asset configuration:
78
176
 
79
- ### Changing the environment
177
+ ```ruby
178
+ database do
179
+ environment :development do
180
+ #...
181
+ end
182
+
183
+ environment :staging, :production do
184
+ #...
185
+ end
186
+ end
187
+ ```
188
+
189
+ You would only need to require the `config/config.rb` file to access the
190
+ configuration.
191
+
192
+ #####c) List of files
193
+
194
+ *NOT DEVELOPED YET: will be available on next version (0.1.1)*
195
+
196
+ This flavour is exactly like the *folder* one with the difference that instead
197
+ of providing the path to the folder to the asset configuration files you provide
198
+ the path to every one of them:
199
+
200
+ ```ruby
201
+ Coconut.configure MyApp do
202
+ asset_files 'database.rb', 'oauth.rb', 's3.rb'
203
+ end
204
+ ```
205
+
206
+ Notice that the paths are relative to where the `config.rb` file is located.
207
+
208
+ ### 2) Asset
209
+ Each of this blocks represent one of the assets of your application. Inside
210
+ each one of them you will include the *Asset* properties on each of the
211
+ environments your application will run on.
212
+
213
+ An *Asset* block consist of the asset name and a Ruby block containing it's
214
+ configuration in each of the environments:
215
+
216
+ ```ruby
217
+ asset_name do
218
+ environment(:production) { property 'value on production' }
219
+ environment(:development, :test) { property 'value on test and development' }
220
+ end
221
+ ```
222
+
223
+ You can specify one or more environments at the same time. You can also use any
224
+ of the aliases the `environment` method has:
225
+
226
+ ```ruby
227
+ env(:development, :test) { property 'value' }
228
+ envs(:development, :test) { property 'value' }
229
+ environment(:development, :test) { property 'value' }
230
+ environments(:development, :test) { property 'value' }
231
+ ```
232
+
233
+ An *Asset* can have *almost* any name. There are some restrictions that apply to
234
+ the names. This restrictions apply to both asset names and property names
235
+ and are explained in the *Environment* section of this README.
236
+
237
+ ### 3) Environment
238
+ Environments are the lowest level of a Coconut configuration. Its composed of
239
+ the configuration properties an asset will have on that environment. So,
240
+ essentially, it's a series of key-value pairs.
241
+
242
+ Properties can have almost any name you can think of. It only has to be a valid
243
+ Ruby method name and you have to avoid those that will collide with calls on
244
+ the `Config` object. Coconut will raise an error if you use one of the reserved
245
+ names:
246
+
247
+ ```ruby
248
+ environment(:development) { object_id 11 }
249
+ # => Coconut::Dsl::InvalidName: object_id can't be used as property name: it will collide with Coconut::Config methods
250
+ ```
251
+
252
+ You can find out the forbidden property (and asset) names by calling the method:
253
+ ```ruby
254
+ Coconut::Dsl::BlankSlate.__forbidden_names
255
+ # => [:respond_to?, :to_hash, :nil?, :===, :=~, :!~, :eql?, :hash, :<=>, :class, :singleton_class, :clone, :dup,
256
+ # :initialize_dup, :initialize_clone, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :freeze,
257
+ # :frozen?, :to_s, :inspect, :methods, :singleton_methods, :protected_methods, :private_methods,
258
+ # :public_methods, :instance_variables, :instance_variable_get, :instance_variable_set,
259
+ # :instance_variable_defined?, :instance_of?, :kind_of?, :is_a?, :tap, :send, :public_send,
260
+ # :respond_to_missing?, :extend, :display, :method, :public_method, :define_singleton_method, :object_id,
261
+ # :to_enum, :enum_for, :==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__, :__id__,
262
+ # :instance_eval, :__send__, :object_id, :__taken?, :__taken_error_message]
263
+ ```
264
+
265
+ If you want to use the methods from `Kernel` that are widely available
266
+ on Ruby objects you will need to prepend the module to the method call:
267
+
268
+ ```ruby
269
+ environment(:development) do
270
+ puts 'here' # declaring a property named "puts"
271
+ Kernel::puts 'here' # printing "here" to STDOUT
272
+ end
273
+ ```
274
+
275
+ Values can be any Ruby object, from a simple integer or string to a lambda.
276
+ You can even use constant or expressions. Keep in mind that the block passed
277
+ to environment will be evaluated in a different context:
278
+ you won't be able to call methods without specifying their receiver.
279
+
280
+ ```ruby
281
+ environment :development do
282
+ four 2 + 2
283
+ arguments ARGV
284
+ encodings Encoding.list
285
+ setup -> do
286
+ enable :sessions, :logging
287
+ disable :dump_errors, :some_custom_option
288
+ end
289
+ end
290
+ ```
291
+
292
+ ## Why coconut?
80
293
  TODO
81
294
 
82
295
  ## Contributing
data/Rakefile CHANGED
@@ -1 +1,30 @@
1
1
  require "bundler/gem_tasks"
2
+ require 'cucumber/rake/task'
3
+ require 'rspec/core/rake_task'
4
+
5
+ namespace :cucumber do
6
+ Cucumber::Rake::Task.new(:ok, 'Run features that should pass') do |t|
7
+ t.cucumber_opts = ['--strict', '--tags ~@wip', '--format progress']
8
+ end
9
+
10
+ Cucumber::Rake::Task.new(:wip, 'Run features that are being worked on') do |t|
11
+ t.cucumber_opts = ['--wip', '--tags @wip']
12
+ end
13
+
14
+ Cucumber::Rake::Task.new(:all, 'Run all features')
15
+ end
16
+
17
+ namespace :rspec do
18
+ desc 'Run specs with progress output'
19
+ RSpec::Core::RakeTask.new(:progress) do |t|
20
+ t.rspec_opts = '--format progress'
21
+ end
22
+
23
+ desc 'Run all specs'
24
+ RSpec::Core::RakeTask.new(:spec)
25
+ end
26
+
27
+ task spec: 'rspec:spec'
28
+ task cucumber: 'cucumber:ok'
29
+ task test: ['rspec:progress', 'cucumber:ok']
30
+ task default: 'test'
@@ -0,0 +1,8 @@
1
+ # Coconut release changes
2
+
3
+ ## Version 0.0.1
4
+ - Single file configuration preview.
5
+
6
+ ## Version 0.1.0
7
+ - Remove Hashie dependency.
8
+ - Add *folder flavour* support.
@@ -17,9 +17,8 @@ Gem::Specification.new do |gem|
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
  gem.require_paths = ["lib"]
19
19
 
20
- gem.add_runtime_dependency 'hashie'
21
-
22
20
  gem.add_development_dependency 'cucumber'
23
21
  gem.add_development_dependency 'rake'
24
22
  gem.add_development_dependency 'rspec'
23
+ gem.add_development_dependency 'yard'
25
24
  end
@@ -0,0 +1,52 @@
1
+ Feature: Load configuration from folder
2
+
3
+ You can split your configuration into asset files, put those files into a
4
+ folder and tell Coconut to load the configuration from there. That
5
+ folder should only include asset files. If it includes some other Ruby file
6
+ Coconut will try to execute it as such and you will get an error.
7
+
8
+ In order to be able to have all the configuration related files in a single
9
+ folder you can adhere to the convention and put your application configuration
10
+ in a file called 'config.rb' inside that folder.
11
+
12
+ If Coconut will ignore that file and it won't be executed as if it was an
13
+ asset file.
14
+
15
+ You can also have your application related configuration in other folder and
16
+ use the file name that you want.
17
+
18
+ Scenario: Application file and assets file under the same folder
19
+ Given I have my application config in "/tmp/coconut_config/config.rb" with content:
20
+ """
21
+ Coconut.configure MyApp do
22
+ asset_folder '/tmp/coconut_config'
23
+ end
24
+ """
25
+ And I have a "s3.rb" asset file on /tmp/coconut_config with content:
26
+ """
27
+ s3 do
28
+ env(:development) { key 'xxx' }
29
+ env(:production) { key 'yyy' }
30
+ end
31
+ """
32
+ When I run my application on the "development" environment
33
+ And I query the configuration for "s3.key"
34
+ Then the configured value should be "xxx"
35
+
36
+ Scenario: Application file and assets file in different folders
37
+ Given I have my application config in "/tmp/coconut.rb" with content:
38
+ """
39
+ Coconut.configure MyApp do
40
+ asset_folder '/tmp/coconut_config'
41
+ end
42
+ """
43
+ And I have a "s3.rb" asset file on /tmp/coconut_config with content:
44
+ """
45
+ facebook do
46
+ env(:development) { key 'xxx' }
47
+ env(:production) { key 'yyy' }
48
+ end
49
+ """
50
+ When I run my application on the "development" environment
51
+ And I query the configuration for "facebook.key"
52
+ Then the configured value should be "xxx"
@@ -1,7 +1,7 @@
1
1
  Feature: Single file configuration
2
2
 
3
3
  Scenario: One environment configuration
4
- Given my application is configured like this:
4
+ Given I have my application config in "/tmp/coconut_config/config.rb" with content:
5
5
  """
6
6
  Coconut.configure(MyApp) do
7
7
  ftp do
@@ -14,3 +14,18 @@ Feature: Single file configuration
14
14
  When I run my application on the "development" environment
15
15
  And I query the configuration for "ftp.user"
16
16
  Then the configured value should be "root"
17
+
18
+ Scenario: Several environments configuration
19
+ Given I have my application config in "/tmp/coconut_config/config.rb" with content:
20
+ """
21
+ Coconut.configure(MyApp) do
22
+ ssh do
23
+ environment(:development, :staging, :production) do
24
+ login 'username'
25
+ end
26
+ end
27
+ end
28
+ """
29
+ When I run my application on the "development" environment
30
+ And I query the configuration for "ssh.login"
31
+ Then the configured value should be "username"
@@ -1,10 +1,15 @@
1
- Given /^my application is configured like this:$/ do |configuration|
2
- @app_config = configuration
1
+ Given /^I have my application config in "(.*?)" with content:$/ do |file, content|
2
+ @config = file
3
+ File.open(file, 'w+') { |file| file.write(content) }
4
+ end
5
+
6
+ Given /^I have a "(.*?)" asset file on (.*?) with content:$/ do |name, folder, content|
7
+ File.open(File.join(folder, name), 'w+') { |file| file.write(content) }
3
8
  end
4
9
 
5
10
  When /^I run my application on the "(.*?)" environment$/ do |environment|
6
11
  ENV['RACK_ENV']= environment
7
- eval @app_config
12
+ eval "load '#{@config}'"
8
13
  end
9
14
 
10
15
  When /^I query the configuration for "(.*?)"$/ do |query|
@@ -2,4 +2,32 @@ $: << File.join(File.dirname(__FILE__), '..', '..', 'lib')
2
2
 
3
3
  require 'coconut'
4
4
 
5
- module MyApp; end
5
+ CONFIG_FOLDER = '/tmp/coconut_config/'
6
+
7
+ Before do
8
+ clean_tmp_folder
9
+ create_folder_for_testing
10
+ create_namespace_for_testing
11
+ end
12
+
13
+ After do
14
+ clean_tmp_folder
15
+ clean_namespace_for_testing
16
+ end
17
+
18
+ def create_namespace_for_testing
19
+ Object.send(:const_set, :MyApp, Module.new)
20
+ end
21
+
22
+ def clean_namespace_for_testing
23
+ Object.send(:remove_const, :MyApp)
24
+ end
25
+
26
+ def clean_tmp_folder
27
+ `rm -rf #{CONFIG_FOLDER}` if File.directory?(CONFIG_FOLDER)
28
+ `rm -f /tmp/coconut.rb` if File.exists?('/tmp/coconut.rb')
29
+ end
30
+
31
+ def create_folder_for_testing
32
+ `mkdir #{CONFIG_FOLDER}`
33
+ end
@@ -1,27 +1,27 @@
1
1
  require_relative 'coconut/version'
2
- require_relative 'coconut/config'
2
+ require_relative 'coconut/dsl/application'
3
3
 
4
4
  module Coconut
5
- def self.configure(namespace, &assets)
6
- raise "#{namespace} already has a config method" if namespace.respond_to? :config
7
- define_config_method namespace, Config.new(current_environment, &assets)
5
+ include Dsl
6
+
7
+ def self.configure(namespace, &config)
8
+ define_config_method namespace, Application.configure(environment, &config)
8
9
  end
9
10
 
10
- def self.current_environment
11
+ def self.environment
12
+ return @__coconut_environment.() unless @__coconut_environment.nil?
11
13
  ENV['RACK_ENV'] || :development
12
14
  end
13
15
 
16
+ def self.take_environment_from(&block)
17
+ @__coconut_environment = block
18
+ end
19
+
14
20
  private
15
21
 
16
22
  def self.define_config_method(namespace, configuration)
17
- singleton_class_of(namespace).instance_eval do
18
- define_method :config do
19
- @_coconut_configuration ||= configuration
20
- end
23
+ namespace.singleton_class.instance_eval do
24
+ define_method(:config) { @__coconut_configuration ||= configuration }
21
25
  end
22
26
  end
23
-
24
- def self.singleton_class_of(class_or_module)
25
- class << class_or_module; self; end
26
- end
27
27
  end
@@ -1,23 +1,53 @@
1
- require 'delegate'
2
- require 'hashie'
3
- require_relative 'asset'
4
- require_relative 'runner'
5
-
6
1
  module Coconut
7
- class Config < DelegateClass(Hashie::Mash)
8
- def initialize(current_environment, &assets_config)
9
- super Hashie::Mash.new
10
- run assets_config, current_environment
2
+ class Config
3
+ def initialize(properties)
4
+ @properties = properties
5
+ @config = config_from(properties)
6
+ end
7
+
8
+ # Coconut::Config objects will respond to methods if its name is
9
+ # equal to any of the configured properties. This method is redefined to
10
+ # make its behaviour consistent with Coconut::Config#method_missing
11
+ #
12
+ # @return [Boolean] whether a config object knows how to respond to method "name"
13
+ def respond_to?(name)
14
+ property?(name) or super
15
+ end
16
+
17
+ # @return [Hash] a Hash representation of the configuration object
18
+ def to_hash
19
+ @properties.dup
11
20
  end
12
21
 
13
22
  private
14
23
 
15
- def run(config, environment)
16
- Runner.new run: config, method_missing: configure_asset_on(environment)
24
+ def config_from(properties)
25
+ properties.each_with_object({}) do |(property, value), config|
26
+ config[property]= config_value_for(value)
27
+ end
28
+ end
29
+
30
+ def config_value_for(value)
31
+ return Config.new(value) if value.is_a? Hash
32
+ value
33
+ end
34
+
35
+ def method_missing(name, *args, &block)
36
+ if property?(name)
37
+ define_property_accessor(name)
38
+ return @config[name]
39
+ end
40
+ super
41
+ end
42
+
43
+ def property?(name)
44
+ @config.has_key?(name)
17
45
  end
18
46
 
19
- def configure_asset_on(environment)
20
- ->(name, *args, &config){ self[name] = Asset.new(environment, &config) }
47
+ def define_property_accessor(name)
48
+ singleton_class.instance_eval do
49
+ define_method(name) { @config[name] }
50
+ end
21
51
  end
22
52
  end
23
53
  end
@@ -0,0 +1,35 @@
1
+ require_relative 'asset'
2
+ require_relative 'asset_folder'
3
+
4
+ module Coconut
5
+ module Dsl
6
+ class Application < BlankSlate
7
+ def self.configure(current_environment, &config)
8
+ new(current_environment).run(&config)
9
+ end
10
+
11
+ def initialize(current_environment)
12
+ @current_environment = current_environment
13
+ end
14
+
15
+ def run(&config)
16
+ @assets_config = {}
17
+ instance_eval &config
18
+ Config.new(@assets_config)
19
+ end
20
+
21
+ private
22
+
23
+ def asset_folder(path)
24
+ instance_eval AssetFolder.config_from(path, IGNORED_FILES)
25
+ end
26
+
27
+ def method_missing(asset, *args, &config)
28
+ ::Kernel::raise InvalidName, __taken_error_message(asset, 'asset name') if __taken?(asset)
29
+ @assets_config[asset] = Asset.configure(@current_environment, &config)
30
+ end
31
+
32
+ IGNORED_FILES = ['config.rb']
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,40 @@
1
+ require_relative '../config'
2
+ require_relative './environment'
3
+
4
+ module Coconut
5
+ module Dsl
6
+ class Asset
7
+ def self.configure(environment, &config)
8
+ new(environment).run(&config)
9
+ end
10
+
11
+ def initialize(current_environment)
12
+ @current_environment = current_environment
13
+ end
14
+
15
+ def run(&config)
16
+ @properties = {}
17
+ instance_eval &config
18
+ @properties
19
+ end
20
+
21
+ private
22
+
23
+ def environment(*environments, &config)
24
+ environments.each { |environment| __configure(environment, config) }
25
+ end
26
+
27
+ alias :env :environment
28
+ alias :envs :environment
29
+ alias :environments :environment
30
+
31
+ def __configure(environment, config)
32
+ @properties.merge! Environment.configure(&config) if __current?(environment)
33
+ end
34
+
35
+ def __current?(environment)
36
+ @current_environment.to_sym == environment.to_sym
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,44 @@
1
+ module Coconut
2
+ module Dsl
3
+ class AssetFolder
4
+ def self.config_from(path, ignored_files)
5
+ new(path, ignored_files).assets_config
6
+ end
7
+
8
+ def initialize(path, ignored_files)
9
+ @path = path
10
+ @ignored_files = ignored_files.map(&method(:path_to))
11
+ end
12
+
13
+ def assets_config
14
+ asset_files_in_folder.map { |file| content(file) }.join "\n"
15
+ end
16
+
17
+ private
18
+
19
+ def asset_files_in_folder
20
+ ruby_files_in_folder - @ignored_files
21
+ end
22
+
23
+ def ruby_files_in_folder
24
+ files_in_folder.select { |file| file.match /\.rb$/ }
25
+ end
26
+
27
+ def files_in_folder
28
+ folder.entries.map(&method(:path_to)).reject { |path| File.directory? path }
29
+ end
30
+
31
+ def folder
32
+ Dir.open(@path)
33
+ end
34
+
35
+ def content(file)
36
+ File.read(file)
37
+ end
38
+
39
+ def path_to(file)
40
+ File.expand_path(File.join(@path, file))
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,33 @@
1
+ require_relative '../config'
2
+
3
+ module Coconut
4
+ module Dsl
5
+ class InvalidName < Exception; end
6
+
7
+ class BlankSlate < BasicObject
8
+ def self.__forbidden_names
9
+ Config.instance_methods + PERMANENT_METHODS
10
+ end
11
+
12
+ private
13
+
14
+ def __taken?(name)
15
+ Config.instance_methods.include? name
16
+ end
17
+
18
+ def __taken_error_message(name, usage)
19
+ "#{name} can't be used as #{usage}: it will collide with Coconut::Config methods"
20
+ end
21
+
22
+ def self.inherited(subclass)
23
+ __eraseable_methods.each{ |method_name| undef_method method_name }
24
+ end
25
+
26
+ def self.__eraseable_methods
27
+ instance_methods - PERMANENT_METHODS
28
+ end
29
+
30
+ PERMANENT_METHODS = [:instance_eval, :__send__, :object_id, :__taken?, :__taken_error_message, :asset_folder]
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ require_relative 'blank_slate'
2
+ require_relative '../config'
3
+
4
+ module Coconut
5
+ module Dsl
6
+ class Environment < BlankSlate
7
+ def self.configure(&config)
8
+ new.configure(&config)
9
+ end
10
+
11
+ def initialize
12
+ @properties = {}
13
+ end
14
+
15
+ def configure(&config)
16
+ instance_eval &config
17
+ @properties
18
+ end
19
+
20
+ private
21
+
22
+ def method_missing(name, *args, &block)
23
+ ::Kernel::raise InvalidName, __taken_error_message(name, 'property name') if __taken?(name)
24
+ @properties[name] = args.first
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,3 +1,3 @@
1
1
  module Coconut
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -2,7 +2,6 @@ require 'coconut'
2
2
 
3
3
  class MyClass; end
4
4
  module MyModule; end
5
- module MyConfig; def self.config; end; end;
6
5
 
7
6
  describe Coconut do
8
7
  it 'defines a config method in the provided namespace' do
@@ -12,9 +11,28 @@ describe Coconut do
12
11
  MyModule.should respond_to(:config)
13
12
  end
14
13
 
15
- it 'raises an error if the namespace already has a config method' do
16
- expect { Coconut.configure(MyConfig, :asset){} }.to raise_error
14
+ it "allows the app's configuration to be run twice" do
15
+ Coconut.configure(MyClass) { asset { env(:development){ property 'initial value' } } }
16
+ Coconut.configure(MyClass) { asset { env(:development){ property 'latest value' } } }
17
+ MyClass::config.asset.property.should eq 'latest value'
17
18
  end
18
- end
19
19
 
20
+ context 'finding out the environment' do
21
+ before { Coconut.instance_variable_set(:@__coconut_environment, nil) }
22
+
23
+ it 'uses the expression provided by the user if any' do
24
+ Coconut.take_environment_from { :somewhere }
25
+ Coconut.environment.should eq :somewhere
26
+ end
27
+
28
+ it 'uses the RACK_ENV environment variable by default' do
29
+ ENV['RACK_ENV'] = 'rack_env'
30
+ Coconut.environment.should eq 'rack_env'
31
+ end
20
32
 
33
+ it 'uses :development as environment if RACK_ENV is not defined' do
34
+ ENV['RACK_ENV'] = nil
35
+ Coconut.environment.should eq :development
36
+ end
37
+ end
38
+ end
@@ -1,34 +1,34 @@
1
1
  require 'coconut/config'
2
2
 
3
3
  describe Coconut::Config do
4
- let(:asset_config) { Coconut::Asset }
4
+ subject { described_class.new(application) }
5
+ let(:application) { Hash[twitter: properties] }
6
+ let(:properties) { Hash[property: 'value', other: 'other'] }
5
7
 
6
- it 'configures a series of assets' do
7
- subject = described_class.new(:current) do
8
- ftp {}
9
- ssh {}
8
+ context 'newly created' do
9
+ it 'has no property methods' do
10
+ (subject.methods - Object.instance_methods).should eq [:to_hash]
10
11
  end
11
- subject.should have_key 'ftp'
12
- subject.should have_key 'ssh'
13
- end
14
12
 
15
- it 'runs the configuration of each asset' do
16
- ftp_config = lambda {}
17
- ssh_config = lambda {}
18
- asset_config.should_receive(:new).with(:current, &ftp_config)
19
- asset_config.should_receive(:new).with(:current, &ssh_config)
20
- described_class.new(:current) do
21
- ftp &ftp_config
22
- ssh &ssh_config
13
+ it 'responds to every property' do
14
+ subject.should respond_to :twitter
23
15
  end
24
16
  end
25
17
 
26
- it 'assets can have (almost) any name' do
27
- subject = described_class.new(:current) do
28
- __id__ {}
29
- equal? {}
30
- inspect {}
18
+ context 'when a property is queried' do
19
+ it 'returns the property value' do
20
+ subject.twitter.property.should eq 'value'
21
+ subject.twitter.other.should eq 'other'
22
+ end
23
+
24
+ it 'defines a new method to access the property' do
25
+ subject.methods.should_not include(:twitter)
26
+ subject.twitter
27
+ subject.methods.should include(:twitter)
31
28
  end
32
- ['__id__', 'equal?', 'inspect'].each { |key| subject.should have_key(key) }
29
+ end
30
+
31
+ it 'can be transformed to a hash' do
32
+ subject.to_hash.should eq application
33
33
  end
34
34
  end
@@ -0,0 +1,24 @@
1
+ require 'coconut/dsl/application'
2
+
3
+ describe Coconut::Dsl::Application do
4
+ it 'creates the config for every asset' do
5
+ config = described_class.new(:current).run do
6
+ asset { environment(:current) { property 'value' } }
7
+ end
8
+ config.asset.property.should eq 'value'
9
+ end
10
+
11
+ it "doesn't allow assets with colliding names" do
12
+ expect {
13
+ described_class.configure(:current) { to_s {} }
14
+ }.to raise_error Coconut::Dsl::InvalidName, /to_s/
15
+ end
16
+
17
+ it 'can load the asset configuration from a folder' do
18
+ path = '.'
19
+ Coconut::Dsl::AssetFolder.stub(:config_from).with(path, ['config.rb']).
20
+ and_return("asset { environment(:current) { property 'value' } }")
21
+ config = described_class.configure(:current) { asset_folder path }
22
+ config.asset.property.should eq 'value'
23
+ end
24
+ end
@@ -0,0 +1,49 @@
1
+ require 'coconut/dsl/asset'
2
+
3
+ describe Coconut::Dsl::Asset do
4
+ it 'creates the asset config for the given environment' do
5
+ asset_config = described_class.configure(:current) do
6
+ environment(:other) { property 'value in other' }
7
+ environment(:current) { property 'value in current' }
8
+ end
9
+ asset_config.fetch(:property).should eq 'value in current'
10
+ end
11
+
12
+ it 'can setup different environments at the same time' do
13
+ asset_config = described_class.configure(:current) do
14
+ environment(:other, :current) { property 'value in other and current' }
15
+ end
16
+ asset_config.fetch(:property).should eq 'value in other and current'
17
+ end
18
+
19
+ it 'can setup environments step by step' do
20
+ asset_config = described_class.configure(:current) do
21
+ environment(:other, :current) { property 'value in other and current' }
22
+ environment(:other) { thing 'thing in other' }
23
+ environment(:current) { thing 'thing in current' }
24
+ end
25
+ asset_config.fetch(:property).should eq 'value in other and current'
26
+ asset_config.fetch(:thing).should eq 'thing in current'
27
+ end
28
+
29
+ it 'allows properties to be overriden' do
30
+ asset_config = described_class.configure(:current) do
31
+ environment(:other, :current) { property 'value in other and current' }
32
+ environment(:current) { property 'only in current' }
33
+ end
34
+ asset_config.fetch(:property).should eq 'only in current'
35
+ end
36
+
37
+ it 'has alternate ways of configuring an environment' do
38
+ asset_config = described_class.configure(:current) do
39
+ env(:current) { property1 1 }
40
+ environment(:current) { property2 2 }
41
+ environments(:current) { property3 3 }
42
+ envs(:current) { property4 4 }
43
+ end
44
+ asset_config.fetch(:property1).should eq 1
45
+ asset_config.fetch(:property2).should eq 2
46
+ asset_config.fetch(:property3).should eq 3
47
+ asset_config.fetch(:property4).should eq 4
48
+ end
49
+ end
@@ -0,0 +1,36 @@
1
+ require 'coconut/dsl/asset_folder'
2
+
3
+ describe 'Assets configuration from files in folder', integration: true do
4
+ let(:path) { '/tmp/coconut-testing/' }
5
+ let(:ignored) { ['config.rb'] }
6
+
7
+ let(:config) { "" }
8
+ let(:s3_config) { "s3 { environment(:current) { property 'p1' } }" }
9
+ let(:db_config) { "database { environment(:current) { property 'p2' } }" }
10
+
11
+ before :all do
12
+ `rm -rf #{path}`
13
+ Dir::mkdir path
14
+ File.open(File.join(path, 'config.rb'), 'w+') { |f| f.write '' }
15
+ File.open(File.join(path, 's3.rb'), 'w+') { |f| f.write s3_config }
16
+ File.open(File.join(path, 'db.rb'), 'w+') { |f| f.write db_config }
17
+ end
18
+
19
+ it 'evals every asset file in the folder as application config' do
20
+ configuration_from_folder.should eq "#{db_config}\n#{s3_config}"
21
+ end
22
+
23
+ context 'with other files that are not Ruby files' do
24
+ before do
25
+ File.open(File.join(path, 'readme.md'), 'w+') { |f| f.write 'readme' }
26
+ end
27
+
28
+ it 'ignores non Ruby files' do
29
+ configuration_from_folder.should eq "#{db_config}\n#{s3_config}"
30
+ end
31
+ end
32
+
33
+ def configuration_from_folder
34
+ Coconut::Dsl::AssetFolder.config_from(path, ignored)
35
+ end
36
+ end
@@ -0,0 +1,9 @@
1
+ require 'coconut/dsl/blank_slate'
2
+
3
+ describe Coconut::Dsl::BlankSlate do
4
+ it 'knows the forbidden names' do
5
+ expected_names = described_class::PERMANENT_METHODS + [:one]
6
+ Coconut::Config.stub(:instance_methods).and_return([:one])
7
+ described_class.__forbidden_names.should include(*expected_names)
8
+ end
9
+ end
@@ -0,0 +1,49 @@
1
+ require 'coconut/dsl/environment'
2
+
3
+ describe Coconut::Dsl::Environment do
4
+ it 'creates an environment config' do
5
+ config = described_class.configure do
6
+ property 'value'
7
+ other 'other'
8
+ end
9
+ config.fetch(:property).should eq 'value'
10
+ config.fetch(:other).should eq 'other'
11
+ end
12
+
13
+ it "doesn't allow properties with colliding names" do
14
+ expect {
15
+ described_class.configure { object_id '11' }
16
+ }.to raise_error Coconut::Dsl::InvalidName, /object_id/
17
+ end
18
+
19
+ it 'can take procs as property values' do
20
+ config = described_class.configure do
21
+ property1 Proc.new { 'value' }
22
+ property2 -> { 'value' }
23
+ property3 Kernel::lambda { 'value' }
24
+ end
25
+ config.fetch(:property1).should be_a_kind_of Proc
26
+ config.fetch(:property2).should be_a_kind_of Proc
27
+ config.fetch(:property3).should be_a_kind_of Proc
28
+ end
29
+
30
+ it 'can take expressions as property values' do
31
+ config = described_class.configure do
32
+ property1 2 + 2
33
+ property2 'aeiou'[0..2]
34
+ property3 [:a, :e, :i, :o, :u].take(3)
35
+ end
36
+ config.fetch(:property1).should eq 4
37
+ config.fetch(:property2).should eq 'aei'
38
+ config.fetch(:property3).should eq [:a, :e, :i]
39
+ end
40
+
41
+ it 'can take constants and scoped method calls as property values' do
42
+ config = described_class.configure do
43
+ arguments Math::PI
44
+ encodings Encoding.list
45
+ end
46
+ config.fetch(:arguments).should eq Math::PI
47
+ config.fetch(:encodings).should eq Encoding.list
48
+ end
49
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coconut
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,17 +9,17 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-24 00:00:00.000000000 Z
12
+ date: 2012-12-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: hashie
15
+ name: cucumber
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
20
20
  - !ruby/object:Gem::Version
21
21
  version: '0'
22
- type: :runtime
22
+ type: :development
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  none: false
@@ -28,7 +28,7 @@ dependencies:
28
28
  - !ruby/object:Gem::Version
29
29
  version: '0'
30
30
  - !ruby/object:Gem::Dependency
31
- name: cucumber
31
+ name: rake
32
32
  requirement: !ruby/object:Gem::Requirement
33
33
  none: false
34
34
  requirements:
@@ -44,7 +44,7 @@ dependencies:
44
44
  - !ruby/object:Gem::Version
45
45
  version: '0'
46
46
  - !ruby/object:Gem::Dependency
47
- name: rake
47
+ name: rspec
48
48
  requirement: !ruby/object:Gem::Requirement
49
49
  none: false
50
50
  requirements:
@@ -60,7 +60,7 @@ dependencies:
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
62
  - !ruby/object:Gem::Dependency
63
- name: rspec
63
+ name: yard
64
64
  requirement: !ruby/object:Gem::Requirement
65
65
  none: false
66
66
  requirements:
@@ -88,20 +88,27 @@ files:
88
88
  - LICENSE.txt
89
89
  - README.md
90
90
  - Rakefile
91
+ - changelog.md
91
92
  - coconut.gemspec
93
+ - features/folder_configuration.feature
92
94
  - features/single_file_configuration.feature
93
95
  - features/step_definitions/configuration_steps.rb
94
96
  - features/support/env.rb
95
97
  - lib/coconut.rb
96
- - lib/coconut/asset.rb
97
98
  - lib/coconut/config.rb
98
- - lib/coconut/environment.rb
99
- - lib/coconut/runner.rb
99
+ - lib/coconut/dsl/application.rb
100
+ - lib/coconut/dsl/asset.rb
101
+ - lib/coconut/dsl/asset_folder.rb
102
+ - lib/coconut/dsl/blank_slate.rb
103
+ - lib/coconut/dsl/environment.rb
100
104
  - lib/coconut/version.rb
101
- - spec/coconut/asset_spec.rb
102
105
  - spec/coconut/coconut_spec.rb
103
106
  - spec/coconut/config_spec.rb
104
- - spec/coconut/environment_spec.rb
107
+ - spec/coconut/dsl/application_spec.rb
108
+ - spec/coconut/dsl/asset_spec.rb
109
+ - spec/coconut/dsl/assets_folder_spec.rb
110
+ - spec/coconut/dsl/blank_slate_spec.rb
111
+ - spec/coconut/dsl/environment_spec.rb
105
112
  homepage: http://github.com/jacegu/coconut
106
113
  licenses: []
107
114
  post_install_message:
@@ -116,7 +123,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
116
123
  version: '0'
117
124
  segments:
118
125
  - 0
119
- hash: 3927338585615885072
126
+ hash: -443559654264598590
120
127
  required_rubygems_version: !ruby/object:Gem::Requirement
121
128
  none: false
122
129
  requirements:
@@ -125,7 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
132
  version: '0'
126
133
  segments:
127
134
  - 0
128
- hash: 3927338585615885072
135
+ hash: -443559654264598590
129
136
  requirements: []
130
137
  rubyforge_project:
131
138
  rubygems_version: 1.8.24
@@ -134,10 +141,15 @@ specification_version: 3
134
141
  summary: Coconut is a simple DSL that allows you to easily write and query your application's
135
142
  configuration with pure Ruby.
136
143
  test_files:
144
+ - features/folder_configuration.feature
137
145
  - features/single_file_configuration.feature
138
146
  - features/step_definitions/configuration_steps.rb
139
147
  - features/support/env.rb
140
- - spec/coconut/asset_spec.rb
141
148
  - spec/coconut/coconut_spec.rb
142
149
  - spec/coconut/config_spec.rb
143
- - spec/coconut/environment_spec.rb
150
+ - spec/coconut/dsl/application_spec.rb
151
+ - spec/coconut/dsl/asset_spec.rb
152
+ - spec/coconut/dsl/assets_folder_spec.rb
153
+ - spec/coconut/dsl/blank_slate_spec.rb
154
+ - spec/coconut/dsl/environment_spec.rb
155
+ has_rdoc:
@@ -1,27 +0,0 @@
1
- require 'delegate'
2
- require 'hashie'
3
- require_relative 'environment'
4
-
5
- module Coconut
6
- class Asset < DelegateClass(Hashie::Mash)
7
- def initialize(current_environment, &config)
8
- super Hashie::Mash.new
9
- run config, current_environment if block_given?
10
- end
11
-
12
- def environment(environment_name, &config)
13
- merge! Environment.new(&config) if current? environment_name
14
- end
15
-
16
- private
17
-
18
- def run(config, environment)
19
- @current_environment = environment
20
- instance_eval &config
21
- end
22
-
23
- def current?(environment)
24
- environment.to_sym == @current_environment.to_sym
25
- end
26
- end
27
- end
@@ -1,22 +0,0 @@
1
- require 'delegate'
2
- require 'hashie'
3
- require_relative 'runner'
4
-
5
- module Coconut
6
- class Environment < DelegateClass(Hashie::Mash)
7
- def initialize(&config)
8
- super Hashie::Mash.new
9
- run config
10
- end
11
-
12
- private
13
-
14
- def run(config)
15
- Runner.new run: config, method_missing: method(:configure_property)
16
- end
17
-
18
- def configure_property(name, values)
19
- self[name] = values.first
20
- end
21
- end
22
- end
@@ -1,28 +0,0 @@
1
- module Coconut
2
- class BlankSlate < BasicObject
3
- private
4
-
5
- def self.inherited(subclass)
6
- eraseable_methods.each{ |method_name| undef_method method_name }
7
- end
8
-
9
- def self.eraseable_methods
10
- instance_methods - PERMANENT_METHODS
11
- end
12
-
13
- PERMANENT_METHODS = [:instance_eval, :__send__, :object_id]
14
- end
15
-
16
- class Runner < BlankSlate
17
- def initialize(options)
18
- @callback = options[:method_missing]
19
- instance_eval &options[:run]
20
- end
21
-
22
- private
23
-
24
- def method_missing(name, *args, &block)
25
- @callback.(name, args, &block)
26
- end
27
- end
28
- end
@@ -1,12 +0,0 @@
1
- require 'coconut/asset'
2
-
3
- describe Coconut::Asset do
4
- it "knows the asset's config on the current environment" do
5
- subject = described_class.new(:current) do
6
- environment(:other) { property 'value on other' }
7
- environment(:current) { property 'value on current' }
8
- end
9
- subject.should have_key 'property'
10
- subject.fetch('property').should eq 'value on current'
11
- end
12
- end
@@ -1,21 +0,0 @@
1
- require 'hashie'
2
- require 'coconut/environment'
3
-
4
- describe Coconut::Environment do
5
- it 'collects method calls as configuration values' do
6
- config = described_class.new { property 'value' }
7
- config.should have_key :property
8
- config.property.should eq 'value'
9
- end
10
-
11
- it 'can have (almost) any property name' do
12
- config = described_class.new do
13
- __id__ 11
14
- equal? 33
15
- inspect 'CoconutJuice'
16
- end
17
- config.fetch('__id__').should eq 11
18
- config.fetch('equal?').should eq 33
19
- config.fetch('inspect').should eq 'CoconutJuice'
20
- end
21
- end