Yodlee and Rails Implementation, Part 3: Bank Login Forms

In our previous post, we retrieved and cached a full list of banks supported by Yodlee.

Now it's time to work on fetching the login forms for those banks, and converting them into usable HTML.

The Golden Pattern

We're going to start by making a bank class inside the app/models/yodlee directory.

app/models/yodlee/bank.rb
module Yodlee
  class Bank < Base

    def initialize bank
      @bank = bank
    end

  end
end

Now we're going to add a yodlee method to the ActiveRecord Bank model.

app/models/bank.rb
class Bank < ActiveRecord::Base

  def yodlee
    @yodlee ||= Yodlee::Bank.new(self)
  end

end

Study the pattern very carefully:

  1. the Yodlee class (Bank) corresponds to an ActiveRecord model class (Bank)
  2. the Yodlee class inherits from Yodlee::Base, which allows it to use the base methods we created, like query
  3. the Yodlee class receives one argument on initialization - the ActiveRecord object it wraps

This allows us to do things like this:

bank = Bank.first

# Use regular ActiveRecord methods
bank.id # => 1
bank.content_service_display_name # => "AT&T Universal Card" 

# Use Yodlee-specific methods we are yet to build
bank.yodlee.login_requirements # => Yodlee API call for login requirements
bank.yodlee.form # => get an HTML login form 

You see, what we're doing is putting all the Yodlee-specific methods and domain modeling within the classes in the app/models/yodlee directory: the Yodlee module. This prevents Yodlee-knowledge infecting the ActiveRecord classes, whom have no need to know this information.

By adding a single method, yodlee, to the ActiveRecord class as we did above, we create a bridge, a connection, between the ActiveRecord instance methods and the Yodlee instance methods, allowing us to do bank.yodlee.foo.

The result is an application that has the correct logic in the correct places. The Yodlee classes know nothing about ActiveRecord and data persistence. The ActiveRecord classes know nothing about the Yodlee API. The classes are as decoupled as we can realistically make them, resulting in a super-clean Yodlee implementation.

We are going to use this pattern repeatedly. Our upcoming User ActiveRecord model will have a corresponding Yodlee::User class. Our Account ActiveRecord model will have a corresponding Yodlee::Account. These ActiveRecord models all will have a yodlee instance method, wrapping the ActiveRecord instance with Yodlee methods.

This pattern is the key to creating a clean Yodlee implementation within a Rails app.

The Form Data

It's time to return to the Yodlee::Bank class. Let's add a method to retrieve the login form data for a bank.

app/models/yodlee/bank.rb
module Yodlee
  class Bank < Base

    def initialize bank
      @bank = bank
    end

    def content_service_id
      @bank.content_service_id
    end

    def login_requirements
      query({
        :endpoint => '/jsonsdk/ItemManagement/getLoginFormForContentService',
        :method => :POST,
        :params => {
          :cobSessionToken  => cobrand_token,
          :contentServiceId => content_service_id
        }
      })
    end

  end
end

The method we just added, login_requirements, is a straightforward API call to the getLoginFormForContentService API, which will return the login form for a particular bank.

Let's run this method now.

bank = Bank.first
bank.yodlee.login_requirements

# Returns: 
# => <Hashie::Mash componentList=[#<Hashie::Mash displayName="User ID" fieldType=#<Hashie::Mash typeName="TEXT"> helpText="41348" isEditable=true isEscaped=false isMFA=false isOptional=false isOptionalMFA=false maxlength=40 name="LOGIN" size=20 valueIdentifier="LOGIN" valueMask="LOGIN_FIELD">, #<Hashie::Mash displayName="Password" fieldType=#<Hashie::Mash typeName="IF_PASSWORD"> helpText="41347" isEditable=true isEscaped=false isMFA=false isOptional=false isOptionalMFA=false maxlength=40 name="PASSWORD" size=20 valueIdentifier="PASSWORD" valueMask="LOGIN_FIELD">] conjunctionOp=#<Hashie::Mash conjuctionOp=1> defaultHelpText="4"> 

