conduit 1.0.2 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/bin/conduit +5 -0
  3. data/lib/conduit.rb +2 -0
  4. data/lib/conduit/blueprints/actions/base.tt +21 -0
  5. data/lib/conduit/blueprints/base.tt +9 -0
  6. data/lib/conduit/blueprints/bin/generate_action.tt +5 -0
  7. data/lib/conduit/blueprints/decorators.tt +2 -0
  8. data/lib/conduit/blueprints/decorators/base.tt +9 -0
  9. data/lib/conduit/blueprints/driver.tt +14 -0
  10. data/lib/conduit/blueprints/generators/action.tt +61 -0
  11. data/lib/conduit/blueprints/generators/blueprints/action.tt +13 -0
  12. data/lib/conduit/blueprints/generators/blueprints/decorator.tt +6 -0
  13. data/lib/conduit/blueprints/generators/blueprints/mock.tt +7 -0
  14. data/lib/conduit/blueprints/generators/blueprints/parser.tt +8 -0
  15. data/lib/conduit/blueprints/generators/blueprints/rspec_action.tt +4 -0
  16. data/lib/conduit/blueprints/parsers/base.tt +121 -0
  17. data/lib/conduit/blueprints/request_mocker.tt +2 -0
  18. data/lib/conduit/blueprints/request_mocker/base.tt +15 -0
  19. data/lib/conduit/blueprints/spec_helper.tt +8 -0
  20. data/lib/conduit/blueprints/support/helpers.tt +12 -0
  21. data/lib/conduit/blueprints/views/layout.tt +1 -0
  22. data/lib/conduit/cli.rb +146 -0
  23. data/lib/conduit/core/action.rb +90 -26
  24. data/lib/conduit/core/decorator.rb +22 -0
  25. data/lib/conduit/core/request_mocker.rb +89 -0
  26. data/lib/conduit/version.rb +1 -1
  27. data/spec/classes/core/action_spec.rb +67 -0
  28. data/spec/classes/core/request_mocker_spec.rb +45 -0
  29. data/spec/support/my_driver/request_mockers/fixtures/foo/failure.xml.erb +4 -0
  30. data/spec/support/my_driver/request_mockers/fixtures/foo/success.xml.erb +4 -0
  31. data/spec/support/my_driver/request_mockers/foo.rb +13 -0
  32. data/spec/support/xml/failure_mock.xml +4 -0
  33. data/spec/support/xml/success_mock.xml +4 -0
  34. data/spec/support/xml/xml_modified_request.xml +7 -0
  35. data/spec/support/xml/xml_request.xml +2 -2
  36. metadata +69 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ae57ae862cb89c3d6032192b7b6222021568d612
4
- data.tar.gz: 25b4c001510e51d24235fd591c86b38284d56840
3
+ metadata.gz: ff4f0e5f7f702375659f970ab9ed86f2ce673ca7
4
+ data.tar.gz: 88ebd51ff6273ba6dad93c01d60f0b78851ddfec
5
5
  SHA512:
6
- metadata.gz: f2ee68b71fbbef5c52dce91b3d9edc8e7add2186d658f8e69b757c00e0ef8c41fd9bfad33e880ae9dfebe9e0539244d9f749efd1d6284751133c714c6a7caa03
7
- data.tar.gz: d33c3ffb5dd8c8bd900cd5ff0decc534939ef9334e0ae2b9160c82bfe007557c13858169b75b17f8245ec455fb471d2a62da9a7168e17fd51998b103f0f62ed0
6
+ metadata.gz: 6106383b60779d51424c96d3319c439a76498c6bc33918f4cdf5363deb6da42666144054210b93bcc0bdb44f4f24e75aa80b9cfd2138f2472d65c977b596a129
7
+ data.tar.gz: 2b6eeb302b22fc3d3e91eb46555d3ad5eeb3c6e698288f2c1624733b02cf39de54a22456a833f5fef969d9ac1e815fcd2d0d5b1d65ce696e9fcafd5669aedc37
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require "bundler/setup"
3
+ require "conduit/cli"
4
+
5
+ Conduit::CLI.start
@@ -23,6 +23,8 @@ module Conduit
23
23
  #
24
24
  autoload :Connection, 'conduit/core/connection'
25
25
  autoload :Render, 'conduit/core/render'
26
+ autoload :Decorator, 'conduit/core/decorator'
27
+ autoload :RequestMocker, 'conduit/core/request_mocker'
26
28
  autoload :Action, 'conduit/core/action'
27
29
  autoload :Parser, 'conduit/core/parser'
28
30
  autoload :Driver, 'conduit/core/driver'
