infopark_reactor_migrations 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +165 -0
  4. data/README +64 -0
  5. data/Rakefile +19 -0
  6. data/infopark_reactor_migrations.gemspec +27 -0
  7. data/lib/generators/cm/migration/USAGE +8 -0
  8. data/lib/generators/cm/migration/migration_generator.rb +15 -0
  9. data/lib/generators/cm/migration/templates/template.rb +7 -0
  10. data/lib/infopark_reactor_migrations.rb +29 -0
  11. data/lib/reactor/cm/attribute.rb +84 -0
  12. data/lib/reactor/cm/bridge.rb +49 -0
  13. data/lib/reactor/cm/editorial_group.rb +22 -0
  14. data/lib/reactor/cm/group.rb +270 -0
  15. data/lib/reactor/cm/language.rb +56 -0
  16. data/lib/reactor/cm/link.rb +132 -0
  17. data/lib/reactor/cm/live_group.rb +22 -0
  18. data/lib/reactor/cm/missing_credentials.rb +7 -0
  19. data/lib/reactor/cm/obj.rb +402 -0
  20. data/lib/reactor/cm/obj_class.rb +186 -0
  21. data/lib/reactor/cm/object_base.rb +164 -0
  22. data/lib/reactor/cm/user.rb +100 -0
  23. data/lib/reactor/cm/workflow.rb +40 -0
  24. data/lib/reactor/cm/xml_attribute.rb +35 -0
  25. data/lib/reactor/cm/xml_markup.rb +85 -0
  26. data/lib/reactor/cm/xml_request.rb +82 -0
  27. data/lib/reactor/cm/xml_request_error.rb +16 -0
  28. data/lib/reactor/cm/xml_response.rb +41 -0
  29. data/lib/reactor/configuration.rb +7 -0
  30. data/lib/reactor/migration.rb +82 -0
  31. data/lib/reactor/migrations/railtie.rb +10 -0
  32. data/lib/reactor/migrations/version.rb +5 -0
  33. data/lib/reactor/plans/common_attribute.rb +32 -0
  34. data/lib/reactor/plans/common_group.rb +44 -0
  35. data/lib/reactor/plans/common_obj_class.rb +69 -0
  36. data/lib/reactor/plans/create_attribute.rb +32 -0
  37. data/lib/reactor/plans/create_group.rb +34 -0
  38. data/lib/reactor/plans/create_obj.rb +48 -0
  39. data/lib/reactor/plans/create_obj_class.rb +28 -0
  40. data/lib/reactor/plans/delete_attribute.rb +23 -0
  41. data/lib/reactor/plans/delete_group.rb +28 -0
  42. data/lib/reactor/plans/delete_obj.rb +22 -0
  43. data/lib/reactor/plans/delete_obj_class.rb +22 -0
  44. data/lib/reactor/plans/prepared.rb +15 -0
  45. data/lib/reactor/plans/rename_group.rb +32 -0
  46. data/lib/reactor/plans/rename_obj_class.rb +24 -0
  47. data/lib/reactor/plans/update_attribute.rb +23 -0
  48. data/lib/reactor/plans/update_group.rb +30 -0
  49. data/lib/reactor/plans/update_obj.rb +30 -0
  50. data/lib/reactor/plans/update_obj_class.rb +26 -0
  51. data/lib/reactor/tools/migrator.rb +135 -0
  52. data/lib/reactor/tools/response_handler/base.rb +22 -0
  53. data/lib/reactor/tools/response_handler/string.rb +19 -0
  54. data/lib/reactor/tools/response_handler/xml_attribute.rb +52 -0
  55. data/lib/reactor/tools/smart_xml_logger.rb +69 -0
  56. data/lib/reactor/tools/sower.rb +89 -0
  57. data/lib/reactor/tools/uploader.rb +131 -0
  58. data/lib/reactor/tools/versioner.rb +120 -0
  59. data/lib/reactor/tools/xml_attributes.rb +70 -0
  60. data/lib/tasks/cm_migrate.rake +8 -0
  61. data/lib/tasks/cm_seeds.rake +41 -0
  62. metadata +193 -0