Caveat Emptor

Before we continue, there is something important I need to mention.

Take a moment to head over to the REST API Reference. Notice that there are API calls under "Container-based Services" and other API calls under "Site-based Services". The API call we used above, getLoginFormForContentService, is a container-based API. What is the difference between container-based vs site-based?

Citibank is a site. Conversely, Citibank Checking, Citibank Credit Cards, and Citibank Loans are multiple content services.

So when we use a container-based API like getLoginFormForContentService, we can specify that it is for the Citibank Checking content service specifically and it will return the login form for Citibank Checking. If we were to use the site-based API equivalent, getSiteLoginForm, it would return a larger form, a composite of all the various Citibank content service forms.

The important thing to know is that the API calls cannot be mixed and matched. If an account is added with a container-based API, its data must be retrieved via a container-based API, not a site-based API. You must decided on using either all site-based APIs or all container-based APIs.

For the vast majority of apps, you are interested in a specific vertical. Your site may be all about servicing users' bank accounts. It may be a site that proposes better loans. It may assist users with their stock portfolios. But it's rare for a startup to create an app that cuts across verticals, that services bank accounts and loans and stocks and investments.

For that reason, I am using container-based APIs throughout this series. If however you wish to use site-based APIs, it will still be easy to follow along - simply switch out the API calls with their site-based equivalents.

The HTML Converter

Let's have a look at the JSON that Yodlee returned when we fetched the login form above.

{
   "conjunctionOp":{
      "conjuctionOp":1
   },
   "componentList":[
      {
         "valueIdentifier":"LOGIN",
         "valueMask":"LOGIN_FIELD",
         "fieldType":{
            "typeName":"TEXT"
         },
         "size":20,
         "maxlength":40,
         "name":"LOGIN",
         "displayName":"User ID",
         "isEditable":true,
         "isOptional":false,
         "isEscaped":false,
         "helpText":"41348",
         "isOptionalMFA":false,
         "isMFA":false
      },
      {
         "valueIdentifier":"PASSWORD",
         "valueMask":"LOGIN_FIELD",
         "fieldType":{
            "typeName":"IF_PASSWORD"
         },
         "size":20,
         "maxlength":40,
         "name":"PASSWORD",
         "displayName":"Password",
         "isEditable":true,
         "isOptional":false,
         "isEscaped":false,
         "helpText":"41347",
         "isOptionalMFA":false,
         "isMFA":false
      }
   ],
   "defaultHelpText":"4"
}

The first field in the response is conjunctionOp. conjunctionOp stands for conjunction operator, and when set to 1 means that the fields in componentList are AND fields: all of the fields must be filled in. If conjunctionOp was set to 0, it would make them OR fields: you only need to fill in one of the fields. I have never yet seen a bank with the conjunctionOp set to 0, and I understand from previous conversations with Yodlee that there are currently no banks with a 0 conjunctionOp, so you can ignore it. For now, anyway :)

Next comes componentList, the meat and potatoes. componentList is a JSON representation of the fields and their requirements, and is more or less self-explanatory.

What we need to do is create a convertor that is clean and mean. It should be able to take a JSON form representation and convert it to HTML flawlessly, but with elegant code.

So, where to begin?

OO To The Rescue

This is the kind of problem that object-oriented methodology solves with relish. What we need to do is create a small cluster of objects.

At the top of this cluster's foodchain will be the Form class. The Form class will take the JSON form representation, and start looping through the componentList. For each component in the list, it will read its type - text, password, etc - and send it on to a corresponding class - TextField, PasswordField etc - which will have the logic necessary to parse the JSON field into an HTML field.

If this sounds a little hazy now, don't fear. It will become clear as we begin.

Let's start backwards, as things will be clearer that way. We'll begin by creating the classes that convert specific fields.

If you scroll up to the JSON above, you will see that the two fields returned had two types: the first field was of a "text" type, the second was a "password" field. Let's create these two.

First create a new folder within app/models/yodlee named fields. Then:

app/models/yodlee/fields/text.rb
module Yodlee
  module Fields
    class Text

      attr_reader :field

      def initialize opts
        @field = opts[:field]
      end

      def render
        "
          <div class='field'>
            <label>#{label} #{asterisk}</label>
            <input class='string #{requirement}' id='#{name}' name='#{name}' size='#{size}' type='text' maxlength='#{maxlength}' value='#{value}' />
          </div>
        " if required?
      end

      def label
        field.displayName
      end

      def asterisk
        field.isOptional ? '' : '*'
      end

      def requirement
        field.isOptional ? 'optional' : 'required'
      end

      def required?
        !field.isOptional
      end

      def size
        field['size']
      end

      def maxlength
        field.maxlength
      end

      def value
        field.value
      end

      def name
        field.valueIdentifier
      end

    end
  end
end

This Text class pre-supposes that the JSON for one single text field will be passed in on initialization. The Form class, which we have yet to create, will do the job of passing in the correct JSON, and when that Form class is built and complete, it will pass the Text class just this JSON snippet, cut out from the Yodlee response above:

{
   "valueIdentifier":"LOGIN",
   "valueMask":"LOGIN_FIELD",
   "fieldType":{
      "typeName":"TEXT"
   },
   "size":20,
   "maxlength":40,
   "name":"LOGIN",
   "displayName":"User ID",
   "isEditable":true,
   "isOptional":false,
   "isEscaped":false,
   "helpText":"41348",
   "isOptionalMFA":false,
   "isMFA":false
}

That JSON snippet has already been converted by the query method in Yodlee::Base to a Hashie::Mash. This Hashie::Mash is saved in the @field instance variable, which is then used by all the other methods in the Text class.

The result is that to use the Text class, we only do two things: pass in the JSON snippet, and then call the render method. Boom! Instant conversion to HTML. Why? Because the render method is basically a string, interpolated with the other methods like name, size, and label, which retrieve those values from the field Hashie::Mash.

I'm not going to go through all the methods in the class, because they are very straightforward. The only thing worth noting is that the render method has a conditional in it, which only renders the field if the field is required. If the field is optional, we don't bother rendering it at all.

Let's now go ahead and do something similar for the next field, which is a password field. We name this class IfPassword, because as you see in the JSON response above, Yodlee uses the type IfPassword to refer to password fields.

app/models/yodlee/fields/if_password.rb
module Yodlee
  module Fields
    class IfPassword

      attr_reader :field

      def initialize opts
        @field = opts[:field]
      end

      def render
        "
          <div class='field'>
            <label>#{label} #{asterisk}</label>
            <input class='string #{requirement}' id='#{name}' name='#{name}' size='#{size}' type='password' maxlength='#{maxlength}' value='#{value}' />
          </div>
        " if required?
      end

      def label
        field.displayName
      end

      def asterisk
        field.isOptional ? '' : '*'
      end

      def requirement
        field.isOptional ? 'optional' : 'required'
      end

      def required?
        !field.isOptional
      end

      def size
        field['size']
      end

      def maxlength
        field.maxlength
      end

      def value
        field.value
      end

      def name
        field.valueIdentifier
      end

    end
  end
end

Being a smart fella, you notice that this IfPassword class is practically identical to the Text class. In fact, the only difference is within the render method, where one class has type='text' and the other has type='password'. What does that mean? Refactor!

Let's do this by making a BaseField class, which has the shared behavior. The Text and IfPassword classes will then inherit from the BaseField class.

app/models/yodlee/fields/base_field.rb
module Yodlee
  module Fields
    class BaseField

      attr_reader :field

      def initialize opts
        @field = opts[:field]
      end

      def render
        "
          <div class='field'>
            <label>#{label} #{asterisk}</label>
            #{input}
          </div>
        " if required?
      end

      def label
        field.displayName
      end

      def asterisk
        field.isOptional ? '' : '*'
      end

      def requirement
        field.isOptional ? 'optional' : 'required'
      end

      def required?
        !field.isOptional
      end

      def size
        field['size']
      end

      def maxlength
        field.maxlength
      end

      def value
        field.value
      end

      def name
        field.valueIdentifier
      end

    end
  end
