munson 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/console CHANGED
@@ -2,13 +2,5 @@
2
2
 
3
3
  require "bundler/setup"
4
4
  require "munson"
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "irb"
14
- IRB.start
5
+ require "pry"
6
+ Pry.start
data/lib/munson/agent.rb CHANGED
@@ -1,55 +1,12 @@
1
1
  module Munson
2
2
  class Agent
3
- extend Forwardable
4
- def_delegators :query, :includes, :sort, :filter, :fields, :fetch, :page
5
-
6
- attr_writer :connection
7
-
8
- attr_accessor :type
9
- attr_accessor :query_builder
10
-
11
- attr_reader :paginator
12
- attr_accessor :paginator_options
13
-
14
3
  # Creates a new Munson::Agent
15
4
  #
16
- # @param [Hash] opts={} describe opts={}
17
- # @option opts [Munson::Connection] :connection to use
18
- # @option opts [#to_s, Munson::Paginator] :paginator to use on query builder
19
- # @option opts [Class] :query_builder provide a custom query builder, defaults to {Munson::QueryBuilder}
20
- # @option opts [#to_s] :type JSON Spec type. Type will be added to the base path set in the Faraday::Connection
21
- def initialize(opts={})
22
- @connection = opts[:connection]
23
- @type = opts[:type]
24
-
25
- @query_builder = opts[:query_builder].is_a?(Class) ?
26
- opts[:query_builder] : Munson::QueryBuilder
27
-
28
- self.paginator = opts[:paginator]
29
- @paginator_options = opts[:paginator_options]
30
- end
31
-
32
- def paginator=(pager)
33
- if pager.is_a?(Symbol)
34
- @paginator = Munson.lookup_paginator(pager)
35
- else
36
- @paginator = pager
37
- end
38
- end
39
-
40
- # Munson::QueryBuilder factory
41
- #
42
- # @example creating a query
43
- # @agent.includes('user').sort(age: :desc)
44
- #
45
- # @return [Munson::QueryBuilder] a query builder
46
- def query
47
- if paginator
48
- query_pager = paginator.new(paginator_options || {})
49
- @query_builder.new paginator: query_pager, agent: self
50
- else
51
- @query_builder.new agent: self
52
- end
5
+ # @param [#to_s] path to JSON API Resource. Path will be added to the base path set in the Faraday::Connection
6
+ # @param [Munson::Connection] connection to use
7
+ def initialize(path, connection: nil)
8
+ @path = path
9
+ @connection = connection
53
10
  end
54
11
 
55
12
  # Connection that will be used for HTTP requests
@@ -60,36 +17,42 @@ module Munson
60
17
  Munson.default_connection
61
18
  end
62
19
 
63
- def find(id, headers: nil, params: nil)
64
- path = [type, id].join('/')
65
- response = get(path: path, headers: headers, params: params)
66
- ResponseMapper.new(response).resource
67
- end
68
-
69
20
  # JSON API Spec GET request
70
21
  #
71
22
  # @option [Hash,nil] params: nil query params
72
- # @option [String] path: nil path to GET, defaults to Faraday::Connection url + Agent#type
23
+ # @option [String] path: nil path to GET, defaults to Faraday::Connection url + Agent#default_path
73
24
  # @option [Hash] headers: nil HTTP Headers
25
+ # @option [String,Fixnum] id: nil ID to append to @path (provided in #new) when accessing a resource. If :path and :id are both specified, :path wins
74
26
  # @return [Faraday::Response]
75
- def get(params: nil, path: nil, headers: nil)
27
+ def get(params: nil, path: nil, headers: nil, id: nil)
76
28
  connection.get(
77
- path: (path || type),
29
+ path: negotiate_path(path, id),
78
30
  params: params,
79
31
  headers: headers
80
32
  )
81
33
  end
82
34
 
35
+ def negotiate_path(path = nil, id = nil)
36
+ if path
37
+ path
38
+ elsif id
39
+ [@path, id].join('/')
40
+ else
41
+ @path
42
+ end
43
+ end
44
+
83
45
  # JSON API Spec POST request