@@ -0,0 +1,21 @@
1
+ #
2
+ # Base action class that all driver actions should extend.
3
+ # Common actions functionality goes here.
4
+ #
5
+
6
+ module Conduit::Driver::<%= classified_name %>
7
+ class Base < Conduit::Core::Action
8
+ class << self
9
+ def inherited(base)
10
+ base.send :required_attributes, *Conduit::Driver::<%= classified_name %>.credentials
11
+ base.send :required_attributes, *Conduit::Driver::<%= classified_name %>.required_attributes
12
+ base.send :optional_attributes, *Conduit::Driver::<%= classified_name %>.optional_attributes
13
+ end
14
+ end
15
+
16
+ def remote_url
17
+ # TODO: Define API endpoint here:
18
+ # http://example.com/api
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ require "conduit"
2
+ require "conduit/<%= underscored_name %>/driver"
3
+ require "conduit/<%= underscored_name %>/version"
4
+ require "conduit/<%= underscored_name %>/request_mocker"
5
+ require "conduit/<%= underscored_name %>/decorators"
6
+
7
+ Conduit.configure do |config|
8
+ config.driver_paths << File.join(File.dirname(__FILE__), "<%= underscored_name %>")
9
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require "bundler/setup"
3
+ require "conduit/<%= underscored_name %>/generators/action"
4
+
5
+ Conduit::<%= classified_name %>::Generators::Action.start
@@ -0,0 +1,2 @@
1
+ mocks = File.join(File.dirname(__FILE__), "decorators", "*.rb")
2
+ Dir.glob(mocks) { |fname| require fname }
@@ -0,0 +1,9 @@
1
+ #
2
+ # Base decorator class that all action decorators should extend.
3
+ # Common decorator functionality goes here.
4
+ #
5
+
6
+ module Conduit::<%= classified_name %>::Decorators
7
+ class Base < Conduit::Core::Decorator
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ require "conduit/<%= underscored_name %>/actions/base"
2
+
3
+ module Conduit
4
+ module Driver
5
+ # Conduit Driver for <%= classified_name %>
6
+ module <%= classified_name %>
7
+ extend Conduit::Core::Driver
8
+
9
+ # TODO: replace `required_credentials` and `optional_attributes` as needed
10
+ required_credentials :username, :password
11
+ optional_attributes :mock_status
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,61 @@
1
+ require "thor/group"
2
+ require "active_support"
3
+ require "byebug"
4
+
5
+ module Conduit
6
+ module <%= classified_name %>
7
+ module Generators
8
+ class Action < Thor::Group
9
+ include Thor::Actions
10
+
11
+ attr_reader :classified_name
12
+ attr_reader :underscored_name
13
+
14
+ argument :name, type: :string
15
+
16
+ def initialize(*args)
17
+ super
18
+ @classified_name = ActiveSupport::Inflector.classify name
19
+ @underscored_name = ActiveSupport::Inflector.underscore name
20
+ @driver_path = "#{File.dirname(__FILE__)}/.."
21
+ @spec_path = "#{@driver_path}/../../../spec"
22
+ end
23
+
24
+ def copy_action
25
+ template("action.tt", "#{@driver_path}/actions/#{@underscored_name}.rb")
26
+ end
27
+
28
+ def copy_decorator
29
+ template("decorator.tt", "#{@driver_path}/decorators/#{@underscored_name}.rb")
30
+ end
31
+
32
+ def copy_parser
33
+ template("parser.tt", "#{@driver_path}/parsers/#{@underscored_name}.rb")
34
+ end
35
+
36
+ def copy_mocks
37
+ template("mock.tt", "#{@driver_path}/request_mocker/#{@underscored_name}.rb")
38
+ create_file "#{@driver_path}/request_mocker/fixtures/responses/#{@underscored_name}/success.xml.erb"
39
+ create_file "#{@driver_path}/request_mocker/fixtures/responses/#{@underscored_name}/failure.xml.erb"
40
+ end
41
+
42
+ def copy_view
43
+ create_file "#{@driver_path}/views/#{@underscored_name}.erb"
44
+ end
45
+
46
+ def add_action_to_driver
47
+ insert_into_file("#{@driver_path}/driver.rb", "\n action :#{@underscored_name}\n", after: "optional_attributes :mock_status\n")
48
+ end
49
+
50
+ def add_spec_files
51
+ template("rspec_action.tt", "#{@spec_path}/conduit/<%= underscored_name %>/actions/#{@underscored_name}_spec.rb")
52
+ create_file "#{@spec_path}/fixtures/requests/#{@underscored_name}/request.xml"
53
+ end
54
+
55
+ def self.source_root
56
+ "#{File.dirname(__FILE__)}/blueprints"
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,13 @@
1
+ require_relative "base"
2
+
3
+ module Conduit::Driver::<%= classified_name %>
4
+ class <%%= classified_name %> < Conduit::Driver::<%= classified_name %>::Base
5
+ operation :<%%= underscored_name %>
6
+
7
+ # Required attributes go here
8
+ # required_attributes :required_attribute_name
9
+
10
+ # Optional attributes go here
11
+ # optional_attributes :optional_attribute_name
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ require_relative "base"
2
+
3
+ module Conduit::<%= classified_name %>::Decorators
4
+ class <%%= classified_name %>Decorator < Base
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ require_relative "base"
2
+
3
+ module Conduit::<%= classified_name %>::RequestMocker
4
+ # Request mocker for <%%= underscored_name %>.
5
+ class <%%= classified_name %> < Base
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ require_relative "base"
2
+
3
+ module Conduit::Driver::<%= classified_name %>
4
+ class <%%= classified_name %>::Parser < Parser::Base
5
+ # Specify attributes here:
6
+ # attribute :attr_name
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ require "spec_helper"
2
+
3
+ describe <%%= classified_name %> do
4
+ end
@@ -0,0 +1,121 @@
1
+ #
2
+ # Base parser class that all request parsers should extend.
3
+ # Common parser functionality goes here.
4
+ #
5
+ # It uses nokogiri to extract attributes from the xml response.
6
+ #
7
+
8
+ require "nokogiri"
9
+ require "active_support"
10
+
11
+ module Conduit::Driver::<%= classified_name %>
12
+ module Parser
13
+ # Provides the base functionality for parsing
14
+ # XML responses from the api
15
+ class Base < Conduit::Core::Parser
16
+ attr_accessor :xml
17
+
18
+ # Extracts the camelized attribute from the xml response
19
+ def self.attribute(attr_name, &block)
20
+ block ||= lambda do
21
+ content_for "//#{ActiveSupport::Inflector.camelize(attr_name, false)}"
22
+ end
23
+ super(attr_name, &block)
24
+ end
25
+
26
+ def initialize(xml)
27
+ @xml = xml
28
+ end
29
+
30
+ def root
31
+ @root ||= Nokogiri::XML(xml).tap do |doc|
32
+ doc.remove_namespaces!
33
+ end
34
+ end
35
+
36
+ # Get the attribute value at a certain path
37
+ def content_for(path, doc = root)
38
+ node = doc.at_xpath(path)
39
+ node.content if node
40
+ end
41
+
42
+ # Returns the following:
43
+ #
44
+ # - `acknowledged` when async
45
+ # - `success` when successful
46
+ # - `failure` when unsuccessful
47
+ def response_status
48
+ if response_errors.any?
49
+ "failure"
50
+ elsif in_progress?
51
+ "acknowledged"
52
+ else
53
+ "success"
54
+ end
55
+ end
56
+
57
+ # Returns normalized request errors if present.
58
+ def response_errors
59
+ if response_content?
60
+ if !content_for("//Fault").nil?
61
+ [normalized_fault]
62
+ else
63
+ # Returns empty array if no errors
64
+ normalized_errors
65
+ end
66
+ else
67
+ [Conduit::Error.new(message: "Unexpected response from server.")]
68
+ end
69
+ end
70
+
71
+ # Indicates if content present.
72
+ def response_content?
73
+ # TODO: replace this if response not present in `Body`
74
+ !content_for("//Body").nil?
75
+ end
76
+
77
+ private
78
+
79
+ # Indicates if a request is in progress. Used for async requests.
80
+ # Should be overwritten in the action's parser in case true.
81
+ def in_progress?
82
+ false
83
+ end
84
+
85
+ # Returns a new `Conduit::Error` instance containing the fault text
86
+ # and the fault code.
87
+ def normalized_fault
88
+ # TODO: replace this if error message and code not present in the
89
+ # below paths
90
+ Conduit::Error.new({
91
+ message: content_for("//Fault//Text"),
92
+ code: content_for("//Fault//Code//Value")
93
+ })
94
+ end
95
+
96
+ # Extracts the error from either `errorMessage` or `errorText`
97
+ def error_response
98
+ # TODO: replace this if error response not present in any of the following
99
+ # paths.
100
+ content_for("//errorMessage") || content_for("//errorText")
101
+ end
102
+
103
+ # Returns a new `Conduit::Error` instance containing the error message and
104
+ # its code.
105
+ def normalized_errors
106
+ # TODO: replace this if error not present in `error_message` and code
107
+ # not present in `errorNumber`
108
+ if error_response && !error_response.empty?
109
+ [
110
+ Conduit::Error.new({
111
+ message: error_response,
112
+ code: content_for("errorNumber")
113
+ })
114
+ ]
115
+ else
116
+ []
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,2 @@
1
+ mocks = File.join(File.dirname(__FILE__), "request_mocker", "*.rb")
2
+ Dir.glob(mocks) { |fname| require fname }
@@ -0,0 +1,15 @@
1
+ #
2
+ # Base request mocker class that all mockers should extend.
3
+ # Common mock functionality goes here.
4
+ #
5
+
6
+ module Conduit::<%= classified_name %>::RequestMocker
7
+ class Base < Conduit::Core::RequestMocker
8
+ private
9
+
10
+ # Default path for fixtures.
11
+ def fixtures_path
12
+ "#{__dir__}/fixtures/responses"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ require "rspec/its"
2
+ require "conduit/<%= underscored_name %>"
3
+ require "rspec/matchers"
4
+ require "equivalent-xml"
5
+
6
+ include Conduit::Driver::<%= classified_name %>
7
+
8
+ Dir[File.join(Dir.pwd, "spec/support/**/*.rb")].each { |file| require file }
@@ -0,0 +1,12 @@
1
+ module Helpers
2
+ def credentials
3
+ {
4
+ mock_status: :success
5
+ }
6
+ end
7
+ end
8
+
9
+ RSpec.configure do |config|
10
+ # rspec configuration goes here
11
+ config.include Helpers
12
+ end
@@ -0,0 +1 @@
1
+ <%%= yield %>
@@ -0,0 +1,146 @@
1
+ require "thor"
2
+ require "active_support"
3
+ require "bundler"
4
+
5
+ module Conduit
6
+ class CLI < Thor
7
+ include Thor::Actions
8
+
9
+ attr_accessor :driver_name, :classified_name, :underscored_name, :base_path
10
+
11
+ desc "generate_driver DRIVER_NAME", "Generates a new xml `conduit-driver` gem"
12
+ long_desc <<-LONGDESC
13
+ `generate_driver DRIVER_NAME` will generate a new gem for the xml conduit
14
+ driver.
15
+
16
+ It will add the base `action`, `decorator`, `parser` and `request_mocker`
17
+ files along with generators and other basic files.
18
+ LONGDESC
19
+ def generate_driver driver_name
20
+ @driver_name = driver_name
21
+ @dasherized_name = ActiveSupport::Inflector.dasherize driver_name
22
+ @classified_name = ActiveSupport::Inflector.classify driver_name
23
+ @underscored_name = ActiveSupport::Inflector.underscore driver_name
24
+ @pwd = Dir.pwd
25
+ @base_path = "#{@pwd}/conduit-#{@dasherized_name}"
26
+
27
+ run("bundle gem conduit-#{@underscored_name} --no-coc --mit --test=rspec")
28
+ fix_names
29
+ copy_files
30
+ modify_files
31
+ run("chmod +x #{@base_path}/bin/generate_action")
32
+ bundle_install
33
+ end
34
+
35
+ private
36
+
37
+ # In case the name of the driver is composed of multiple words we need to
38
+ # generate the gem with an underscored named (in order to avoid nested modules
39
+ # for each part of the name) and then replace the gem and base file name with
40
+ # the dasherized one.
41
+ # For example, if the driver is called `best-wireless` we create `conduit-best_driver`
42
+ # gem and we change the name to `conduit-best-driver`
43
+ def fix_names
44
+ if @underscored_name.split("_").length > 1
45
+ current_path = "#{@pwd}/conduit-#{@underscored_name}"
46
+
47
+ # Change gem name in gemspec file
48
+ gemspec_file = "#{current_path}/conduit-#{@underscored_name}.gemspec"
49
+ gsub_file(gemspec_file, /spec\.name(.*)/, "spec.name = \"conduit-#{@dasherized_name}\"")
50
+
51
+ # Rename gemspec file
52
+ run("mv #{gemspec_file} #{current_path}/conduit-#{@dasherized_name}.gemspec")
53
+
54
+ # Rename gem dir
55
+ run("mv #{current_path} #{@base_path}")
56
+ end
57
+ end
58
+
59
+ def bundle_install
60
+ # Taken from https://github.com/rails/rails/blob/92703a9ea5d8b96f30e0b706b801c9185ef14f0e/railties/lib/rails/generators/app_base.rb#L363
61
+ _bundle_command = Gem.bin_path("bundler", "bundle")
62
+
63
+ Bundler.with_clean_env do
64
+ inside(@base_path) do
65
+ full_command = %Q["#{Gem.ruby}" "#{_bundle_command}" install]
66
+ run(full_command)
67
+ end
68
+ end
69
+ end
70
+
71
+ # Indicates where the template files are located
72
+ def self.source_root
73
+ "#{__dir__}/blueprints"
74
+ end
75
+
76
+ # List of the template files and their destination
77
+ def files_to_copy
78
+ lib_path = "#{@base_path}/lib/conduit/#{@underscored_name}"
79
+
80
+ {
81
+ "base.tt" => "#{@base_path}/lib/conduit/#{@underscored_name}.rb",
82
+ "actions/base.tt" => "#{lib_path}/actions/base.rb",
83
+ "decorators/base.tt" => "#{lib_path}/decorators/base.rb",
84
+ "parsers/base.tt" => "#{lib_path}/parsers/base.rb",
85
+ "request_mocker/base.tt" => "#{lib_path}/request_mocker/base.rb",
86
+ "views/layout.tt" => "#{lib_path}/views/layout.erb",
87
+ "decorators.tt" => "#{lib_path}/decorators.rb",
88
+ "driver.tt" => "#{lib_path}/driver.rb",
89
+ "request_mocker.tt" => "#{lib_path}/request_mocker.rb",
90
+ "generators/action.tt" => "#{lib_path}/generators/action.rb",
91
+ "generators/blueprints/action.tt" => "#{lib_path}/generators/blueprints/action.tt",
92
+ "generators/blueprints/decorator.tt" => "#{lib_path}/generators/blueprints/decorator.tt",
93
+ "generators/blueprints/mock.tt" => "#{lib_path}/generators/blueprints/mock.tt",
94
+ "generators/blueprints/parser.tt" => "#{lib_path}/generators/blueprints/parser.tt",
95
+ "generators/blueprints/rspec_action.tt" => "#{lib_path}/generators/blueprints/rspec_action.tt",
96
+ "bin/generate_action.tt" => "#{@base_path}/bin/generate_action",
97
+ "support/helpers.tt" => "#{@base_path}/spec/support/helpers.rb",
98
+ "spec_helper.tt" => "#{@base_path}/spec/spec_helper.rb"
99
+ }
100
+ end
101
+
102
+ # Copy template files
103
+ def copy_files
104
+ files_to_copy.each do |origin, destination|
105
+ template(origin, destination, force: true)
106
+ end
107
+ end
108
+
109
+ # Adds missing lines to the files
110
+ def modify_files
111
+ gemspec_file = "#{@base_path}/conduit-#{@dasherized_name}.gemspec"
112
+
113
+ # add gemspec dependencies
114
+ str = " # Dependencies\n"\
115
+ " #\n"\
116
+ " spec.add_dependency \"conduit\", \"~> 1.0.3\"\n"\
117
+ " # xml parser\n"\
118
+ " spec.add_dependency \"nokogiri\"\n"\
119
+ " # to get string inflectors\n"\
120
+ " spec.add_dependency \"activesupport\"\n\n"\
121
+ " # Development Dependencies\n"\
122
+ " #\n"\
123
+ " spec.add_development_dependency \"shoulda-matchers\"\n"\
124
+ " # to compare xml files in tests\n"\
125
+ " spec.add_development_dependency \"equivalent-xml\"\n"\
126
+ " spec.add_development_dependency \"rspec-its\"\n"\
127
+ " # for building CLI\n"\
128
+ " spec.add_development_dependency \"thor\"\n"\
129
+ " # for debugging\n"\
130
+ " spec.add_development_dependency \"byebug\"\n"
131
+
132
+ insert_into_file gemspec_file, str, after: "spec.require_paths = [\"lib\"]\n\n"
133
+
134
+ # remove description
135
+ gsub_file(gemspec_file, /spec\.description(.*)\n/, "")
136
+
137
+ # update summary
138
+ new_summary = "spec.summary = \"#{ActiveSupport::Inflector.humanize @underscored_name} Driver for Conduit\""
139
+ gsub_file(gemspec_file, /spec\.summary(.*)/, new_summary)
140
+
141
+ # update homepage
142
+ new_homepage = "spec.homepage = \"http://www.github.com/conduit/conduit-#{@dasherized_name}\""
143
+ gsub_file(gemspec_file, /spec\.homepage(.*)/, new_homepage)
144
+ end
145
+ end
146
+ end
@@ -27,6 +27,7 @@ module Conduit
27
27
  base.send :include, Conduit::Core::Connection