end
app/models/yodlee/fields/text.rb
module Yodlee
  module Fields
    class Text < BaseField

      def input
        "<input class='string #{requirement}' id='#{name}' name='#{name}' size='#{size}' type='text' maxlength='#{maxlength}' value='#{value}' />"
      end

    end
  end
end
app/models/yodlee/fields/if_password.rb
module Yodlee
  module Fields
    class IfPassword < BaseField

      def input
        "<input class='string #{requirement}' id='#{name}' name='#{name}' size='#{size}' type='password' maxlength='#{maxlength}' value='#{value}' />"
      end

    end
  end
end

WOW! Well that slimmed them right down.

The Form Object

So we have the field classes figured out. It's time to create the Form class, which will take the JSON response, loop through the fields in the componentList, and send each component off to the right field class for rendering.

app/models/yodlee/form.rb
module Yodlee
  class Form

    attr_reader :fields, :wrapper

    def initialize opts
      @fields = opts[:fields]
    end

    def render
      fields.componentList.map do |element|
        type = element.fieldType.typeName.downcase.classify
        Yodlee::Fields.const_get(type).new(field: element).render
      end.join('').squish
    end

  end
end

So, what is this doing? The Form class expects to receive the JSON response we got from the getLoginFormForContentService API. The render method loops through the componentList section of that response. The type of each field is listed under element.fieldType.typeName.downcase.classify. We then use Ruby's Module#const_get to fetch that class from the string name, so that "text" converts to class Yodlee::Fields::Text. We then create a new instance of that class, passing in just the JSON for that field, and call the render method.

All this is done within a map. The result is an array. Each element in the array is a string of HTML - the string outputted for the field by the render method in each field class. We join those strings together and then call squish, an ActiveSupport method that will trim out any whitespace from the final joined string.

The last bit of the puzzle is to go back to the Yodlee::Bank class and add a method to pass the getLoginFormForContentService JSON into a Form object.

app/models/yodlee/bank.rb
def form
  @form ||= Yodlee::Form.new(fields: login_requirements).render
end

Wheeeew! That was quite a bit of coding. Let's give this whirl and see if it works in the terminal.

reload!
bank = Bank.first
bank.yodlee.form

# Should return: 
# => "<div class='field'> <label>User ID *</label> <input class='string required' id='LOGIN' name='LOGIN' size='20' type='text' maxlength='40' value='' /> </div> <input class='string required' id='PASSWORD' name='PASSWORD' size='20' type='password' maxlength='40' value='' />" 

How awesome is that?

Sticking With Theory

It's important to understand that this approach is a prime example of object-oriented code done right. Why?

The Yodlee::Bank class understands that its primary responsibility is general Yodlee bank data, and that the nitty gritty of form rendering is beyond its purview. So it doesn't get involved. It simply defers to the Form object and asks it, "Please render this form for me".

The Form object does the same thing. It understands that knowing the ins-and-outs of all the different field specs is not its job. So it doesn't attempt rendering the form on its own. Instead, it splits the form into fields, sends each field to the class (Text, IfPassword, etc) which has the knowledge necessary for that field, and says, "Please render this field for me."

The result is a small cluster of objects which defer to each other and never step on each other's toes.

Another result is simple and elegant code. Did you notice there are no if-else statements, even though we are dealing with multiple field types? We don't need if-else statements, because each class only has knowledge of one type of field.

Another by-product of structuring code in this way is that it is easily extendable. To deal with additional field types, like dropdown selects, radio buttons, check boxes, and so on, we simply need to add a class for that type of field and give it a render method. More on this later.

Making It Rails-y

Rails is all about convention over configuration. One important Rails convention is that all forms use nested parameters.

