Module: Pluggy::Util

Defined in:
lib/pluggy/util.rb

Overview

Naming conversion and request encoding.

Pluggy speaks camelCase; Ruby speaks snake_case. Two conversions run in opposite directions: responses are read through snake_case accessors (see PluggyObject), and request params are written from snake_case kwargs into camelCase wire keys.

The conversion is deliberately NOT round-tripped: camel_case(snake_case(x)) is lossy for acronym-bearing keys ("issuerCNPJ" -> :issuer_cnpj -> "issuerCnpj"). That is fine because only outgoing params are camelized, and those are an explicit per-service allowlist rather than something we derive from a response.

Constant Summary collapse

ACRONYM_BOUNDARY =

"issuerCNPJName" -> "issuerCNPJ_Name"

/([A-Z\d]+)([A-Z][a-z])/
LOWER_UPPER =

"createdAt" -> "created_At"

/([a-z\d])([A-Z])/
COMMA_JOINED_PARAMS =

Comma-joined in the query string. countries and types are style: form, explode: false in the spec; cnpjs is typed as a bare comma-separated string. Everything else (notably ids) repeats the key.

%w[countries types cnpjs].freeze
DATE_ONLY_PARAMS =

The spec types these as format: date-time but every description says "Format (yyyy-mm-dd)". The descriptions win — see plan §7.

%w[from to dateFrom dateTo].freeze
DATETIME_PARAMS =

...and this one really does want the full timestamp.

%w[createdAtFrom].freeze
DATETIME_FORMAT =
"%Y-%m-%dT%H:%M:%S.%LZ"

Class Method Summary collapse

Class Method Details

.camel_case(key) ⇒ Object

:item_id => "itemId", :oauth_redirect_uri => "oauthRedirectUri"



56
57
58
59
60
61
62
63
64
# File 'lib/pluggy/util.rb', line 56

def camel_case(key)
  k = key.to_s
  return k unless k.include?("_")

  @camel_cache[k] ||= begin
    head, *rest = k.split("_")
    head + rest.map(&:capitalize).join
  end
end

.encode_body(hash, opaque: []) ⇒ Object

Deep snake_case -> camelCase for request bodies.

opaque names keys whose contents must not be touched. POST /items parameters and POST /items/id/mfa are free-form => String maps whose keys are connector-defined ("user", "cpf", "cpf_cnpj"), so camelizing them would silently break item creation.



135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/pluggy/util.rb', line 135

def encode_body(hash, opaque: [])
  (hash || {}).each_with_object({}) do |(key, value), out|
    next if value.nil?

    wire = wire_key(key)
    out[wire] =
      if opaque.include?(wire)
        stringify_keys(value)
      else
        encode_body_value(value, opaque)
      end
  end
end

.encode_body_value(value, opaque) ⇒ Object



149
150
151
152
153
154
155
# File 'lib/pluggy/util.rb', line 149

def encode_body_value(value, opaque)
  case value
  when Hash then encode_body(value, opaque: opaque)
  when Array then value.map { |e| e.is_a?(Hash) ? encode_body(e, opaque: opaque) : e }
  else value
  end
end

.encode_query(params) ⇒ Object



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/pluggy/util.rb', line 72

def encode_query(params)
  pairs = []

  (params || {}).each do |key, value|
    next if value.nil?

    wire = wire_key(key)

    case value
    when Array
      next if value.empty?

      if COMMA_JOINED_PARAMS.include?(wire)
        pairs << [wire, value.map { |e| format_param(wire, e) }.join(",")]
      else
        value.each { |e| pairs << [wire, format_param(wire, e)] }
      end
    when Hash
      raise ArgumentError, "nested hash is not encodable in a query string: #{wire}"
    else
      pairs << [wire, format_param(wire, value)]
    end
  end

  URI.encode_www_form(pairs)
end

.format_param(wire, value) ⇒ Object

Per-parameter date formatting (see DATE_ONLY_PARAMS / DATETIME_PARAMS).



100
101
102
103
104
105
106
107
108
# File 'lib/pluggy/util.rb', line 100

def format_param(wire, value)
  if DATE_ONLY_PARAMS.include?(wire)
    to_date_string(value)
  elsif DATETIME_PARAMS.include?(wire)
    to_datetime_string(value)
  else
    value.to_s
  end
end

.snake_case(key) ⇒ Object

"createdAt" => "created_at", "CET" => "cet", "hasMFA" => "has_mfa", "issuerCNPJ" => "issuer_cnpj", "payeeMCC" => "payee_mcc"



47
48
49
50
51
52
53
# File 'lib/pluggy/util.rb', line 47

def snake_case(key)
  k = key.to_s
  @snake_cache[k] ||= k.gsub(ACRONYM_BOUNDARY, '\1_\2')
                       .gsub(LOWER_UPPER, '\1_\2')
                       .tr("-", "_")
                       .downcase
end

.stringify_keys(value) ⇒ Object

Shallow String-ify of a free-form map's keys, leaving them otherwise untouched.



159
160
161
162
163
# File 'lib/pluggy/util.rb', line 159

def stringify_keys(value)
  return value unless value.is_a?(Hash)

  value.each_with_object({}) { |(k, v), out| out[k.to_s] = v }
end

.to_date_string(value) ⇒ Object



110
111
112
113
114
115
116
117
118
# File 'lib/pluggy/util.rb', line 110

def to_date_string(value)
  case value
  when Date then value.iso8601
  when Time then value.to_date.iso8601
  else
    # Tolerate a caller passing a full timestamp for a date-only param.
    value.to_s[0, 10]
  end
end

.to_datetime_string(value) ⇒ Object



120
121
122
123
124
125
126
127
# File 'lib/pluggy/util.rb', line 120

def to_datetime_string(value)
  case value
  when Time then value.utc.strftime(DATETIME_FORMAT)
  when DateTime then value.to_time.utc.strftime(DATETIME_FORMAT)
  when Date then Time.utc(value.year, value.month, value.day).strftime(DATETIME_FORMAT)
  else value.to_s
  end
end

.wire_key(key) ⇒ Object

Symbol keys are snake_case and get camelized. String keys pass through verbatim -- the escape hatch for any key our camelizer would mangle.



68
69
70
# File 'lib/pluggy/util.rb', line 68

def wire_key(key)
  key.is_a?(String) ? key : camel_case(key)
end