28
28
  base.send :include, Conduit::Core::Render
29
29
  base.send :include, InstanceMethods
30
+
30
31
  base.extend ClassMethods
31
32
 
32
33
  # TODO: Move this to the driver scope
@@ -87,22 +88,30 @@ module Conduit
87
88
  def_delegator :'self.class', :requirements
88
89
  def_delegator :'self.class', :attributes
89
90
 
90
- # def requirements
91
- # self.class.requirements
92
- # end
93
-
94
- # def attributes
95
- # self.class.attributes
96
- # end
97
-
98
- # Object used for passing data to the view
99
- # Only keys listed in attributes will be
100
- # used.
91
+ # The view's context will be the action's decorator.
92
+ # The decorator name depends on the current action's name.
93
+ #
94
+ # For example `Conduit::MyDriver::Decorators::ActivateDecorator`
95
+ # will be the view's context for the `activate` action.
101
96
  #
102
97
  def view_context
103
- OpenStruct.new(@options.select do |k, v|
104
- attributes.include?(k)
105
- end)
98
+ view_decorator.new(
99
+ OpenStruct.new(attributes_with_values)
100
+ )
101
+ end
102
+
103
+ # Returns a hash of all the defined attributes and
104
+ # their values.
105
+ #
106
+ # If an attribute's value is not passed as an option it
107
+ # will default to `nil`.
108
+ #
109
+ def attributes_with_values
110
+ attributes.inject({}) do |hash, attribute|
111
+ hash.tap do |h|
112
+ h[attribute] = @options[attribute]
113
+ end
114
+ end
106
115
  end
