instedd-pigeon 0.1.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.
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