instedd-pigeon 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. data/.gitignore +25 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +9 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +140 -0
  6. data/Rakefile +1 -0
  7. data/app/assets/images/pigeon/android_local_gateway_preview.png +0 -0
  8. data/app/assets/images/pigeon/android_local_gateway_qr.png +0 -0
  9. data/app/assets/images/pigeon/other_local_gateway_preview.png +0 -0
  10. data/app/assets/javascripts/pigeon/qst-server-wizard.js.coffee +58 -0
  11. data/app/assets/javascripts/pigeon/template.js.coffee +43 -0
  12. data/app/assets/javascripts/pigeon/twitter-template.js.coffee +38 -0
  13. data/app/assets/javascripts/pigeon/wizard.js.coffee +95 -0
  14. data/app/assets/javascripts/pigeon.js +2 -0
  15. data/app/assets/stylesheets/pigeon.css.sass +13 -0
  16. data/app/controllers/pigeon/twitter_controller.rb +44 -0
  17. data/app/helpers/.gitkeep +0 -0
  18. data/app/helpers/pigeon/channel_helper.rb +93 -0
  19. data/app/helpers/pigeon/renderer/base.rb +172 -0
  20. data/app/helpers/pigeon/renderer/channel_renderer.rb +58 -0
  21. data/app/helpers/pigeon/renderer.rb +8 -0
  22. data/app/helpers/pigeon/tag_helper.rb +26 -0
  23. data/app/helpers/pigeon/template_helper.rb +20 -0
  24. data/app/helpers/pigeon/text_helper.rb +16 -0
  25. data/app/models/.gitkeep +0 -0
  26. data/app/models/pigeon/channel.rb +220 -0
  27. data/app/models/pigeon/channel_attribute.rb +64 -0
  28. data/app/models/pigeon/channel_schema.rb +113 -0
  29. data/app/models/pigeon/nested_attribute.rb +23 -0
  30. data/app/models/pigeon/nested_scopes.rb +34 -0
  31. data/app/models/pigeon/nuntium_channel.rb +92 -0
  32. data/app/models/pigeon/verboice_channel.rb +82 -0
  33. data/app/views/pigeon/twitter/callback.html.erb +6 -0
  34. data/config/routes.rb +4 -0
  35. data/config/schemas/nuntium/nuntium.yml +210 -0
  36. data/config/schemas/nuntium/qst-server.yml +180 -0
  37. data/config/schemas/nuntium/twitter.yml +56 -0
  38. data/config/schemas/verboice/verboice.yml +76 -0
  39. data/lib/pigeon/engine.rb +30 -0
  40. data/lib/pigeon/errors.rb +28 -0
  41. data/lib/pigeon/initializer.rb +13 -0
  42. data/lib/pigeon/nuntium.rb +10 -0
  43. data/lib/pigeon/verboice.rb +10 -0
  44. data/lib/pigeon/version.rb +3 -0
  45. data/lib/pigeon.rb +25 -0
  46. data/pigeon.gemspec +24 -0
  47. data/spec/data/test_schemas.yml +21 -0
  48. data/spec/dummy/README.rdoc +261 -0
  49. data/spec/dummy/Rakefile +7 -0
  50. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  51. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  52. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  53. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  54. data/spec/dummy/app/mailers/.gitkeep +0 -0
  55. data/spec/dummy/app/models/.gitkeep +0 -0
  56. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  57. data/spec/dummy/config/application.rb +59 -0
  58. data/spec/dummy/config/boot.rb +10 -0
  59. data/spec/dummy/config/database.yml +25 -0
  60. data/spec/dummy/config/environment.rb +5 -0
  61. data/spec/dummy/config/environments/development.rb +37 -0
  62. data/spec/dummy/config/environments/production.rb +67 -0
  63. data/spec/dummy/config/environments/test.rb +37 -0
  64. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  65. data/spec/dummy/config/initializers/inflections.rb +15 -0
  66. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  67. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  68. data/spec/dummy/config/initializers/session_store.rb +8 -0
  69. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  70. data/spec/dummy/config/locales/en.yml +5 -0
  71. data/spec/dummy/config/routes.rb +3 -0
  72. data/spec/dummy/config.ru +4 -0
  73. data/spec/dummy/lib/assets/.gitkeep +0 -0
  74. data/spec/dummy/log/.gitkeep +0 -0
  75. data/spec/dummy/public/404.html +26 -0
  76. data/spec/dummy/public/422.html +26 -0
  77. data/spec/dummy/public/500.html +25 -0
  78. data/spec/dummy/public/favicon.ico +0 -0
  79. data/spec/dummy/script/rails +6 -0
  80. data/spec/helpers/pigeon/channel_helper_spec.rb +173 -0
  81. data/spec/helpers/pigeon/renderer/base_spec.rb +109 -0
  82. data/spec/helpers/pigeon/renderer/channel_renderer_spec.rb +61 -0
  83. data/spec/helpers/pigeon/tag_helper_spec.rb +36 -0
  84. data/spec/helpers/pigeon/template_helper_spec.rb +57 -0
  85. data/spec/models/pigeon/channel_attribute_spec.rb +98 -0
  86. data/spec/models/pigeon/channel_schema_spec.rb +63 -0
  87. data/spec/models/pigeon/channel_spec.rb +205 -0
  88. data/spec/models/pigeon/nuntium_channel_spec.rb +53 -0
  89. data/spec/spec_helper.rb +43 -0
  90. data/spec/support/active_model_lint.rb +15 -0
  91. data/spec/support/test_schemas.rb +12 -0
  92. metadata +254 -0