107
116
 
108
117
  # Location where the view files can be found
@@ -121,24 +130,41 @@ module Conduit
121
130
  render(tpl)
122
131
  end
123
132
 
124
- # Entry method. The class will use
125
- # use this to trigger the request.
133
+ # Method called to make the actual request.
126
134
  #
127
135
  # Override to customize.
128
136
  #
129
- def perform
137
+ def perform_request
130
138
  response = request(body: view, method: :post)
131
- parser = parser_class.new(response.body)
139
+ parser_instance = parser.new(response.body)
140
+
141
+ Conduit::ApiResponse.new(raw_response: response, parser: parser_instance)
142
+ end
132
143
 
133
- Conduit::ApiResponse.new(raw_response: response,
134
- parser: parser)
144
+ # Entry method. Calls either the mocker or the `perform_request`
145
+ # method.
146
+ #
147
+ def perform
148
+ # When testing we can mock the request. The mocker used
149
+ # will depend on the action's name. For example:
150
+ # `Conduit::MyDriver::RequestMocker::Activate` will be responsible for
151
+ # mocking the `activate` action.
152
+ #
153
+ # * To mock success pass `mock_status: 'success'` as an option.
154
+ # * To mock failure pass `mock_status: 'failure'` as an option.
155
+ if mock_mode?
156
+ mocker = request_mocker.new(self, @options)
157
+ mocker.with_mocking { perform_request }
158
+ else
159
+ perform_request
160
+ end
135
161
  end