For example, let's say you have a form to create a new User, and the available fields are name and email. If you inspect the HTML that Rails creates for this form, the name attributes of the input fields are not 'name' and 'email', they are 'USER[name]' and 'USER[email]'. This then results in a nested parameter hash we can use in controllers, and is the reason we can use the familiar Rails construct params[:user], which you might use as @user = User.new(params[:user]).

We therefore need to add this ability to our form parser. We need to be able to specify a name for the form fields to be nested under.

Let's revisit the form method in Yodlee::Bank, and allow optional arguments to be passed in.

app/models/yodlee/bank.rb
def form opts = {}
  @form ||= Yodlee::Form.new(opts.merge({ fields: login_requirements })).render
end

Next, in the Form class we will check to see if a 'wrapper' option has been passed in. If it has, we then pass it along to each field class as an option.

app/models/yodlee/form.rb
module Yodlee
  class Form

    attr_reader :fields, :wrapper

    def initialize opts
      @fields  = opts[:fields]
      # LISTEN FOR A WRAPPER OPTION:
      @wrapper = opts[:wrapper]
    end

    def render
      @fields.componentList.map do |element|
        type = element.fieldType.typeName.downcase.classify
        # PASS ALONG THE WRAPPER OPTION:
        Yodlee::Fields.const_get(type).new(field: element, wrapper: wrapper).render
      end.join('').squish
    end

  end
end

Lastly, in the BaseField class, we change the name of the input should a wrapper argument be present.

app/models/yodlee/fields/base_field.rb
module Yodlee
  module Fields
    class BaseField

      def initialize opts
        @field   = opts[:field]
        # LISTEN FOR WRAPPER OPTION:
        @wrapper = opts[:wrapper]
      end

      # ... rest of methods omitted ...

      def name
        name = field.valueIdentifier
        if @wrapper.present?
          @wrapper + '[' + name + ']'
        else
          name
        end
      end

    end
  end
end

Now, we can test it.

reload!
bank = bank.first

# First, with no wrapper option:
bank.yodlee.form
# => "<div class='field'> <label>User ID *</label> <input class='string required' id='LOGIN' name='LOGIN' size='20' type='text' maxlength='40' value='' /> </div> <input class='string required' id='PASSWORD' name='PASSWORD' size='20' type='password' maxlength='40' value='' />" 

# Now, with a wrapper option:
bank.yodlee.form(wrapper: 'account')
# => "<div class='field'> <label>User ID *</label> <input class='string required' id='account[LOGIN]' name='account[LOGIN]' size='20' type='text' maxlength='40' value='' /> </div> <input class='string required' id='account[PASSWORD]' name='account[PASSWORD]' size='20' type='password' maxlength='40' value='' />"

Worked beautifully. Later, when we discuss showing this form within a web page to our users, we will use this wrapper option to generate valid Rails forms.

Crossing Your T's

In this tutorial, we only dealt with two types of basic fields, text and if_password. There are many other types of fields too. Going through the implementation of every field would take me forever, but using the pattern established above, you can plug in code for every field type. If is very important to have every field type covered, or your users will have an unstable experience.

The full list of field types are:

  • TEXT
  • IF_PASSWORD
  • OPTIONS
  • CHECKBOX
  • RADIO
  • IF_LOGIN
  • URL
  • HIDDEN
  • IMAGE_URL
  • CONTENT_URL
  • CUSTOM
  • CLUDGE

In addition, there are two types of composite fields.

  • FieldInfoMultiFixed is a field made up of multiple sub-fields. For example, you might have a date of birth FieldInfoMultiFixed which contains three dropdown fields, for month, day, and year.
  • FieldInfoChoice is a field of multiple sub-fields where only one sub-field is required. For example, you may have a FieldInfoChoice that has inside it a social security number field and an account pin field, and the end user has to fill in one or the other but not both.

Wrapping Up

Please take a moment to check your current code from this tutorial against these gists, to ensure you have it down right.

Coming Up Next

Now that we've got the login forms from the banks, it's time to use them and create accounts for our users. In the next tutorial, we'll discuss user management.