@@ -0,0 +1,172 @@
1
+ module Pigeon
2
+ module Renderer
3
+ class Base
4
+ include Sprockets::Helpers::RailsHelper
5
+ include Sprockets::Helpers::IsolatedHelper
6
+ include ActionView::Helpers::OutputSafetyHelper
7
+ include ActionView::Helpers::TextHelper
8
+ include ActionView::Helpers::TagHelper
9
+ include ActionView::Helpers::AssetTagHelper
10
+
11
+ class << self
12
+ def handle(command, handler)
13
+ @handlers ||= {}
14
+ @handlers[command.to_s] = handler
15
+ end
16
+
17
+ def find_handler(command)
18
+ @handlers ||= {}
19
+ @handlers.fetch(command.to_s) do |command|
20
+ if superclass.respond_to?(:find_handler)
21
+ self.superclass.find_handler(command)
22
+ else
23
+ nil
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ attr_accessor :delegate
30
+
31
+ handle :raw, :render_raw
32
+ handle :template, :render_template_command
33
+ handle :wizard, :render_template_command
34
+ handle :page, :render_template_command
35
+
36
+ def render(v)
37
+ return '' if v.empty?
38
+
39
+ if v.first.starts_with? '@'
40
+ render_at_command(v)
41
+ else
42
+ render_vector(v)
43
+ end
44
+ end
45
+
46
+ def render_at_command(v)
47
+ command = v.first[1..-1]
48
+ handler = find_handler(command)
49
+ if !handler.nil?
50
+ dispatch_command_handler(handler, v)
51
+ elsif delegate.present? && delegate.respond_to?(:call)
52
+ delegate.call(v)
53
+ else
54
+ ''
55
+ end
56
+ end
57
+
58
+ def extract_options(data)
59
+ if data[1].is_a?(Hash)
60
+ [data[0], data[1], data.drop(2)]
61
+ else
62
+ [data[0], {}, data.drop(1)]
63
+ end
64
+ end
65
+
66
+ def render_vector(v)
67
+ selector, user_options, content = extract_options(v)
68
+ tag_name, options = parse_tag_name(selector)
69
+
70
+ options.update(user_options.with_indifferent_access) do |key, oldval, newval|
71
+ case key.to_s
72
+ when "id"
73
+ oldval
74
+ when "class"
75
+ oldval + " " + newval
76
+ else
77
+ newval
78
+ end
79
+ end
80
+
81
+ if tag_name.downcase == 'img'
82
+ if URI(path = options[:src]).relative?
83
+ options[:src] = image_path(path)
84
+ end
85
+ end
86
+
87
+ if content.empty?
88
+ if %w(br meta img link script hr).include? tag_name.downcase
89
+ tag tag_name, options
90
+ else
91
+ content_tag tag_name, '', options
92
+ end
93
+ else
94
+ content_html = safe_join(content.map do |elt|
95
+ case elt
96
+ when Array
97
+ render(elt)
98
+ else
99
+ elt
100
+ end
101
+ end)
102
+ content_tag tag_name, content_html, options
103
+ end
104
+ end
105
+
106
+ def render_raw(data)
107
+ data.drop(1).join.html_safe
108
+ end
109
+
110
+ def render_template_command(v)
111
+ command, options, content = extract_options(v)
112
+
113
+ options = options.inject({}) do |options, (key, value)|
114
+ if %w(class style).include?(key.to_s) || key.to_s.starts_with?('data-')
115
+ options[key] = value
116
+ else
117
+ options["data-#{key.to_s}"] = value
118
+ end
119
+ options
120
+ end
121
+ if %w(@template @wizard).include?(command)
122
+ options["data-application-name"] ||= Pigeon.config.application_name
123
+ options["data-twitter-path"] = Pigeon::Engine.routes.url_helpers.twitter_path
124
+ end
125
+
126
+ case command
127
+ when '@wizard'
128
+ render ["div.pigeon.pigeon_wizard", options] + content
129
+ when '@page'
130
+ render ["div.pigeon_wizard_page", options] + content
131
+ when '@template'
132
+ render ["div.pigeon.pigeon_template", options] + content
133
+ else
134
+ ''
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ def parse_tag_name(input)
141
+ tag_name = input.scan(/\A[^#.]*/).first
142
+ tag_name = 'div' if tag_name.blank?
143
+ options = {}.with_indifferent_access
144
+ classes = []
145
+ input.scan(/[#.][^#.]+/) do |tok|
146
+ case tok.first
147
+ when '#'
148
+ options[:id] = tok[1..-1]
149
+ when '.'
150
+ classes << tok[1..-1]
151
+ end
152
+ end
153
+ options[:class] = classes.join(' ') unless classes.empty?
154
+
155
+ [tag_name, options]
156
+ end
157
+
158
+ def find_handler(command)
159
+ self.class.find_handler(command)
160
+ end
161
+
162
+ def dispatch_command_handler(handler, data)
163
+ if handler.is_a?(Symbol)
164
+ self.send(handler, data)
165
+ elsif handler.is_a?(Proc)
166
+ handler.class(data)
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+
@@ -0,0 +1,58 @@
1
+ module Pigeon
2
+ module Renderer
3
+ class ChannelRenderer < Base
4
+ include Pigeon::ChannelHelper
5
+
6
+ attr_accessor :output_buffer
7
+ attr_accessor :channel, :scope
8
+
9
+ handle :field, :render_attribute_command
10
+ handle :label, :render_attribute_command
11
+ handle :attr, :render_attribute_command
12
+ handle :hidden, :render_attribute_command
13
+
14
+ def initialize(channel = nil, scope = nil)
15
+ self.channel = channel
16
+ self.scope = scope
17
+ end
18
+
19
+ def render_attribute_command(v)
20
+ command, options, content = extract_options(v)
21
+ name = content.first
22
+ options[:scope] = scope if scope.present?
23
+
24
+ case command
25
+ when '@field'
26
+ pigeon_channel_attribute_field(channel, name, options)
27
+ when '@label'
28
+ pigeon_channel_attribute_label(channel, name, options)
29
+ when '@attr'
30
+ if options.include?(:class)
31
+ options[:class] << ' pigeon_attribute'
32
+ else
33
+ options[:class] = 'pigeon_attribute'
34
+ end
35
+ pigeon_channel_attribute(channel, name, options)
36
+ when '@hidden'
37
+ options = options.update({ :type => :hidden })
38
+ pigeon_channel_attribute_field(channel, name, options)
39
+ else
40
+ ''
41
+ end
42
+ end
43
+
44
+ def render_template_command(v)
45
+ command, options, content = extract_options(v)
46
+ if %w(@wizard @template).include?(command)
47
+ options["data-scope"] ||= scope if scope.present?
48
+ options["data-persisted"] = channel.persisted?
49
+ options["data-kind"] = channel.kind
50
+ options["data-name"] = channel.name
51
+ end
52
+ super([command, options] + content)
53
+ end
54
+
55
+ end
56
+ end
57
+ end
58
+
@@ -0,0 +1,8 @@
1
+ module Pigeon
2
+ module Renderer
3
+ extend ActiveSupport::Autoload
4
+
5
+ autoload :Base
6
+ end
7
+ end
8
+
@@ -0,0 +1,26 @@
1
+ module Pigeon
2
+ module TagHelper
3
+ def pigeon_verboice_channel_kinds_for_select
4
+ options_for_select(pigeon_schema_options(VerboiceChannel.schemas))
5
+ end
6
+
7
+ def pigeon_nuntium_channel_kinds_for_select
8
+ options_for_select(pigeon_schema_options(NuntiumChannel.schemas))
9
+ end
10
+
11
+ def pigeon_channel_kinds_for_select
12
+ grouped_options_for_select({
13
+ "Voice Channel" =>
14
+ pigeon_schema_options(VerboiceChannel.schemas, 'verboice/'),
15
+ "Message Channel" =>
16
+ pigeon_schema_options(NuntiumChannel.schemas, 'nuntium/')
17
+ })
18
+ end
19
+
20
+ def pigeon_schema_options(schemas, value_prefix = '')
21
+ schemas.map do |c|
22
+ [c.humanized_name, "#{value_prefix}#{c.kind}"]
23
+ end.sort_by(&:first)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ module Pigeon
2
+ module TemplateHelper
3
+ def pigeon_render_template(template, &block)
4
+ renderer = Renderer::Base.new
5
+ renderer.delegate = block if block_given?
6
+ renderer.render template
7
+ end
8
+
9
+ def pigeon_render_channel_template(channel, template = nil, scope = 'channel', &block)
10
+ renderer = Renderer::ChannelRenderer.new channel, scope
11
+ renderer.delegate = block if block_given?
12
+ template ||= channel.schema.template
13
+ renderer.render template
14
+ end
15
+
16
+ def pigeon_render_channel(channel, scope = 'channel')
17
+ pigeon_render_channel_template(channel, nil, scope)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ module Pigeon
2
+ module TextHelper
3
+
4
+ def channel_type_humanize(channel_type)
5
+ case channel_type
6
+ when :verboice
7
+ "Voice channel"
8
+ when :nuntium
9
+ "Message channel"
10
+ else
11
+ "Unknown channel"
12
+ end
13
+ end
14
+
15
+ end
16
+ end
File without changes
@@ -0,0 +1,220 @@
1
+ module Pigeon
2
+ class Channel
3
+ extend ActiveModel::Naming
4
+ extend ActiveModel::Translation
5
+ include ActiveModel::Conversion
6
+ include ActiveModel::Validations
7
+ include NestedScopes
8
+
9
+ class << self
10
+ def type() nil end
11
+
12
+ def schemas
13
+ []
14
+ end
15
+
16
+ def find_type(type)
17
+ begin
18
+ "Pigeon::#{type.to_s.capitalize}Channel".constantize
19
+ rescue
20
+ nil
21
+ end
22
+ end
23
+
24
+ def find_schema(kind)
25
+ schemas.find { |s| s.kind == kind }
26
+ end
27
+
28
+ def channel_accessor(*names)
29
+ options = names.extract_options!
30
+
31
+ names.each do |name|
32
+ reader, line = "def #{name}; attributes['#{name}']; end", __LINE__
33
+ writer, line = "def #{name}=(value); attributes['#{name}'] = value; end", __LINE__
34
+
35
+ class_eval reader, __FILE__, line unless options[:instance_reader] == false
36
+ class_eval writer, __FILE__, line unless options[:instance_writer] == false
37
+ end
38
+ end
39
+
40
+ def list
41
+ raise NotImplementedError
42
+ end
43
+
44
+ def find(*arguments)
45
+ scope = arguments.slice!(0)
46
+ options = arguments.slice!(0) || {}
47
+
48
+ case scope
49
+ when :all
50
+ list.map { |id| find_single(id) }
51
+ when :first
52
+ find_single(list.first)
53
+ when :last
54
+ find_single(list.first)
55
+ else
56
+ find_single(scope)
57
+ end
58
+ end
59
+
60
+ def all() find(:all) end
61
+ def first() find(:first) end
62
+ def last() find(:last) end
63
+
64
+ private
65
+
66
+ def find_single(id)
67
+ raise NotImplementedError
68
+ end
69
+ end
70
+
71
+ attr_reader :attributes
72
+ attr_accessor :schema
73
+
74
+ channel_accessor :name, :kind
75
+
76
+ validates_presence_of :name, :kind
77
+
78
+ def type
79
+ self.class.type
80
+ end
81
+
82
+ def initialize(attrs = {}, persisted = false)
83
+ @errors = ChannelErrors.new(self)
84
+ @attributes = {}.with_indifferent_access
85
+ @persisted = persisted
86
+ @destroyed = false
87
+
88
+ attrs = attrs.dup
89
+ @schema = attrs.delete(:schema) || self.class.find_schema(attrs[:kind])
90
+ attrs[:kind] ||= @schema.kind unless @schema.nil?
91
+
92
+ load_default_values
93
+ load_schema_defaults
94
+
95
+ load(attrs)
96
+ end
97
+
98
+ def read_attribute_for_validation(key)
99
+ attributes[key]
100
+ end
101
+
102
+ def persisted?
103
+ @persisted
104
+ end
105
+
106
+ def new_record?
107
+ !persisted?
108
+ end
109
+
110
+ def destroyed?
111
+ @destroyed
112
+ end
113
+
114
+ alias :id :name
115
+ alias :id= :name=
116
+
117
+ def known_attributes
118
+ schema.try(:known_attributes) || []
119
+ end
120
+
121
+ def method_missing(method_symbol, *arguments)
122
+ method_name = method_symbol.to_s
123
+
124
+ if method_name =~ /(=|\?)$/
125
+ case $1
126
+ when "="
127
+ attributes[$`] = arguments.first
128
+ when "?"
129
+ attributes[$`]
130
+ end
131
+ else
132
+ return attributes[method_name] if attributes.include?(method_name)
133
+ # not set right now but we know about it
134
+ return nil if known_attributes.include?(method_name)
135
+ super
136
+ end
137
+ end
138
+
139
+ def generate_name
140
+ "#{kind || 'channel'}-#{Time.now.strftime('%Y%m%d%H%M%S%3N')}"
141
+ end
142
+
143
+ def generate_name!
144
+ self.name = generate_name
145
+ end
146
+
147
+ def [](key)
148
+ attributes[key]
149
+ end
150
+
151
+ def []=(key, value)
152
+ attributes[key] = value
153
+ end
154
+
155
+ def read_attribute(attr_name)
156
+ find_attr_recursive(attr_name, attributes)
157
+ end
158
+
159
+ def write_attribute(attr_name, value)
160
+ hash, key = find_attr_ref_recursive(attr_name, attributes)
161
+ hash && hash[key] = value
162
+ end
163
+
164
+ def assign_attributes(new_attributes)
165
+ return if new_attributes.blank?
166
+
167
+ new_attributes = new_attributes.stringify_keys
168
+ new_attributes.each do |key, value|
169
+ if value.is_a?(Hash)
170
+ write_attribute(key, read_attribute(key).merge(value))
171
+ else
172
+ write_attribute(key, value)
173
+ end
174
+ end
175
+ end
176
+
177
+ def attributes=(new_attributes)
178
+ return unless new_attributes.is_a?(Hash)
179
+ assign_attributes(new_attributes)
180
+ end
181
+
182
+ def human_attribute_name(attr_name, options = {})
183
+ (!schema.nil? && schema.find_attribute(attr_name).try(:humanized_name)) ||
184
+ self.class.human_attribute_name(attr_name, options)
185
+ end
186
+
187
+ def save
188
+ raise NotImplementedError
189
+ end
190
+
191
+ def save!
192
+ save || raise(ChannelInvalid.new(self))
193
+ end
194
+
195
+ def destroy
196
+ raise NotImplementedError
197
+ end
198
+
199
+ private
200
+
201
+ def load_default_values
202
+ end
203
+
204
+ def load_schema_defaults
205
+ load schema.default_values unless schema.nil?
206
+ end
207
+
208
+ def load(attributes)
209
+ (attributes || {}).each do |key, value|
210
+ @attributes[key.to_s] = case value
211
+ when Hash
212
+ (@attributes[key.to_s] || {}).merge(value)
213
+ else
214
+ value.duplicable? ? value.dup : value
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
220
+
@@ -0,0 +1,64 @@
1
+ module Pigeon
2
+ class ChannelAttribute < NestedAttribute
3
+ attr_reader :type, :default_value, :humanized_name, :label, :tooltip, :user_editable, :options
4
+ attr_writer :scope
5
+
6
+ def initialize(name, type, options = {})
7
+ raise ArgumentError, "name cannot be blank (scope was '#{options[:scope]}')" unless name.present?
8
+ raise ArgumentError, "invalid type #{type}" unless self.class.valid_type?(type)
9
+
10
+ @name = name
11
+ @type = type.to_sym
12
+ options = options.with_indifferent_access
13
+
14
+ @scope = options.delete(:scope)
15
+
16
+ @default_value = options['default_value'] || options['value']
17
+ @humanized_name = options['humanized_name'] || options['display']
18
+ @label = options['label']
19
+ if @label.present?
20
+ @humanized_name ||= @label
21
+ elsif @humanized_name.present?
22
+ @label ||= @humanized_name
23
+ else
24
+ @label = name.to_s.humanize
25
+ @humanized_name = @label
26
+ end
27
+ @tooltip = options['tooltip']
28
+ @user_editable = options['user'].nil? ? true : options['user']
29
+ @options = options['options'] || []
30
+ end
31
+
32
+ def self.valid_type?(type)
33
+ %w(string boolean password enum integer timezone hidden).include?(type.to_s)
34
+ end
35
+
36
+ def self.build_default(name, hint_value = nil)
37
+ type = case hint_value
38
+ when Fixnum
39
+ :integer
40
+ when FalseClass, TrueClass
41
+ :boolean
42
+ else
43
+ :string
44
+ end
45
+ name, scope = build_scope_recursive(name)
46
+ new name, type, value: hint_value, scope: scope
47
+ end
48
+
49
+ private
50
+
51
+ extend NestedScopes
52
+
53
+ def self.build_scope_recursive(name, scope = nil)
54
+ fold_scoped_name(name, [nil, scope]) do |(current, scope), name|
55
+ if current.nil?
56
+ [name, scope]
57
+ else
58
+ [name, NestedAttribute.new(current, scope)]
59
+ end
60
+ end
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,113 @@
1
+ module Pigeon
2
+ class ChannelSchema
3
+ include NestedScopes
4
+
5
+ class << self
6
+ def from_hash(type, hash)
7
+ self.new type, hash["kind"], hash["humanized_name"], hash["attributes"], hash["template"]
8
+ end
9
+
10
+ def list_from_hash(type, hash)
11
+ hash.map do |name, data|
12
+ data["kind"] ||= name
13
+ from_hash type, data
14
+ end
15
+ end
16
+ end
17
+
18
+ attr_reader :type, :kind, :humanized_name, :attribute_data, :template_data
19
+
20
+ def initialize(type, kind, humanized_name = nil, attributes = [], template = nil)
21
+ raise ArgumentError, "type cannot be blank" if type.blank?
22
+ raise ArgumentError, "kind cannot be blank" if kind.blank?
23
+
24
+ @type = type
25
+ @kind = kind
26
+ @humanized_name = humanized_name || kind
27
+ @attribute_data = attributes
28
+ @template_data = template
29
+ end
30
+
31
+ def attributes
32
+ @attributes ||= process_attributes(@attribute_data)
33
+ end
34
+
35
+ def known_attributes
36
+ attributes.keys
37
+ end
38
+
39
+ def user_attributes
40
+ flattened_map_filter(attributes) do |attr|
41
+ attr.user_editable ? attr.scoped_name : nil
42
+ end
43
+ end
44
+
45
+ def default_values
46
+ recursive_map_filter(attributes, &:default_value)
47
+ end
48
+
49
+ def template
50
+ @template ||= @template_data ? process_template(@template_data) : default_template
51
+ end
52
+
53
+ def type_kind
54
+ "#{type}/#{kind}"
55
+ end
56
+
57
+ def default_template
58
+ ["@template"] + user_attributes.map do |attr_name|
59
+ ["@attr", attr_name]
60
+ end
61
+ end
62
+
63
+ def find_attribute(name)
64
+ find_attr_recursive(name, attributes)
65
+ end
66
+
67
+ private
68
+
69
+ # recursive map-filter of the channel schema attributes
70
+ def flattened_map_filter(attributes, &block)
71
+ attributes.inject([]) do |result, (name, attr)|
72
+ if attr.is_a? Hash
73
+ value = flattened_map_filter(attr, &block)
74
+ result + value
75
+ else
76
+ value = yield(attr)
77
+ result + [value].reject(&:nil?)
78
+ end
79
+ end
80
+ end
81
+
82
+ def recursive_map_filter(attributes, &block)
83
+ attributes.inject({}) do |result, (name, attr)|
84
+ if attr.is_a? Hash
85
+ value = recursive_map_filter(attr, &block)
86
+ else
87
+ value = yield(attr)
88
+ end
89
+ result[name] = value if value.present?
90
+ result
91
+ end
92
+ end
93
+
94
+ def process_attributes(attributes, scope = nil)
95
+ Hash[attributes.map do |attr|
96
+ name = attr["name"]
97
+ if attr["attributes"].present?
98
+ # recursively process nested attributes
99
+ attribute = process_attributes(attr["attributes"], NestedAttribute.new(name, scope))
100
+ else
101
+ type = attr["type"] || "string"
102
+ attribute = Pigeon::ChannelAttribute.new(name, type, attr)
103
+ attribute.scope = scope
104
+ end
105
+ [name, attribute]
106
+ end].with_indifferent_access
107
+ end
108
+
109
+ def process_template(template)
110
+ template
111
+ end
112
+ end
113
+ end