munson 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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