halogen 0.0.1

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.
@@ -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'