84
46
  #
85
47
  # @option [Hash,nil] body: {} query params
86
- # @option [String] path: nil path to GET, defaults to Faraday::Connection url + Agent#type
48
+ # @option [String] path: nil path to GET, defaults to Faraday::Connection url + Agent#default_path
87
49
  # @option [Hash] headers: nil HTTP Headers
88
50
  # @option [Type] http_method: :post describe http_method: :post
51
+ # @option [String,Fixnum] id: nil ID to append to default path when accessing a resource. If :path and :id are both specified, :path wins
89
52
  # @return [Faraday::Response]
90
- def post(body: {}, path: nil, headers: nil, http_method: :post)
53
+ def post(body: {}, path: nil, headers: nil, http_method: :post, id: nil)
91
54
  connection.post(
92
- path: (path || type),
55
+ path: negotiate_path(path, id),
93
56
  body: body,
94
57
  headers: headers,
95
58
  http_method: http_method
@@ -99,31 +62,34 @@ module Munson
99
62
  # JSON API Spec PATCH request
100
63
  #
101
64
  # @option [Hash,nil] body: nil query params
102
- # @option [String] path: nil path to GET, defaults to Faraday::Connection url + Agent#type
65
+ # @option [String] path: nil path to GET, defaults to Faraday::Connection url + Agent#default_path
103
66
  # @option [Hash] headers: nil HTTP Headers
67
+ # @option [String,Fixnum] id: nil ID to append to default path when accessing a resource. If :path and :id are both specified, :path wins
104
68
  # @return [Faraday::Response]
105
- def patch(body: nil, path: nil, headers: nil)
106
- post(body, path: path, headers: headers, http_method: :patch)
69
+ def patch(body: nil, path: nil, headers: nil, id: nil)
70
+ post(body: body, path: path, headers: headers, http_method: :patch, id: id)
107
71
  end
108
72
 
109
73
  # JSON API Spec PUT request
110
74
  #
111
75
  # @option [Hash,nil] body: nil query params
112
- # @option [String] path: nil path to GET, defaults to Faraday::Connection url + Agent#type
76
+ # @option [String] path: nil path to GET, defaults to Faraday::Connection url + Agent#default_path
113
77
  # @option [Hash] headers: nil HTTP Headers
78
+ # @option [String,Fixnum] id: nil ID to append to default path when accessing a resource. If :path and :id are both specified, :path wins
114
79
  # @return [Faraday::Response]
115
- def put(body: nil, path: nil, headers: nil)
116
- post(body, path: path, headers: headers, http_method: :put)
80
+ def put(body: nil, path: nil, headers: nil, id: nil)
81
+ post(body: body, path: path, headers: headers, http_method: :put, id: id)
117
82
  end
118
83
 
119
84
  # JSON API Spec DELETE request
120
85
  #
121
86
  # @option [Hash,nil] body: nil query params
122
- # @option [String] path: nil path to GET, defaults to Faraday::Connection url + Agent#type
87
+ # @option [String] path: nil path to GET, defaults to Faraday::Connection url + Agent#default_path
123
88
  # @option [Hash] headers: nil HTTP Headers
89
+ # @option [String,Fixnum] id: nil ID to append to default path when accessing a resource. If :path and :id are both specified, :path wins
124
90
  # @return [Faraday::Response]
125
- def delete(body: nil, path: nil, headers: nil)
126
- post(body, path: path, headers: headers, http_method: :delete)
91
+ def delete(body: nil, path: nil, headers: nil, id: nil)
92
+ post(body: body, path: path, headers: headers, http_method: :delete, id: id)
127
93
  end
128
94
  end
129
95
  end
@@ -0,0 +1,76 @@
1
+ module Munson
2
+ class Attribute
3
+ attr_reader :name
4
+ attr_reader :cast_type
5
+ attr_reader :options
6
+
7
+ def initialize(name, cast_type, options={})
8
+ options[:default] ||= nil
9
+ options[:array] ||= false
10
+ @name = name
11
+ @cast_type = cast_type
12
+ @options = options
13
+ end
14
+
15
+ # Process a raw JSON value
16
+ def process(value)
17
+ value.nil? ? default_value : cast(value)
18
+ end
19
+
20
+ # Super naive casting!
21
+ def cast(value)
22
+ return (@options[:array] ? [] : nil) if value.nil?
23
+ value.is_a?(Array) ?
24
+ value.map { |v| cast_value(v) } :
25
+ cast_value(value)
26
+ end
27
+
28
+ def cast_value(value)
29
+ return nil if value.nil?
30
+
31
+ case cast_type
32
+ when Proc
33
+ cast_type.call(value)
34
+ when :string, :to_s, String
35
+ value.to_s
36
+ when :integer, :to_i, Fixnum
37
+ value.to_i
38
+ when :bigdecimal
39
+ BigDecimal.new(value.to_s)
40
+ when :float, :to_f, Float
41
+ value.to_f
42
+ when :date, Date
43
+ Date.parse(value) rescue nil
44
+ when :time, Time
45
+ Time.parse(value) rescue nil
46
+ else
47
+ value
48
+ end
49
+ end
50
+
51
+
52
+ # Serializes the value back to JSON datatype
53
+ #
54
+ def serialize(value)
55
+ case options[:serialize]
56
+ when Proc
57
+ options[:serialize].call(value)
58
+ when Symbol
59
+ value.send(options[:serialize])
60
+ else
61
+ value
62
+ end
63
+ end
64
+
65
+ def default_value
66
+ case @options[:default]
67
+ when Proc
68
+ @options[:default].call
69
+ when nil
70
+ @options[:array] ? [] : nil
71
+ else
72
+ @options[:default].clone
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,43 @@
1
+ module Munson
2
+ class Client
3
+ extend Forwardable
4
+ def_delegators :query, :include, :sort, :filter, :fields, :fetch, :page, :find
5
+ def_delegators :connection, :url=, :url, :response_key_format, :response_key_format=
6
+
7
+ attr_writer :path
8
+ attr_writer :query_builder
9
+ attr_accessor :type
10
+
11
+ def initialize(opts={})
12
+ opts.each do |k,v|
13
+ setter = "#{k}="
14
+ send(setter,v) if respond_to?(setter)
15
+ end
16
+ end
17
+
18
+ def query
19
+ (@query_builder || Query).new(self)
20
+ end
21
+
22
+ def agent
23
+ Agent.new(path, connection: connection)
24
+ end
25
+
26
+ def path
27
+ @path || "/#{type}"
28
+ end
29
+
30
+ def configure(&block)
31
+ yield(self)
32
+ self
33
+ end
34
+
35
+ def connection
36
+ @connection ||= Munson.default_connection.clone
37
+ end
38
+
39
+ def connection=(connection)
40
+ @connection = connection
41
+ end
42
+ end
43
+ end
@@ -1,4 +1,22 @@
1
1
  module Munson
2
- class Collection < ::Array
2
+ class Collection
3
+ include Enumerable
4
+ extend Forwardable
5
+ def_delegator :@collection, :last
6
+
7
+ attr_reader :meta
8
+ attr_reader :jsonapi
9
+ attr_reader :links
10
+
11
+ def initialize(collection=[], errors: nil, meta: nil, jsonapi: nil, links: nil)
12
+ @collection = collection
13
+ @meta = meta
14
+ @jsonapi = jsonapi
15
+ @links = links
16
+ end
17
+
18
+ def each(&block)
19
+ @collection.each(&block)
20
+ end
3
21
  end
4
22
  end
@@ -7,6 +7,7 @@ module Munson
7
7
  # @private
8
8
  attr_reader :faraday, :options
9
9
 
10
+ CONNECTION_OPTIONS = [:response_key_format].freeze
10
11
  FARADAY_OPTIONS = [:request, :proxy, :ssl, :builder, :url,
11
12
  :parallel_manager, :params, :headers, :builder_class].freeze
12
13
 
@@ -14,21 +15,35 @@ module Munson
14
15
  # a faraday connection that includes two pieces of middleware for handling
15
16
  # JSON API Spec
16
17
  #
17
- # @param [Hash] opts {Munson::Connection} configuration options
18
+ # @param [Hash] args {Munson::Connection} configuration arguments
18
19
  # @param [Proc] block to yield to Faraday::Connection
19
20
  # @see https://github.com/lostisland/faraday/blob/master/lib/faraday/connection.rb Faraday::Connection
20
21
  #
22
+ # @example Setting the key format
23
+ # $my_connection = Munson::Connection.new response_key_format: :dasherize, url: "http://api.example.com" do |c|
24
+ # c.use Your::Custom::Middleware
25
+ # end
26
+ #
27
+ # $my_connection = Munson::Connection.new response_key_format: :camelize, url: "http://api.example.com" do |c|
28
+ # c.use Your::Custom::Middleware
29
+ # end
30
+ #
21
31
  # @example Creating a new connection
22
- # my_connection = Munson::Connection.new url: "http://api.example.com" do |c|
32
+ # $my_connection = Munson::Connection.new url: "http://api.example.com" do |c|
23
33
  # c.use Your::Custom::Middleware
24
34
  # end
25
35
  #
26
- # class User
27
- # include Munson::Resource
28
- # munson.connection = my_connection
36
+ # class User < Munson::Resource
37
+ # self.type = :users
38
+ # munson.connection = $my_connection
29
39
  # end
30
- def initialize(opts, &block)
31
- configure(opts, &block)
40
+ def initialize(args, &block)
41
+ configure(args, &block)
42
+ end
43
+
44
+ # Clones a connection
45
+ def clone
46
+ Connection.new(options.dup, &@block)
32
47
  end
33
48
 
34
49
  # JSON API Spec GET request
@@ -43,7 +58,7 @@ module Munson
43
58
  def get(path: nil, params: nil, headers: nil)
44
59
  faraday.get do |request|
45
60
  request.headers.merge!(headers) if headers
46
- request.url path.to_s, params
61
+ request.url path.to_s, externalize_keys(params)
47
62
  end
48
63
  end
49
64
 
@@ -55,14 +70,13 @@ module Munson
55
70
  # @option [Type] http_method: :post describe http_method: :post
56
71
  # @return [Faraday::Response]
57
72
  def post(body: {}, path: nil, headers: nil, http_method: :post)
58
- connection.faraday.send(http_method) do |request|
73
+ faraday.send(http_method) do |request|
59
74
  request.headers.merge!(headers) if headers
60
75
  request.url path.to_s
61
76
  request.body = body
62
77
  end
63
78
  end
64
79
 
65
-
66
80
  # Configure the connection
67
81
  #
68
82
  # @param [Hash] opts {Munson::Connection} configuration options
@@ -82,18 +96,51 @@ module Munson
82
96
  # Munson::Connection.new url: "http://api.example.com" do |c|
83
97
  # c.use MyTokenAuth
84
98
  # end
85
- def configure(opts={}, &block)
86
- @options = opts
99
+ def configure(args={}, &block)
100
+ # Cache these for #clone method
101
+ @options = args
102
+ @block = block
87
103
 
88
104
  faraday_options = @options.reject { |key, value| !FARADAY_OPTIONS.include?(key.to_sym) }
89
105
  @faraday = Faraday.new(faraday_options) do |conn|
90
106
  yield conn if block_given?
91
107
 
92
- conn.use Munson::Middleware::EncodeJsonApi
93
- conn.use Munson::Middleware::JsonParser
108
+ conn.request :"Munson::Middleware::EncodeJsonApi", key_formatter
109
+ conn.response :"Munson::Middleware::JsonParser", key_formatter
94
110
 
95
111
  conn.adapter Faraday.default_adapter
96
112
  end
97
113
  end
114
+
115
+ (CONNECTION_OPTIONS + FARADAY_OPTIONS).each do |option_writer|
116
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
117
+ # Set the option value and reconfigure this connection
118
+ def #{option_writer}=(value)
119
+ @options[:#{option_writer}] = value
120
+ configure(@options, &@block)
121
+ @options[:#{option_writer}]
122
+ end
123
+ RUBY
124
+ end
125
+
126
+ def url
127
+ faraday.url_prefix.to_s
128
+ end
129
+
130
+ def response_key_format
131
+ @options[:response_key_format]
132
+ end
133
+
134
+ private def key_formatter
135
+ response_key_format ? Munson::KeyFormatter.new(response_key_format) : nil
136
+ end
137
+
138
+ private def externalize_keys(value)
139
+ if response_key_format && value.is_a?(Hash)
140
+ key_formatter.externalize(value)
141
+ else
142
+ value
143
+ end
144
+ end
98
145
  end
99
146
  end
@@ -0,0 +1,140 @@
1
+ module Munson
2
+ class Document
3
+ attr_accessor :id
4
+ attr_reader :type
5
+
6
+ def initialize(jsonapi_document)
7
+ @id = jsonapi_document[:data][:id]
8
+ @type = jsonapi_document[:data][:type].to_sym
9
+ @jsonapi_document = jsonapi_document
10
+
11
+ if jsonapi_document[:data] && jsonapi_document[:data][:attributes]
12
+ @original_attributes = jsonapi_document[:data][:attributes].clone
13
+ @attributes = jsonapi_document[:data][:attributes].clone
14
+ else
15
+ @original_attributes = {}
16
+ @attributes = {}
17
+ end
18
+ end
19
+
20
+ # @return [Hash] hash for persisting this JSON API Resource via POST/PATCH/PUT
21
+ def payload
22
+ doc = { data: { type: @type } }
23
+ if id
24
+ doc[:data][:id] = id
25
+ doc[:data][:attributes] = changed
26
+ else
27
+ doc[:data][:attributes] = attributes
28
+ end
29
+ doc
30
+ end
31
+
32
+ def data
33
+ @jsonapi_document[:data]
34
+ end
35
+
36
+ def included
37
+ @jsonapi_document[:included] || []
38
+ end
39
+
40
+ def attributes
41
+ @attributes
42
+ end
43
+
44
+ def attributes=(attrs)
45
+ @attributes.merge!(attrs)
46
+ end
47
+
48
+ def changes
49
+ attributes.reduce({}) do |memo, (k,v)|
50
+ if @original_attributes[k] != attributes[k]
51
+ memo[k] = [@original_attributes[k], attributes[k]]
52
+ end
53
+ memo
54
+ end
55
+ end
56
+
57
+ def changed
58
+ attributes.reduce({}) do |memo, (k,v)|
59
+ if @original_attributes[k] != attributes[k]
60
+ memo[k] = attributes[k]
61
+ end
62
+ memo
63
+ end
64
+ end
65
+
66
+ def save(agent)
67
+ response = if id
68
+ agent.patch(id: id.to_s, body: payload)
69
+ else
70
+ agent.post(body: payload)
71
+ end
72
+
73
+ Munson::Document.new(response.body)
74
+ end
75
+
76
+ def url
77
+ links[:self]
78
+ end
79
+
80
+ def [](key)
81
+ attributes[key]
82
+ end
83
+
84
+ def errors
85
+ data[:errors] || []
86
+ end
87
+
88
+ # Raw relationship hashes
89
+ def relationships
90
+ data[:relationships] || {}
91
+ end
92
+
93
+ def links
94
+ data[:links] || {}
95
+ end
96
+
97
+ def meta
98
+ data[:meta] || {}
99
+ end
100
+
101
+ # Initialized {Munson::Document} from #relationships
102
+ # @param [Symbol] name of relationship
103
+ def relationship(name)
104
+ if relationship_data(name).is_a?(Array)
105
+ relationship_data(name).map { |meta_data| find_included_item(meta_data) }
106
+ elsif relationship_data(name).is_a?(Hash)
107
+ find_included_item(relationship_data(name))
108
+ else
109
+ raise RelationshipNotFound, <<-ERR
110
+ The relationship `#{name}` was called, but does not exist on the document.
111
+ Relationships available are: #{relationships.keys.join(',')}
112
+ ERR
113
+ end
114
+ end
115
+
116
+ def relationship_data(name)
117
+ relationships[name] ? relationships[name][:data] : nil
118
+ end
119
+
120
+ # @param [Hash] relationship from JSONAPI relationships hash
121
+ # @return [Munson::Document,nil] the included relationship, if found
122
+ private def find_included_item(relationship)
123
+ resource = included.find do |included_resource|
124
+ included_resource[:type] == relationship[:type] &&
125
+ included_resource[:id] == relationship[:id]
126
+ end
127
+
128
+ if resource
129
+ Document.new(data: resource, included: included)
130
+ else
131
+ raise RelationshipNotIncludedError, <<-ERR
132
+ The relationship `#{relationship[:type]}` was called,
133
+ but it was not included in the request.
134
+
135
+ Try adding `include=#{relationship[:type]}` to your query.
136
+ ERR
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,87 @@
1
+ class Munson::KeyFormatter
2
+ # @param [Symbol] resource_key_format
3
+ def initialize(resource_key_format)
4
+ @format = resource_key_format
5
+ end
6
+
7
+ # Converts underscored keys to `format`
8
+ def externalize(hash)
9
+ deep_transform_keys(hash) do |key|
10
+ if @format == :dasherize
11
+ dasherize(key)
12
+ elsif @format == :camelize
13
+ camelize(key)
14
+ else
15
+ raise UnrecognizedKeyFormatter, <<-ERR
16
+ No key formatter found for `#{@format}`.
17
+
18
+ Valid :key_format values are `:camelize` and `:underscore`.
19
+ You may also provide a hash of lambdas `{format:->(key){}, unformat:->(key){}}`
20
+ ERR
21
+ end
22
+ end
23
+ end
24
+
25
+ # Converts keys formatted in `format` to underscore
26
+ def internalize(hash)
27
+ deep_transform_keys(hash) do |key|
28
+ if @format == :dasherize
29
+ undasherize(key)
30
+ elsif @format == :camelize
31
+ underscore(key)
32
+ else
33
+ raise UnrecognizedKeyFormatter, <<-ERR
34
+ No key formatter found for `#{@format}`.
35
+
36
+ Valid :key_format values are `:camelize` and `:underscore`.
37
+ You may also provide a hash of lambdas `{format:->(key){}, unformat:->(key){}}`
38
+ ERR
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+ def deep_transform_keys(hash, &block)
45
+ result = {}
46
+ hash.each do |key, value|
47
+ result[yield(key)] = map_value(value, &block)
48
+ end
49
+ result
50
+ end
51
+
52
+ def map_value(value, &block)
53
+ case value
54
+ when Hash
55
+ deep_transform_keys(value, &block)
56
+ when Array
57
+ value.map { |v| map_value(v, &block) }
58
+ else
59
+ value
60
+ end
61
+ end
62
+
63
+ def camelize(key)
64
+ string = key.to_s
65
+ string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { |match| match.downcase }
66
+ string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }
67
+ string.to_sym
68
+ end
69
+
70
+ def underscore(camel_cased_word)
71
+ return camel_cased_word unless camel_cased_word =~ /[A-Z-]|::/
72
+ word = camel_cased_word.to_s
73
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze)
74
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze)
75
+ word.tr!("-".freeze, "_".freeze)
76
+ word.downcase!
77
+ word.to_sym
78
+ end
79
+
80
+ def undasherize(key)
81
+ key.to_s.tr('-'.freeze, '_'.freeze).to_sym
82
+ end
83
+
84
+ def dasherize(key)
85
+ key.to_s.tr('_'.freeze, '-'.freeze).to_sym
86
+ end
87
+ end