136
162
 
137
163
  private
138
164
 
139
165
  # Ensures that all required attributes are present
140
166
  # If not all attributes are present, will raise
141
- # an ArgumentError listing missing attributes
167
+ # an ArgumentError listing missing attributes.
142
168
  #
143
169
  def validate!(options)
144
170
  missing_keys = (requirements.to_a - options.keys)
@@ -148,15 +174,53 @@ module Conduit
148
174
  end
149
175
  end
150
176
 
151
- # Returns the parser for this action
152
- # subclasses responsible for providing the
153
- # response_body and response_status.
177
+ # Returns the capitalized action name.
178
+ #
179
+ def action_name
180
+ ActiveSupport::Inflector.demodulize(self.class)
181
+ end
182
+
183
+ # Returns the name of the driver that this action belongs to.
184
+ #
185
+ def driver_name
186
+ self.class.to_s.deconstantize.demodulize.underscore
187
+ end
188
+
189
+ # Returns the parser for this action subclasses responsible for providing
190
+ # the response_body and response_status.
154
191
  #
155
- def parser_class
192
+ def parser
156
193
  self.class.const_get(:Parser)
157
194
  end
158
- end
159
195
 
196
+ # Returns the mocker class to use based on the action's name.
197
+ #
198
+ def request_mocker
199
+ "Conduit::#{driver_name.classify}::RequestMocker::#{action_name}".constantize
200
+ end
201
+
202
+ # Returns the decorator class to use based on the action's name. If no
203
+ # decorator class defined, it will return the core decorator class.
204
+ #
205
+ def view_decorator
206
+ # In ruby 2.0.0 we can't do `const_defined? MyDriver::Decorators::FooDecorator`
207
+ # so we have to check each module one by one.
208
+ if Conduit.const_defined?(driver_name.classify) &&
209
+ Conduit.const_get(driver_name.classify).const_defined?("Decorators") &&
210
+ Conduit.const_get("#{driver_name.classify}::Decorators").const_defined?("#{action_name}Decorator")
211
+
212
+ Conduit.const_get("#{driver_name.classify}::Decorators::#{action_name}Decorator")
213
+ else
214
+ Conduit::Core::Decorator
215
+ end
216
+ end
217
+
218
+ # Indicates whether the request should be mocked or not.
219
+ #
220
+ def mock_mode?
221
+ @options.has_key?(:mock_status) && (!@options[:mock_status].nil? && !@options[:mock_status].empty?)
222
+ end
223
+ end
160
224
  end
