hal_api-rails 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,32 @@
1
+ require 'roar/json/hal'
2
+
3
+ module HalApi::Errors
4
+
5
+ class ApiError < StandardError
6
+ attr_accessor :status
7
+
8
+ def initialize(message = nil, status = 500)
9
+ super(message || "API Error")
10
+ self.status = status
11
+ end
12
+ end
13
+
14
+ class NotFound < ApiError
15
+ def initialize(message = nil)
16
+ super(message || "Resource Not Found", 404)
17
+ end
18
+ end
19
+
20
+ class UnsupportedMediaType < ApiError
21
+ def initialize(type)
22
+ super("Unsupported Media Type '#{type.inspect}'", 415)
23
+ end
24
+ end
25
+
26
+ module Representer
27
+ include Roar::JSON::HAL
28
+
29
+ property :status
30
+ property :message
31
+ end
32
+ end
@@ -0,0 +1,77 @@
1
+ require 'forwardable'
2
+ require 'openssl'
3
+ require 'active_model'
4
+
5
+ class HalApi::PagedCollection
6
+ extend ActiveModel::Naming
7
+ extend Forwardable
8
+
9
+ attr_accessor :items, :request, :options
10
+
11
+ def_delegators :items, :total_count, :prev_page, :next_page, :total_pages, :first_page?, :last_page?
12
+ alias_method :total, :total_count
13
+
14
+ def_delegators :request, :params
15
+
16
+ def to_model
17
+ self
18
+ end
19
+
20
+ def persisted?
21
+ false
22
+ end
23
+
24
+ def initialize(items, request=nil, options=nil)
25
+ self.items = items
26
+ self.request = request || request_stub
27
+ self.options = options || {}
28
+ self.options[:is_root_resource] = true unless (self.options[:is_root_resource] == false)
29
+ end
30
+
31
+ def cache_key
32
+ item_keys = items.inject([]) do |keys, i|
33
+ keys << i.try(:id)
34
+ keys << i.try(:updated_at).try(:utc).to_i
35
+ end
36
+ key_components = ['c', item_class.model_name.cache_key]
37
+ key_components << OpenSSL::Digest::MD5.hexdigest(item_keys.join)
38
+ ActiveSupport::Cache.expand_cache_key(key_components)
39
+ end
40
+
41
+ def request_stub
42
+ OpenStruct.new(params: {})
43
+ end
44
+
45
+ def is_root_resource
46
+ !!self.options[:is_root_resource]
47
+ end
48
+
49
+ def show_curies
50
+ is_root_resource && !options[:no_curies]
51
+ end
52
+
53
+ def item_class
54
+ options[:item_class] || self.items.first.try(:item_class) || self.items.first.class
55
+ end
56
+
57
+ def item_decorator
58
+ options[:item_decorator] || "Api::#{item_class.name}Representer".constantize
59
+ end
60
+
61
+ # url to use for the self:href, can be a string or proc
62
+ def url
63
+ options[:url]
64
+ end
65
+
66
+ # If this is an embedded collection, the parent will be set here for use in urls
67
+ def parent
68
+ rep = options[:parent]
69
+ return rep unless rep.respond_to?(:becomes)
70
+ klass = rep.class.try(:base_class)
71
+ (klass && (klass != rep.class)) ? rep.becomes(klass) : rep
72
+ end
73
+
74
+ def count
75
+ items.length
76
+ end
77
+ end
@@ -0,0 +1,15 @@
1
+ require "hal_api/rails/version"
2
+ require "hal_api"
3
+
4
+ module HalApi::Rails
5
+ require "responders"
6
+
7
+ Mime::Type.register 'application/hal+json', :hal
8
+
9
+ # roar fix for 4.1
10
+ # https://github.com/apotonick/roar-rails/issues/65
11
+ ::ActionController::Renderers.add :hal do |js, options|
12
+ self.content_type ||= Mime::HAL
13
+ js.to_json
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ module HalApi
2
+ module Rails
3
+ VERSION = "0.2.0"
4
+ end
5
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ module HalApi::RepresentedModel
4
+ extend ActiveSupport::Concern
5
+
6
+ # this concept is used in result set, perhaps doesn't belong in model itself
7
+ # consider if there is a better way to do this - decorate model instead?
8
+ attr_accessor :is_root_resource
9
+
10
+ def is_root_resource
11
+ !!@is_root_resource
12
+ end
13
+
14
+ def show_curies
15
+ is_root_resource
16
+ end
17
+
18
+ included do
19
+ extend ActiveModel::Naming unless (method(:model_name) rescue nil)
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ class HalApi::Representer < Roar::Decorator
4
+ require 'hal_api/representer/caches'
5
+ require 'hal_api/representer/curies'
6
+ require 'hal_api/representer/embeds'
7
+ require 'hal_api/representer/format_keys'
8
+ require 'hal_api/representer/link_serialize'
9
+ require 'hal_api/representer/uri_methods'
10
+
11
+ include Roar::JSON::HAL
12
+ include HalApi::Representer::FormatKeys
13
+ include HalApi::Representer::UriMethods
14
+ include HalApi::Representer::Curies
15
+ include HalApi::Representer::Embeds
16
+ include HalApi::Representer::Caches
17
+ include HalApi::Representer::LinkSerialize
18
+ self_link
19
+ profile_link
20
+ end
@@ -0,0 +1,44 @@
1
+ require 'active_support/concern'
2
+
3
+ # expects underlying model to have filename, class, and id attributes
4
+ module HalApi::Representer::Caches
5
+ extend ActiveSupport::Concern
6
+ require 'hal_api/representer/caches/serialized_json'
7
+
8
+ # Pass in an option for the format this is going `to_`
9
+ # used in caching the final string format of the obj
10
+ # rather than the intermediary `Hash`, a modest accelerant
11
+ def to_json(options={})
12
+ options[:to_] = :json
13
+ super(options)
14
+ end
15
+
16
+ # def create_representation_with(doc, options, format)
17
+ # cache.fetch(cache_key(represented, options), cache_options) do
18
+ # response = super(doc, options, format)
19
+ # response = HalApi::Representer::Caches::SerializedJson.new(JSON.dump(response)) if (options[:to_] == :json)
20
+ # response
21
+ # end
22
+ # end
23
+
24
+ def cache_key(obj, options)
25
+ key_components = [cache_key_class_name]
26
+ key_components << (obj.try(:is_root_resource) ? 'r' : 'l')
27
+ key_components << obj
28
+ key_components << options.select{|k,v| Array(options['_keys']).include?(k.to_s)}
29
+
30
+ ActiveSupport::Cache.expand_cache_key(key_components)
31
+ end
32
+
33
+ def cache
34
+ Rails.cache
35
+ end
36
+
37
+ def cache_key_class_name
38
+ self.class.name.underscore.gsub(/_representer$/, "")
39
+ end
40
+
41
+ def cache_options
42
+ {compress: true, race_condition_ttl: 10, expires_in: 1.hour}
43
+ end
44
+ end
@@ -0,0 +1,9 @@
1
+ # Wrapper for string that is already json
2
+ # inspired by: http://grosser.it/2013/10/16/compiled-json-for-partially-cached-json-response-precompiled-handlebar-templates/
3
+ class HalApi::Representer::Caches::SerializedJson
4
+ def initialize(s); @s = s; end
5
+ def to_json(*args); @s; end
6
+ def to_s; @s; end
7
+
8
+ undef_method :as_json
9
+ end
@@ -0,0 +1,62 @@
1
+ require 'active_support/concern'
2
+
3
+ module HalApi::Representer::Curies
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_eval do
8
+ class_attribute :default_curie, instance_writer: false
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+
14
+ LINK_RELATIONS = %w(about alternate appendix archives author bookmark
15
+ canonical chapter collection contents copyright create-form curies current
16
+ describedby describes disclosure duplicate edit edit-form edit-media
17
+ enclosure first glossary help hosts hub icon index item last latest-version
18
+ license lrdd memento monitor monitor-group next next-archive nofollow
19
+ noreferrer original payment predecessor-version prefetch prev preview
20
+ previous prev-archive privacy-policy profile related replies search section
21
+ self service start stylesheet subsection successor-version tag
22
+ terms-of-service timegate timemap type up version-history via working-copy
23
+ working-copy-of)
24
+
25
+ def use_curie(curie)
26
+ self.default_curie = curie
27
+ end
28
+
29
+ def curies(curie, &block)
30
+ use_curie(curie) unless default_curie
31
+ link({rel: :curies, array: true}) do
32
+ if represented.try(:show_curies)
33
+ block.call
34
+ end
35
+ end
36
+ end
37
+
38
+ def curify(rel, curie=default_curie)
39
+ if curie.blank? || rel.to_s =~ /:/ || LINK_RELATIONS.include?(rel.to_s)
40
+ rel
41
+ else
42
+ "#{curie}:#{rel}".to_sym
43
+ end
44
+ end
45
+
46
+ def link(options, &block)
47
+ options = {:rel => options} unless options.is_a?(Hash)
48
+ options[:rel] = curify(options[:rel])
49
+ super(options, &block)
50
+ end
51
+
52
+ def property(name, options={})
53
+ options[:as] = curify(options[:as] || name) if options[:embedded]
54
+ super(name, options)
55
+ end
56
+
57
+ def collection(name, options={})
58
+ options[:as] = curify(options[:as] || name) if options[:embedded]
59
+ super(name, options)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,87 @@
1
+ require 'active_support/concern'
2
+
3
+ # expects underlying model to have filename, class, and id attributes
4
+ module HalApi::Representer::Embeds
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ Representable::Mapper.send(:include, Resources) if !Representable::Mapper.include?(Resources)
9
+ end
10
+
11
+ module Resources
12
+
13
+ def skip_property?(binding, options)
14
+ super(binding, options) || suppress_embed?(binding, options)
15
+ end
16
+
17
+ # embed if zoomed
18
+ def suppress_embed?(binding, options)
19
+ name = (binding[:as] || binding.name).to_s
20
+ embedded = !!binding[:embedded]
21
+
22
+ # not embedded, return false - nothing to suppress
23
+ return false if !embedded
24
+
25
+ # check if it should be zoomed, suppress if not
26
+ !embed_zoomed?(name, binding[:zoom], options[:zoom])
27
+ end
28
+
29
+ def embed_zoomed?(name, zoom_def=nil, zoom_param=nil)
30
+ # if the embed in the representer definition has `zoom: :always` defined
31
+ # always embed it, even if it is in another embed
32
+ # (this is really meant for collections where embedded items must be included)
33
+ return true if zoom_def == :always
34
+
35
+ # passing nil explicitly overwrites defaults in signature,
36
+ # so we default to nil and fix in the method body
37
+ zoom_def = true if zoom_def.nil?
38
+
39
+ # if there is no zoom specified in the request params (options)
40
+ # then embed based on the zoom option in the representer definition
41
+
42
+ # if there is a zoom specified in the request params (options)
43
+ # then do not zoom when this name is not in the request
44
+ zoom_param.nil? ? zoom_def : zoom_param.include?(name)
45
+ end
46
+ end
47
+
48
+ # Possible values for zoom option in the embed representer definition
49
+ # * false - will be zoomed only if in the root doc and in the zoom param
50
+ # * true - zoomed in root doc if no zoom_param, or if included in zoom_param
51
+ # * always - zoomed no matter what is in zoom param, and even if in embed
52
+ module ClassMethods
53
+
54
+ def embed(name, options={})
55
+ options[:embedded] = true
56
+ options[:writeable] = false
57
+ options[:if] ||= ->(_a) { id } unless options[:zoom] == :always
58
+
59
+ if options[:paged]
60
+ opts = {
61
+ no_curies: true,
62
+ item_class: options.delete(:item_class),
63
+ url: options.delete(:url),
64
+ item_decorator: options.delete(:item_decorator),
65
+ per: options.delete(:per) || Kaminari.config.default_per_page
66
+ }
67
+ options[:getter] ||= ->(*) do
68
+ # set # per page based on default, option value integer, or special :all
69
+ per = opts.delete(:per)
70
+ per = self.send(name).count if per == :all
71
+ PagedCollection.new(self.send(name).page(1).per(per), nil, opts.merge({parent: self}))
72
+ end
73
+ options[:decorator] = Api::PagedCollectionRepresenter
74
+ end
75
+
76
+ property(name, options)
77
+ end
78
+
79
+ def embeds(name, options={})
80
+ options[:embedded] = true
81
+ options[:writeable] = false
82
+ options[:if] ||= ->(_a) { id } unless options[:zoom] == :always
83
+
84
+ collection(name, options)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,28 @@
1
+ require 'active_support/concern'
2
+
3
+ # Link relation names seem to have a standard of being dasherized, as they are URIs
4
+ # property names, on the other hand should, should be camelcase, lower first
5
+ module HalApi::Representer::FormatKeys
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+
10
+ def link(options, &block)
11
+ options = {:rel => options} unless options.is_a?(Hash)
12
+ options[:rel] = options[:rel].to_s.dasherize
13
+ super(options, &block)
14
+ end
15
+
16
+ def property(name, options={})
17
+ n = (options[:as] || name).to_s
18
+ options[:as] = options[:embedded] ? n.dasherize : n.camelize(:lower)
19
+ super(name, options)
20
+ end
21
+
22
+ def collection(name, options={})
23
+ n = (options[:as] || name).to_s
24
+ options[:as] = options[:embedded] ? n.dasherize : n.camelize(:lower)
25
+ super(name, options)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ require 'active_support/concern'
2
+
3
+ module HalApi::Representer::LinkSerialize
4
+
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+
9
+ def link(options, &block)
10
+ set_link_property(options)
11
+ super(options, &block)
12
+ end
13
+
14
+ def set_link_property(options)
15
+ if options.is_a?(Hash) && (options.delete(:writeable) || options[:reader])
16
+ name = options[:rel].to_s.split(':').last.split('/').last
17
+ pname = "set_#{name}_uri"
18
+ reader = options.delete(:reader) || ->(doc, _args) do
19
+ try("#{name}_id=", id_from_url(doc[pname])) if doc[pname]
20
+ end
21
+
22
+ property(pname, readable: false, reader: reader)
23
+ end
24
+ end
25
+ end
26
+ end