halogen 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+ #
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'halogen/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'halogen'
9
+ spec.version = Halogen::VERSION
10
+ spec.authors = ['Heather Rivers']
11
+ spec.email = ['heather@modeanalytics.com']
12
+ spec.summary = 'HAL+JSON generator'
13
+ spec.description = 'Provides a framework-agnostic interface for ' \
14
+ 'generating HAL+JSON representations of resources'
15
+ spec.homepage = 'https://github.com/mode/halogen'
16
+ spec.license = 'MIT'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0")
19
+ spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(/^(test|spec|features)\//)
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.required_ruby_version = '~> 2.0'
24
+
25
+ spec.add_dependency 'json', '~> 1.8'
26
+
27
+ spec.add_development_dependency 'bundler', '~> 1.7'
28
+ spec.add_development_dependency 'rake', '~> 10.0'
29
+ spec.add_development_dependency 'rspec', '~> 3.2'
30
+ spec.add_development_dependency 'simplecov', '~> 0.9'
31
+ spec.add_development_dependency 'yard', '~> 0.8'
32
+ end
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+ #
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__)))
5
+
6
+ require 'json'
7
+
8
+ # HAL+JSON generator
9
+ #
10
+ # Provides a framework-agnostic interface for generating HAL+JSON
11
+ # representations of resources
12
+ #
13
+ module Halogen
14
+ # Provide Halogen methods for the including module
15
+ #
16
+ # @return [Module]
17
+ #
18
+ def self.included(base)
19
+ base.extend ClassMethods
20
+
21
+ base.send :include, InstanceMethods
22
+ base.send :include, Properties
23
+ base.send :include, Links
24
+ base.send :include, Embeds
25
+
26
+ config.extensions.each { |extension| base.send :include, extension }
27
+
28
+ base.send :attr_reader, :options
29
+ end
30
+
31
+ module ClassMethods # :nodoc:
32
+ # @return [Halogen::Definitions] the definitions container instance
33
+ #
34
+ def definitions
35
+ @definitions ||= Definitions.new
36
+ end
37
+
38
+ # @param [Symbol, String] name of the resource
39
+ #
40
+ # @return [Module] self
41
+ #
42
+ def resource(name)
43
+ include Resource
44
+
45
+ define_resource(name)
46
+ end
47
+
48
+ # @param [Symbol, String] name of the collection
49
+ #
50
+ # @return [Module] self
51
+ #
52
+ def collection(name)
53
+ include Collection
54
+
55
+ define_collection(name)
56
+ end
57
+ end
58
+
59
+ module InstanceMethods # :nodoc:
60
+ # @param options [nil, Hash] hash of options
61
+ #
62
+ # @return [Object] the representer instance
63
+ #
64
+ def initialize(options = {})
65
+ @options = Halogen::HashUtil.symbolize_keys!(options)
66
+ end
67
+
68
+ # @return [String] rendered JSON
69
+ #
70
+ def to_json
71
+ render.to_json
72
+ end
73
+
74
+ # @return [Hash] rendered representation
75
+ #
76
+ def render
77
+ {}
78
+ end
79
+
80
+ # @return [nil, Object] the parent representer, if this instance is an
81
+ # embedded child
82
+ #
83
+ def parent
84
+ @parent ||= options.fetch(:parent, nil)
85
+ end
86
+
87
+ # @return [Integer] the depth at which this representer is embedded
88
+ #
89
+ def depth
90
+ @depth ||= parent ? parent.depth + 1 : 0
91
+ end
92
+
93
+ protected
94
+
95
+ # Allow included modules to decorate rendered hash
96
+ #
97
+ # @param key [Symbol] the key (e.g. `embedded`, `links`)
98
+ # @param result [Hash] the partially rendered hash to decorate
99
+ #
100
+ # @return [Hash] the decorated hash
101
+ #
102
+ def decorate_render(key, result)
103
+ result.tap do
104
+ value = send(key)
105
+
106
+ result[:"_#{key}"] = value if value.any?
107
+ end
108
+ end
109
+
110
+ # Iterate through enabled definitions of the given type, allowing instance
111
+ # to build up resulting hash
112
+ #
113
+ # @param type [Symbol, String] the definition type
114
+ #
115
+ # @return [Hash] the result
116
+ #
117
+ def render_definitions(type)
118
+ definitions = self.class.definitions.fetch(type, [])
119
+
120
+ definitions.each_with_object({}) do |definition, result|
121
+ next unless definition.enabled?(self)
122
+
123
+ yield definition, result
124
+ end
125
+ end
126
+ end
127
+
128
+ class << self
129
+ # @yield [Halogen::Configuration] configuration instance for modification
130
+ #
131
+ def configure
132
+ yield config
133
+ end
134
+
135
+ # Configuration instance
136
+ #
137
+ # @return [Halogen::Configuration]
138
+ #
139
+ def config
140
+ @config ||= Configuration.new
141
+ end
142
+ end
143
+ end
144
+
145
+ require 'halogen/collection'
146
+ require 'halogen/configuration'
147
+ require 'halogen/definition'
148
+ require 'halogen/definitions'
149
+ require 'halogen/embeds'
150
+ require 'halogen/errors'
151
+ require 'halogen/links'
152
+ require 'halogen/properties'
153
+ require 'halogen/resource'
154
+ require 'halogen/hash_util'
155
+ require 'halogen/version'
156
+
157
+ require 'halogen/railtie' if defined?(::Rails)
@@ -0,0 +1,44 @@
1
+ # encoding: utf-8
2
+ #
3
+ module Halogen
4
+ # Behavior for representers with a primary collection resource.
5
+ #
6
+ # The main reason to declare a collection is that the resource with that name
7
+ # will always be embedded during rendering.
8
+ #
9
+ module Collection
10
+ def self.included(base) # :nodoc:
11
+ if base.included_modules.include?(Resource)
12
+ fail InvalidCollection, "#{base.name} has already defined a resource"
13
+ end
14
+
15
+ base.extend ClassMethods
16
+
17
+ base.send :include, InstanceMethods
18
+
19
+ base.class.send :attr_accessor, :collection_name
20
+ end
21
+
22
+ module ClassMethods # :nodoc:
23
+ # @param name [Symbol, String] name of the collection
24
+ #
25
+ # @return [Module] self
26
+ #
27
+ def define_collection(name)
28
+ self.collection_name = name.to_s
29
+ end
30
+ end
31
+
32
+ module InstanceMethods # :nodoc:
33
+ # Override super to ensure that the primary collection is always embedded
34
+ #
35
+ # @param key [String] the embed key to check
36
+ #
37
+ # @return [true, false] whether the given key should be embedded
38
+ #
39
+ def embed?(key)
40
+ super || key == self.class.collection_name
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+ #
3
+ module Halogen
4
+ # Simple configuration class
5
+ #
6
+ class Configuration
7
+ # Array of extension modules to be included in all representers
8
+ #
9
+ # @return [Array<Module>]
10
+ #
11
+ def extensions
12
+ @extensions ||= []
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,73 @@
1
+ # encoding: utf-8
2
+ #
3
+ module Halogen
4
+ # Stores instructions for how to render a value for a given representer
5
+ # instance
6
+ #
7
+ class Definition
8
+ attr_reader :name, :options
9
+
10
+ attr_accessor :procedure
11
+
12
+ # Construct a new Definition instance
13
+ #
14
+ # @param name [Symbol, String] definition name
15
+ # @param options [Hash] hash of options
16
+ #
17
+ # @return [Halogen::Definition] the instance
18
+ #
19
+ def initialize(name, options, procedure)
20
+ @name = name.to_sym
21
+ @options = Halogen::HashUtil.symbolize_keys!(options)
22
+ @procedure = procedure
23
+ end
24
+
25
+ # @param instance [Object] the representer instance with which to evaluate
26
+ # the stored procedure
27
+ #
28
+ def value(instance)
29
+ options.fetch(:value) do
30
+ procedure ? instance.instance_eval(&procedure) : instance.send(name)
31
+ end
32
+ end
33
+
34
+ # @return [true, false] whether this definition should be included based on
35
+ # its conditional guard, if any
36
+ #
37
+ def enabled?(instance)
38
+ if options.key?(:if)
39
+ !!eval_guard(instance, options.fetch(:if))
40
+ elsif options.key?(:unless)
41
+ !eval_guard(instance, options.fetch(:unless))
42
+ else
43
+ true
44
+ end
45
+ end
46
+
47
+ # @return [true] if nothing is raised
48
+ #
49
+ # @raise [Halogen::InvalidDefinition] if the definition is invalid
50
+ #
51
+ def validate
52
+ return true unless options.key?(:value) && procedure
53
+
54
+ fail InvalidDefinition,
55
+ "Cannot specify both value and procedure for #{name}"
56
+ end
57
+
58
+ private
59
+
60
+ # Evaluate guard procedure or method
61
+ #
62
+ def eval_guard(instance, guard)
63
+ case guard
64
+ when Proc
65
+ instance.instance_eval(&guard)
66
+ when Symbol, String
67
+ instance.send(guard)
68
+ else
69
+ guard
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+ #
3
+ module Halogen
4
+ # Each representer class has a Halogen::Definitions instance which stores
5
+ # Halogen::Definition instances by type.
6
+ #
7
+ class Definitions < Hash
8
+ # @param definition [Halogen::Definition]
9
+ #
10
+ # @return [Halogen::Definition] the added definition
11
+ #
12
+ def add(definition)
13
+ type = definition.class.name
14
+
15
+ definition.validate
16
+
17
+ self[type] ||= []
18
+ self[type] << definition
19
+
20
+ definition
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,105 @@
1
+ # encoding: utf-8
2
+ #
3
+ module Halogen
4
+ module Embeds # :nodoc:
5
+ def self.included(base) # :nodoc:
6
+ base.extend ClassMethods
7
+
8
+ base.send :include, InstanceMethods
9
+ end
10
+
11
+ module ClassMethods # :nodoc:
12
+ # @param name [Symbol, String]
13
+ # @param options [nil, Hash]
14
+ #
15
+ # @return [Halogen::Embeds::Definition]
16
+ #
17
+ def embed(name, options = {}, &procedure)
18
+ definitions.add(Definition.new(name, options, procedure))
19
+ end
20
+ end
21
+
22
+ module InstanceMethods # :nodoc:
23
+ # @return [Hash] the rendered hash with embedded resources, if any
24
+ #
25
+ def render
26
+ decorate_render :embedded, super
27
+ end
28
+
29
+ # @return [Hash] hash of rendered resources to embed
30
+ #
31
+ def embedded
32
+ render_definitions(Definition.name) do |definition, result|
33
+ next unless embed?(definition.name.to_s)
34
+
35
+ value = instance_eval(&definition.procedure)
36
+
37
+ child = embedded_child(definition.name.to_s, value)
38
+
39
+ result[definition.name] = child if child
40
+ end
41
+ end
42
+
43
+ # @return [Hash, Array<Hash>] either a single rendered child representer
44
+ # or an array of them
45
+ #
46
+ def embedded_child(key, value)
47
+ return unless value
48
+
49
+ opts = child_embed_opts(key)
50
+
51
+ if value.is_a?(Array)
52
+ value.map { |item| render_child(item, opts) }.compact
53
+ else
54
+ render_child(value, opts)
55
+ end
56
+ end
57
+
58
+ # @param key [String]
59
+ #
60
+ # @return [Hash]
61
+ #
62
+ def child_embed_opts(key)
63
+ opts = embed_options.fetch(key, {})
64
+
65
+ # Turn { :report => 1 } into { :report => {} } for child
66
+ opts = {} unless opts.is_a?(Hash)
67
+
68
+ opts
69
+ end
70
+
71
+ # @param repr [Object] the child representer
72
+ # @param opts [Hash] the embed options to assign to the child
73
+ #
74
+ # @return [nil, Hash] the rendered child
75
+ #
76
+ def render_child(repr, opts)
77
+ return unless repr.class.included_modules.include?(Halogen)
78
+
79
+ repr.options.merge!(embed: opts, parent: self)
80
+
81
+ repr.render
82
+ end
83
+
84
+ # @return [Hash] hash of options with top level string keys
85
+ #
86
+ def embed_options
87
+ options.fetch(:embed, {}).tap do |result|
88
+ Halogen::HashUtil.stringify_keys!(result)
89
+ end
90
+ end
91
+
92
+ # @param key [String]
93
+ #
94
+ # @return [true, false] whether to embed the key
95
+ #
96
+ def embed?(key)
97
+ return false unless embed_options.include?(key)
98
+
99
+ !%w(0 false).include?(embed_options.fetch(key).to_s)
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ require 'halogen/embeds/definition'