161
225
  end
162
226
  end
@@ -0,0 +1,22 @@
1
+ #
2
+ # Decorators are used to act as the view's
3
+ # context. This is where most of the logic occurs,
4
+ # mainly where options are organized in a hash
5
+ # for the view to serialize.
6
+ #
7
+ # Extend this class for every action. For example,
8
+ # for the `activate` action, an `ActivateDecorator`
9
+ # is defined which exposes a `line_activation_attributes` method
10
+ # for the view.
11
+ #
12
+
13
+ require 'delegate'
14
+ require 'forwardable'
15
+
16
+ module Conduit
17
+ module Core
18
+ class Decorator < SimpleDelegator
19
+ extend Forwardable
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,89 @@
1
+ #
2
+ # Mocks action responses for testing. We can choose
3
+ # the type of response (success, failure, error).
4
+ #
5
+ # Response fixtures are stored in a directory specified inside
6
+ # `fixtures_path`. Each action has its own folder and each response file
7
+ # is named according to the status we're mocking.
8
+ #
9
+ # In order to mock a request, pass the option `mock_status` to
10
+ # the action with its value being the returned status.
11
+ #
12
+
13
+ require 'erb'
14
+ require 'excon'
15
+ require 'tilt'
16
+
17
+ module Conduit
18
+ module Core
19
+ class RequestMocker
20
+ def initialize(base, options = nil)
21
+ @base = base
22
+ @options = options
23
+ @mock_status = options[:mock_status].to_sym || :success
24
+ @http_status = options[:http_status] || 200
25
+ end
26
+
27
+ # Starts mocking, override if using something different than
28
+ # excon to make the requests.
29
+ #
30
+ def mock
31
+ Excon.defaults[:mock] = true
32
+ Excon.stub({}, { body: response, status: @http_status })
33
+ end
34
+
35
+ # Ends mocking, override if using something different than
36
+ # excon to make the requests.
37
+ #
38
+ def unmock
39
+ Excon.defaults[:mock] = false
40
+ Excon.stubs.clear
41
+ end
42
+
43
+ # Wrapps the block inside a mocking state.
44
+ #
45
+ def with_mocking
46
+ mock and yield.tap { unmock }
47
+ end
48
+
49
+ private
50
+
51
+ def render_response
52
+ Tilt::ERBTemplate.new(fixture).render(@base.view_context)
53
+ end
54
+
55
+ def action_name
56
+ ActiveSupport::Inflector.demodulize(self.class.name).underscore
57
+ end
58
+
59
+ # Directory where fixtures are stored.
60
+ #
61
+ def fixtures_path
62
+ raise NoMethodError, 'Please define fixtures_path in your request mocker.'
63
+ end
64
+
65
+ def fixture
66
+ "#{fixtures_path}/#{action_name}/#{@mock_status}.xml.erb"
67
+ end
68
+
69
+ def response
70
+ if response_statuses.include?(@mock_status)
71
+ return error_response if @mock_status == :error
72
+ render_response
73
+ else
74
+ raise(ArgumentError, "Mock status must be one of the following: #{response_statuses.join(', ')}")
75
+ end
76
+ end
77
+
78
+ # Allowed statuses.
79
+ def response_statuses
80
+ %i(success failure error)
81
+ end
82
+
83
+ # Mocked error response.
84
+ def error_response
85
+ '{"status": 500, "error": "Internal Service Error"}'
86
+ end
87
+ end
88
+ end
89
+ end
@@ -1,3 +1,3 @@
1
1
  module Conduit
2
- VERSION = '1.0.2'
2
+ VERSION = '1.0.3'
3
3
  end
@@ -3,6 +3,7 @@ require 'spec_helper'
3
3
  shared_examples_for Conduit::Core::Action do
4
4
 
5
5
  let(:request) { read_support_file('xml/xml_request.xml') }
6
+ let(:modified_request) { read_support_file('xml/xml_modified_request.xml') }
6
7
  let(:response) { read_support_file('xml/xml_response.xml') }
7
8
 
8
9
  subject do
@@ -28,6 +29,19 @@ shared_examples_for Conduit::Core::Action do
28
29
  end
29
30
 
30
31
  context 'with an instance' do
32
+ describe '#attributes_with_values' do
33
+ context 'with optional attribute not set' do
34
+ let (:attrs) { request_attributes.merge(buz: nil) }
35
+ its(:attributes_with_values) { should eq(attrs) }
36
+ end
37
+
38
+ context 'with optional attribute set' do
39
+ let (:attrs) { request_attributes.merge(buz: "value for buz") }
40
+ subject { described_class.new(attrs) }
41
+ its(:attributes_with_values) { should eq(attrs) }
42
+ end
43
+ end
44
+
31
45
  describe '#view' do