@@ -0,0 +1,135 @@
1
+ require 'reactor/tools/versioner'
2
+
3
+ module Reactor
4
+ # Class responsible for running a single migration, a helper for Migrator
5
+ class MigrationProxy
6
+ def initialize(versioner, name, version, direction, filename)
7
+ @versioner = versioner
8
+ @name = name
9
+ @version = version
10
+ @filename = filename
11
+ @direction = direction
12
+ end
13
+
14
+ def load_migration
15
+ load @filename
16
+ end
17
+
18
+ def run
19
+ return down if @direction.to_sym == :down
20
+ return up
21
+ end
22
+
23
+ def up
24
+ if @versioner.applied?(@version) then
25
+ puts "Migrating up: #{@name} (#{@filename}) already applied, skipping"
26
+ return true
27
+ else
28
+ result = class_name.send(:up) and @versioner.add(@version)
29
+ class_name.contained.each do |version|
30
+ puts "#{class_name.to_s} contains migration #{version}"
31
+ #@versioner.add(version) # not neccesary!
32
+ end if result
33
+ result
34
+ end
35
+ end
36
+
37
+ def down
38
+ result = class_name.send(:down) and @versioner.remove(@version)
39
+ class_name.contained.each do |version|
40
+ puts "#{class_name.to_s} contains migration #{version}"
41
+ @versioner.remove(version)
42
+ end if result
43
+ result
44
+ end
45
+
46
+ def class_name
47
+ return Kernel.const_get(@name)
48
+ end
49
+
50
+ def name
51
+ @name
52
+ end
53
+
54
+ def version
55
+ @version
56
+ end
57
+
58
+ def filename
59
+ @filename
60
+ end
61
+ end
62
+
63
+ # Migrator is responsible for running migrations.
64
+ #
65
+ # <b>You should not use this class directly! Use rake cm:migrate instead.</b>
66
+ #
67
+ # Migrating to a specific version is possible by specifing VERSION environment
68
+ # variable: rake cm:migrate VERSION=0
69
+ # Depending on your current version migrations will be run up
70
+ # (target version > current version) or down (target version < current version)
71
+ #
72
+ # MIND THE FACT, that you land at the version <i>nearest</i> to target_version
73
+ # (possibly target version itself)
74
+ class Migrator
75
+ # Constructor takes two parameters migrations_path (relative path of migration files)
76
+ # and target_version (an integer or nil).
77
+ #
78
+ # Used by a rake task.
79
+ def initialize(migrations_path, target_version=nil)
80
+ @migrations_path = migrations_path
81
+ @target_version = target_version.to_i unless target_version.nil?
82
+ @target_version = 99999999999999 if target_version.nil?
83
+ @versioner = Versioner.instance
84
+ end
85
+
86
+ # Runs the migrations in proper direction (up or down)
87
+ # Ouputs current version when done
88
+ def migrate
89
+ return up if @target_version.to_i > current_version.to_i
90
+ return down
91
+ end
92
+
93
+ def up
94
+ rem_migrations = migrations.reject do |version, name, file|
95
+ version.to_i > @target_version.to_i or applied?(version)
96
+ end
97
+ run(rem_migrations, :up)
98
+ end
99
+
100
+ def down
101
+ rem_migrations = migrations.reject do |version, name, file|
102
+ version.to_i <= @target_version.to_i or not applied?(version)
103
+ end
104
+ run(rem_migrations.reverse, :down)
105
+ end
106
+
107
+ def migrations
108
+ files = Dir["#{@migrations_path}/[0-9]*_*.rb"].sort.collect do |file|
109
+ version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
110
+ [version, name, file]
111
+ end
112
+ end
113
+
114
+ def applied?(version)
115
+ @versioner.applied?(version)
116
+ end
117
+
118
+ def current_version
119
+ @versioner.current_version
120
+ end
121
+
122
+ def run(rem_migrations, direction)
123
+ begin
124
+ rem_migrations.each do |version, name, file|
125
+ migration = MigrationProxy.new(@versioner, name.camelize, version, direction, file)
126
+ puts "Migrating #{direction.to_s}: #{migration.name} (#{migration.filename})"
127
+ migration.load_migration and migration.run or raise "Migrating #{direction.to_s}: #{migration.name} (#{migration.filename}) failed"
128
+ end
129
+ ensure
130
+ puts "At version: " + @versioner.current_version.to_s
131
+ puts "WARNING: Could not store applied migrations!" if not @versioner.store
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,22 @@
1
+ module Reactor
2
+
3
+ module ResponseHandler
4
+
5
+ # Common base class to handle a xml response. Provides helper methods to extract the content
6
+ # from a xml response.
7
+ class Base
8
+
9
+ attr_accessor :response
10
+ attr_accessor :context
11
+
12
+ # Common strategy method for each sub class.
13
+ def get(response, context)
14
+ @response = response
15
+ @context = context
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,19 @@
1
+ require 'reactor/tools/response_handler/base'
2
+
3
+ module Reactor
4
+
5
+ module ResponseHandler
6
+
7
+ class String < Base
8
+
9
+ def get(response, string)
10
+ super(response, string)
11
+
12
+ self.response.xpath(string)
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,52 @@
1
+ require 'reactor/tools/response_handler/base'
2
+
3
+ module Reactor
4
+
5
+ module ResponseHandler
6
+
7
+ class XmlAttribute < Base
8
+
9
+ def get(response, attribute)
10
+ super(response, attribute)
11
+
12
+ name = attribute.name
13
+ type = attribute.type
14
+
15
+ method_name = "extract_#{type}"
16
+
17
+ self.send(method_name, name)
18
+ end
19
+
20
+ private
21
+
22
+ # Extracts a string value with the given +name+ and returns a string.
23
+ def extract_string(name)
24
+ self.response.xpath("//#{name}/text()").to_s
25
+ end
26
+
27
+ # Extracts a list value with the given +name+ and returns an array of strings.
28
+ def extract_list(name)
29
+ result = self.response.xpath("//#{name}/listitem/text()")
30
+ result = result.kind_of?(Array) ? result : [result]
31
+
32
+ result.map(&:to_s)
33
+ end
34
+
35
+ # This shit will break with the slightest change of the CM.
36
+ def extract_signaturelist(name)
37
+ signatures = []
38
+ self.response.xpath("//#{name}/").each do |potential_signature|
39
+ if (potential_signature.name.to_s == "listitem")
40
+ attribute = potential_signature.children.first.text.to_s
41
+ group = potential_signature.children.last.text.to_s
42
+ signatures << {:attribute => attribute, :group => group}
43
+ end
44
+ end
45
+ signatures
46
+ end
47
+
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,69 @@
1
+ require 'nokogiri'
2
+ require 'term/ansicolor'
3
+
4
+ class SmartXmlLogger
5
+
6
+ include Term::ANSIColor
7
+
8
+ def initialize(forward_to, method = nil)
9
+ @logger = forward_to
10
+ @method = method
11
+ end
12
+
13
+ def configure(key, options)
14
+ @configuration ||= {}
15
+ @configuration[key] = options
16
+ end
17
+
18
+ def log(text)
19
+ return unless @logger
20
+ @logger.send(@method, text)
21
+ end
22
+
23
+ def log_xml(key, xml)
24
+ return unless @logger
25
+
26
+ options = @configuration[key]
27
+
28
+ dom = Nokogiri::XML::Document.parse(xml)
29
+
30
+ node_set = options[:xpath] ? dom.xpath(options[:xpath]) : dom
31
+
32
+ self.log(if node_set.respond_to?(:each)
33
+ node_set.map{|node| self.print_node(node, options[:start_indent] || 0)}.join
34
+ else
35
+ self.print_node(node_set, options[:start_indent] || 0)
36
+ end)
37
+ end
38
+
39
+ #private
40
+
41
+ def print_node(node, indent = 0)
42
+ return '' if node.text?
43
+
44
+ empty = node.children.empty?
45
+ has_text = node.children.detect{|child| child.text?}
46
+
47
+ out = ' ' * indent
48
+
49
+ attrs = node.attributes.values.map{|attr| %|#{attr.name}="#{red(attr.value)}"|}.join(' ')
50
+ attrs = " #{attrs}" if attrs.present?
51
+
52
+ out << "<#{green(node.name)}#{attrs}#{'/' if empty}>"
53
+
54
+ if has_text
55
+ out << "#{red(node.text)}"
56
+ else
57
+ out << "\n"
58
+ end
59
+
60
+ node.children.each do |child|
61
+ out << self.print_node(child, indent + 2)
62
+ end
63
+
64
+ out << ' ' * indent unless has_text || empty
65
+ out << "</#{green(node.name)}>\n" unless empty
66
+ out
67
+ end
68
+
69
+ end
@@ -0,0 +1,89 @@
1
+ module Reactor
2
+
3
+ class Sower
4
+ def initialize(filename)
5
+ @filename = filename
6
+ end
7
+ def sow
8
+ require @filename
9
+ end
10
+ end
11
+
12
+ end
13
+
14
+ class SeedObject < RailsConnector::Obj
15
+ end
16
+
17
+ module RailsConnector
18
+ class Obj
19
+
20
+ attr_accessor :keep_edited
21
+
22
+ def self.plant(path, &block)
23
+ obj = Obj.find_by_path(path)
24
+ raise ActiveRecord::RecordNotFound.new('plant: Ground not found:' +path) if obj.nil?
25
+ #obj.objClass = 'Container' # TODO: enable it!
26
+ #obj.save!
27
+ #obj.release!
28
+ obj.send(:reload_attributes)
29
+ obj.instance_eval(&block) if block_given?
30
+ # ActiveRecord is incompatible with changing the obj class, therefore you get RecordNotFound
31
+ begin
32
+ obj.save!
33
+ rescue ActiveRecord::RecordNotFound
34
+ end
35
+ obj.release unless obj.keep_edited || !Obj.last.edited?
36
+ obj
37
+ end
38
+
39
+ # creates of fetches an obj with given name (within context),
40
+ # executes a block on it (instance_eval)
41
+ # saves and releases (unless keep_edited = true was called)
42
+ # the object afterwards
43
+ def obj(name, objClass = 'Container', &block)
44
+ obj = Obj.find_by_path(File.join(self.path.to_s, name.to_s))
45
+ if obj.nil?
46
+ obj = Obj.create(:name => name, :parent => self.path, :obj_class => objClass)
47
+ else
48
+ obj = Obj.find_by_path(File.join(self.path.to_s, name.to_s))
49
+ if obj.obj_class != objClass
50
+ obj.obj_class = objClass
51
+ begin
52
+ obj.save!
53
+ rescue ActiveRecord::RecordNotFound
54
+ end
55
+ obj = Obj.find_by_path(File.join(self.path.to_s, name.to_s))
56
+ end
57
+ end
58
+ obj.send(:reload_attributes, objClass)
59
+ obj.instance_eval(&block) if block_given?
60
+ obj.save!
61
+ obj.release unless obj.keep_edited || !Obj.last.edited?
62
+ obj
63
+ end
64
+
65
+ def self.with(path, objClass = 'Container', &block)
66
+ splitted_path = path.split('/')
67
+ name = splitted_path.pop
68
+ # ensure path exists
69
+ (splitted_path.length).times do |i|
70
+ subpath = splitted_path[0,(i+1)].join('/').presence || '/'
71
+ subpath_parent = splitted_path[0,i].join('/').presence || '/'
72
+ subpath_name = splitted_path[i]
73
+ create(:name => subpath_name, :parent => subpath_parent, :obj_class => 'Container') unless Obj.find_by_path(subpath) unless subpath_name.blank?
74
+ end
75
+ parent_path = splitted_path.join('/').presence || '/'
76
+ parent = Obj.find_by_path(parent_path)
77
+ parent.obj(name, objClass, &block)
78
+ end
79
+
80
+ def do_not_release!
81
+ @keep_edited = true
82
+ end
83
+
84
+ def t(key, opts={})
85
+ I18n.t(key, opts)
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,131 @@
1
+ module Reactor
2
+ module Tools
3
+ class Uploader
4
+
5
+ attr_reader :cm_obj
6
+
7
+ def initialize(cm_obj)
8
+ self.cm_obj = cm_obj
9
+ end
10
+
11
+ # Uses streaming interface to upload data from
12
+ # given IO stream or memory location.
13
+ # Extension is used as basis for content detection.
14
+ # Larger file transfers should be executed through IO
15
+ # streams, which conserve memory.
16
+ #
17
+ # After the data has been successfuly transfered to
18
+ # streaming interface it stores contentType and resulting
19
+ # ticket into Reactor::Cm::Obj provided on initialization.
20
+ #
21
+ # NOTE: there is a known bug for Mac OS X: if you are
22
+ # uploading more files (IO objects) in sequence,
23
+ # the upload may fail randomly. For this platform
24
+ # and this case fallback to memory streaming is used.
25
+ def upload(data_or_io, extension)
26
+ if (data_or_io.kind_of?IO)
27
+ io = data_or_io
28
+ begin
29
+ ticket_id = stream_io(io, extension)
30
+ rescue Errno::EINVAL => e
31
+ if RUBY_PLATFORM.downcase.include?("darwin")
32
+ # mac os x is such a piece of shit
33
+ # writing to a socket can fail with EINVAL, randomly without
34
+ # visible reason when using body_stream
35
+ # in this case fallback to memory upload which always works (?!?!)
36
+ Reactor::Cm::LOGGER.log "MacOS X bug detected for #{io.inspect}"
37
+ io.rewind
38
+ return upload(io.read, extension)
39
+ else
40
+ raise e
41
+ end
42
+ end
43
+ else
44
+ ticket_id = stream_data(data_or_io, extension)
45
+ end
46
+
47
+ cm_obj.set(:contentType, extension)
48
+ cm_obj.set(:blob, {ticket_id=>{:encoding=>'stream'}})
49
+
50
+ ticket_id
51
+ end
52
+
53
+ protected
54
+
55
+ attr_writer :cm_obj
56
+
57
+ # Stream into CM from memory. Used in cases when the file
58
+ # has already been read into memory
59
+ def stream_data(data, extension)
60
+ response, ticket_id = (Net::HTTP.new(self.class.streaming_host, self.class.streaming_port).post('/stream', data,
61
+ {'Content-Type' => self.class.content_type_for_ext(extension)}))
62
+
63
+ handle_response(response, ticket_id)
64
+ end
65
+
66
+ # Stream directly an IO object into CM. Uses minimal memory,
67
+ # as the IO is read in 1024B-Blocks
68
+ def stream_io(io, extension)
69
+ request = Net::HTTP::Post.new('/stream')
70
+ request.body_stream = io
71
+ request.content_length = read_io_content_length(io)
72
+ request.content_type = self.class.content_type_for_ext(extension)
73
+
74
+ response, ticket_id = nil, nil
75
+ Net::HTTP.start(self.class.streaming_host, self.class.streaming_port) do |http|
76
+ http.read_timeout = 60
77
+ #http.set_debug_output $stderr
78
+ response, ticket_id = http.request(request)
79
+ end
80
+
81
+ handle_response(response, ticket_id)
82
+ end
83
+
84
+ # Returns ticket_id if response if one of success (success or redirect)
85
+ def handle_response(response, ticket_id)
86
+ if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPRedirection)
87
+ ticket_id
88
+ else
89
+ nil
90
+ end
91
+ end
92
+
93
+ # Returns the size of the IO stream.
94
+ # The underlying stream must support either
95
+ # the :stat method or be able to seek to
96
+ # random position
97
+ def read_io_content_length(io)
98
+ if (io.respond_to?(:stat))
99
+ # For files it is easy to read the filesize
100
+ return io.stat.size
101
+ else
102
+ # For streams it is not. We seek to end of
103
+ # the stream, read the position, and rewind
104
+ # to the previous location
105
+ old_pos = io.pos
106
+ io.seek(0, IO::SEEK_END)
107
+ content_length = io.pos
108
+ io.seek(old_pos, IO::SEEK_SET)
109
+
110
+ content_length
111
+ end
112
+ end
113
+
114
+ def self.streaming_host
115
+ Reactor::Configuration.xml_access[:host]
116
+ end
117
+
118
+ def self.streaming_port
119
+ Reactor::Configuration.xml_access[:port]
120
+ end
121
+
122
+ # It should theoretically return correct/matching
123
+ # mime type for given extension. But since the CM
124
+ # accepts 'application/octet-stream', no extra logic
125
+ # or external dependency is required.
126
+ def self.content_type_for_ext(extension)
127
+ 'application/octet-stream'
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,120 @@
1
+ require 'base64'
2
+ require 'yaml'
3
+ require 'singleton'
4
+
5
+ module Reactor
6
+ # Class responsible for interfacing with version-storing mechanism
7
+ class Versioner
8
+ include Singleton
9
+
10
+ # Slave class used by Versioner class to load and store migrated files
11
+ # inside the CM. It uses separate object type named "version_store"
12
+ # and stores data as base64'ed YAML inside recordSetCallback
13
+ # (Versionszuweisungsfunktion).
14
+ # Theoretically you could use any class for this purpose, but you would
15
+ # lose the ability to set recordSetCallback for this class. Other than
16
+ # that, it does not affect the object class in any way!
17
+ #
18
+ # Maybe the future version won't disrupt even this fuction.
19
+ class Slave
20
+ def name
21
+ "version_store"
22
+ end
23
+
24
+ def base_name
25
+ "objClass"
26
+ end
27
+
28
+ def exists?
29
+ begin
30
+ request = Reactor::Cm::XmlRequest.prepare do |xml|
31
+ xml.where_key_tag!(base_name, 'name', name)
32
+ xml.get_key_tag!(base_name, 'name')
33
+ end
34
+ response = request.execute!
35
+ return response.ok?
36
+ rescue
37
+ return false
38
+ end
39
+ end
40
+
41
+ def load
42
+ create if not exists?
43
+ request = Reactor::Cm::XmlRequest.prepare do |xml|
44
+ xml.where_key_tag!(base_name, 'name', name)
45
+ xml.get_key_tag!(base_name, 'recordSetCallback')
46
+ end
47
+ response = request.execute!
48
+ base64 = response.xpath("//recordSetCallback").text.to_s
49
+ yaml = Base64::decode64(base64)
50
+ data = YAML::load(yaml)
51
+ return [] if data.nil? or data == false
52
+ return data.to_a
53
+ end
54
+
55
+ def store(data)
56
+ create if not exists?
57
+ yaml = data.to_yaml
58
+ base64 = Base64::encode64(yaml).gsub("\n", '').gsub("\r", '')
59
+ content = '#' + base64
60
+ request = Reactor::Cm::XmlRequest.prepare do |xml|
61
+ xml.where_key_tag!(base_name, 'name', name)
62
+ xml.set_key_tag!(base_name, 'recordSetCallback', content)
63
+ end
64
+ response = request.execute!
65
+ response.ok?
66
+ end
67
+
68
+ def create
69
+ request = Reactor::Cm::XmlRequest.prepare do |xml|
70
+ xml.create_tag!(base_name) do
71
+ xml.tag!('name') do
72
+ xml.text!(name)
73
+ end
74
+ xml.tag!('objType') do
75
+ xml.text!('document')
76
+ end
77
+ end
78
+ end
79
+ response = request.execute!
80
+ response.ok?
81
+ end
82
+ end
83
+
84
+ def initialize
85
+ @versions = []
86
+ @backend = Slave.new
87
+ load
88
+ end
89
+
90
+ def load
91
+ @versions = @backend.load
92
+ end
93
+
94
+ def store
95
+ @backend.store(@versions)
96
+ end
97
+
98
+ def applied?(version)
99
+ @versions.include? version.to_s
100
+ end
101
+
102
+ def add(version)
103
+ @versions << version.to_s
104
+ end
105
+
106
+ def remove(version)
107
+ not @versions.delete(version.to_s).nil?
108
+ end
109
+
110
+ def versions
111
+ @versions
112
+ end
113
+
114
+ def current_version
115
+ current = @versions.sort.reverse.first
116
+ return 0 if current.nil?
117
+ return current
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,70 @@
1
+ require 'reactor/cm/xml_attribute'
2
+ require 'reactor/tools/response_handler/xml_attribute'
3
+
4
+ module Reactor
5
+
6
+ module XmlAttributes
7
+
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ class_attribute :_attributes
12
+ self._attributes = {}
13
+
14
+ class_attribute :response_handler
15
+ self.response_handler = ResponseHandler::XmlAttribute.new
16
+ end
17
+
18
+ module ClassMethods
19
+
20
+ # This method can act as both getter and setter.
21
+ # I admit, that it is not the best design ever.
22
+ # But it makes a pretty good DSL
23
+ def primary_key(new_value = nil)
24
+ if new_value.nil?
25
+ @primary_key
26
+ else
27
+ @primary_key = new_value.to_s
28
+ @primary_key
29
+ end
30
+ end
31
+
32
+ def attribute(name, options = {})
33
+ xml_name = options.delete(:name).presence || name
34
+ type = options.delete(:type).presence
35
+
36
+ attribute = Reactor::Cm::XmlAttribute.new(xml_name, type, options)
37
+
38
+ self._attributes[name.to_sym] = attribute
39
+
40
+ attr_accessor name
41
+ end
42
+
43
+ def attributes(scopes = [])
44
+ scopes = Array(scopes)
45
+ attributes = self._attributes
46
+
47
+ if scopes.present?
48
+ attributes.reject { |_, xml_attribute| (xml_attribute.scopes & scopes).blank? }
49
+ else
50
+ attributes
51
+ end
52
+ end
53
+
54
+ def xml_attribute(name)
55
+ self._attributes[name.to_sym]
56
+ end
57
+
58
+ def xml_attribute_names
59
+ self._attributes.values.map(&:name)
60
+ end
61
+
62
+ def attribute_names
63
+ self._attributes.keys
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+
70
+ end