32
46
  before do
33
47
  Excon.stub({}, body: request, status: 200)
@@ -40,6 +54,59 @@ shared_examples_for Conduit::Core::Action do
40
54
  end
41
55
  end
42
56
 
57
+ describe '#view_context' do
58
+ context 'without decorator class' do
59
+ it 'returns an instance of core decorator' do
60
+ subject.view_context.should be_a_kind_of(Conduit::Core::Decorator)
61
+ end
62
+ end
63
+
64
+ context 'with decorator class' do
65
+ let(:driver) { subject.class.name.deconstantize.demodulize }
66
+ let(:action) { subject.class.name.demodulize }
67
+
68
+ # Define decorator module programmatically:
69
+ let(:decorator) do
70
+ # Create DriverName::Decorators module
71
+ module TheDriver
72
+ module Decorators
73
+ end
74
+ end
75
+
76
+ # Add action decorator to DriverName::Decorators
77
+ class TheDecorator < Conduit::Core::Decorator
78
+ def foo
79
+ "new #{super}"
80
+ end
81
+
82
+ def baz
83
+ "modified baz"
84
+ end
85
+ end
86
+
87
+ # Set the above defined modules and class in Conduit
88
+ stub_const("TheDriver::Decorators::#{action}Decorator", TheDecorator)
89
+
90
+ TheDriver
91
+ end
92
+
93
+ before do
94
+ stub_const("Conduit::#{driver}", decorator)
95
+ Excon.stub({}, body: modified_request, status: 200)
96
+ end
97
+
98
+ it 'returns an instance of core decorator' do
99
+ subject.view_context.should be_a_kind_of(Conduit.const_get(driver)::Decorators::const_get("#{action}Decorator"))
100
+ end
101
+
102
+ it 'returns a new rendered view for an action' do
103
+ a = subject.view.gsub(/\s+/, '')
104
+ b = modified_request.gsub(/\s+/, '')
105
+ a.should == b
106
+ end
107
+ end
108
+ end
109
+
43
110
  describe '#perform' do
44
111
  before { Excon.stub({}, body: response, status: 200) }
45
112
 
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+ require 'excon'
3
+
4
+ shared_examples_for Conduit::Core::RequestMocker do
5
+
6
+ let(:success) { read_support_file('xml/success_mock.xml') }
7
+ let(:failure) { read_support_file('xml/failure_mock.xml') }
8
+ let(:fake_action) do
9
+ class FakeAction
10
+ def view_context
11
+ OpenStruct.new({ value: "Yay!" })
12
+ end
13
+ end
14
+
15
+ FakeAction.new
16
+ end
17
+
18
+ context 'success mocking' do
19
+ subject { described_class.new(fake_action, { mock_status: 'success' }) }
20
+ let(:response) { subject.with_mocking { Excon.new('http://example.com').request }.body }
21
+ it 'return success response' do
22
+ response.should eq(success)
23
+ end
24
+ end
25
+
26
+ context 'failure mocking' do
27
+ subject { described_class.new(fake_action, { mock_status: 'failure' }) }
28
+ let(:response) { subject.with_mocking { Excon.new('http://example.com').request }.body }
29
+ it 'return failure response' do
30
+ response.should eq(failure)
31
+ end
32
+ end
33
+
34
+ context 'with disallowed response status' do
35
+ subject { described_class.new(fake_action, { mock_status: 'fake_status' }) }
36
+ let(:response) { subject.with_mocking { Excon.new('http://example.com').request }.body }
37
+ it 'raises and error' do
38
+ lambda { response }.should raise_error(ArgumentError)
39
+ end
40
+ end
41
+ end
42
+
43
+ describe Conduit::MyDriver::RequestMocker::Foo do
44
+ it_behaves_like Conduit::Core::RequestMocker
45
+ end
@@ -0,0 +1,4 @@
1
+ <BeQuick product="conduit">
2
+ <response status="failure" action="foo" />
3
+ <error>Err 404</error>
4
+ </BeQuick>
@@ -0,0 +1,4 @@
1
+ <BeQuick product="conduit">
2
+ <response status="success" action="foo" />
3
+ <val><%= value %></val>
4
+ </BeQuick>
@@ -0,0 +1,13 @@
1
+ module Conduit
2
+ module MyDriver
3
+ module RequestMocker
4
+ class Foo < Conduit::Core::RequestMocker
5
+ private
6
+
7
+ def fixtures_path
8
+ "#{__dir__}/fixtures"
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,4 @@
1
+ <BeQuick product="conduit">
2
+ <response status="failure" action="foo" />
3
+ <error>Err 404</error>
4
+ </BeQuick>
@@ -0,0 +1,4 @@
1
+ <BeQuick product="conduit">
2
+ <response status="success" action="foo" />
3
+ <val>Yay!</val>
4
+ </BeQuick>
@@ -0,0 +1,7 @@
1
+ <BeQuick product="conduit">
2
+ <action name="foo">
3
+ <foo>new value for foo</foo>
4
+ <bar>value for bar</bar>
5
+ <baz>modified baz</baz>
6
+ </action>
7
+ </BeQuick>
@@ -1,7 +1,7 @@
1
- <BeQuickproduct="conduit">
1
+ <BeQuick product="conduit">
2
2
  <action name="foo">
3
3
  <foo>value for foo</foo>
4
4
  <bar>value for bar</bar>
5
5
  <baz>value for baz</baz>
6
6
  </action>
7
- </BeQuick>
7
+ </BeQuick>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: conduit
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Kelley
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-24 00:00:00.000000000 Z
11
+ date: 2016-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -66,6 +66,34 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: thor
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: shoulda-matchers
71
99
  requirement: !ruby/object:Gem::Requirement
@@ -125,20 +153,43 @@ dependencies:
125
153
  description:
126
154
  email:
127
155
  - mike@codezombie.org
128
- executables: []
156
+ executables:
157
+ - conduit
129
158
  extensions: []
130
159
  extra_rdoc_files: []
131
160
  files:
132
161
  - Rakefile
162
+ - bin/conduit
133
163
  - lib/conduit.rb
134
164
  - lib/conduit/api_response.rb
165
+ - lib/conduit/blueprints/actions/base.tt
166
+ - lib/conduit/blueprints/base.tt
167
+ - lib/conduit/blueprints/bin/generate_action.tt
168
+ - lib/conduit/blueprints/decorators.tt
169
+ - lib/conduit/blueprints/decorators/base.tt
170
+ - lib/conduit/blueprints/driver.tt
171
+ - lib/conduit/blueprints/generators/action.tt
172
+ - lib/conduit/blueprints/generators/blueprints/action.tt
173
+ - lib/conduit/blueprints/generators/blueprints/decorator.tt
174
+ - lib/conduit/blueprints/generators/blueprints/mock.tt
175
+ - lib/conduit/blueprints/generators/blueprints/parser.tt
176
+ - lib/conduit/blueprints/generators/blueprints/rspec_action.tt
177
+ - lib/conduit/blueprints/parsers/base.tt
178
+ - lib/conduit/blueprints/request_mocker.tt
179
+ - lib/conduit/blueprints/request_mocker/base.tt
180
+ - lib/conduit/blueprints/spec_helper.tt
181
+ - lib/conduit/blueprints/support/helpers.tt
182
+ - lib/conduit/blueprints/views/layout.tt
183
+ - lib/conduit/cli.rb
135
184
  - lib/conduit/configuration.rb
136
185
  - lib/conduit/connection_error.rb
137
186
  - lib/conduit/core/action.rb
138
187
  - lib/conduit/core/connection.rb
188
+ - lib/conduit/core/decorator.rb
139
189
  - lib/conduit/core/driver.rb
140
190
  - lib/conduit/core/parser.rb
141
191
  - lib/conduit/core/render.rb
192
+ - lib/conduit/core/request_mocker.rb
142
193
  - lib/conduit/drivers/keep
143
194
  - lib/conduit/error.rb
144
195
  - lib/conduit/storage.rb
@@ -150,14 +201,21 @@ files:
150
201
  - spec/classes/core/action_spec.rb
151
202
  - spec/classes/core/driver_spec.rb
152
203
  - spec/classes/core/parser_spec.rb
204
+ - spec/classes/core/request_mocker_spec.rb
153
205
  - spec/classes/util_spec.rb
154
206
  - spec/spec_helper.rb
155
207
  - spec/support/helper.rb
156
208
  - spec/support/my_driver/actions/foo.rb
157
209
  - spec/support/my_driver/driver.rb
158
210
  - spec/support/my_driver/parsers/foo.rb
211
+ - spec/support/my_driver/request_mockers/fixtures/foo/failure.xml.erb
212
+ - spec/support/my_driver/request_mockers/fixtures/foo/success.xml.erb
213
+ - spec/support/my_driver/request_mockers/foo.rb
159
214
  - spec/support/my_driver/views/foo.erb
160
215
  - spec/support/my_driver/views/layout.erb
216
+ - spec/support/xml/failure_mock.xml
217
+ - spec/support/xml/success_mock.xml
218
+ - spec/support/xml/xml_modified_request.xml
161
219
  - spec/support/xml/xml_request.xml
162
220
  - spec/support/xml/xml_response.xml
163
221
  homepage: https://github.com/conduit/conduit
@@ -179,7 +237,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
179
237
  version: '0'
180
238
  requirements: []
181
239
  rubyforge_project:
182
- rubygems_version: 2.6.3
240
+ rubygems_version: 2.4.5
183
241
  signing_key:
184
242
  specification_version: 4
185
243
  summary: Conduit is an interface for debit platforms.
@@ -187,14 +245,20 @@ test_files:
187
245
  - spec/classes/core/action_spec.rb
188
246
  - spec/classes/core/driver_spec.rb
189
247
  - spec/classes/core/parser_spec.rb
248
+ - spec/classes/core/request_mocker_spec.rb
190
249
  - spec/classes/util_spec.rb
191
250
  - spec/spec_helper.rb
192
251
  - spec/support/helper.rb
193
252
  - spec/support/my_driver/actions/foo.rb
194
253
  - spec/support/my_driver/driver.rb
195
254
  - spec/support/my_driver/parsers/foo.rb
255
+ - spec/support/my_driver/request_mockers/fixtures/foo/failure.xml.erb
256
+ - spec/support/my_driver/request_mockers/fixtures/foo/success.xml.erb
257
+ - spec/support/my_driver/request_mockers/foo.rb
196
258
  - spec/support/my_driver/views/foo.erb
197
259
  - spec/support/my_driver/views/layout.erb
260
+ - spec/support/xml/failure_mock.xml
261
+ - spec/support/xml/success_mock.xml
262
+ - spec/support/xml/xml_modified_request.xml
198
263
  - spec/support/xml/xml_request.xml
199
264
  - spec/support/xml/xml_response.xml
200
- has_rdoc: